Chapter 17. Using jQuery in Large Projects

Rob Burns

Introduction

jQuery is often used to add small user interface enhancements to a website. However, for larger, more complex web applications, jQuery is also quite useful. The sample recipes throughout this chapter show how jQuery can be used to address the needs of more substantial and interactive web content. The first three recipes explore different methods of persisting data in a web browser. These are followed by a look at easing the use of Ajax and JavaScript as the quantity of code and data in your application grows.

17.1. Using Client-Side Storage

Problem

You are writing a rich Internet application that processes nontrivial amounts of user data in the web browser. Motivated by the desire to cache this data for performance reasons or to enable offline use of your application, you need to store data on the client.

Solution

A simple to-do list will be used to illustrate storing data on the client. As with many of the recipes in this chapter, a jQuery plugin will be used to handle browser inconsistencies:

<!DOCTYPE html>
<html><head>
    <title>17.1 - Using Client-Side Storage</title>
    <script type="text/javascript" src="../../jquery-1.3.2.min.js"></script>
    <script type="text/javascript" src="jquery.jstore-all.js"></script>
</head>

<body>
    <h1>17.1 - Using Client-Side Storage</h1>
    <p>Storage engine: <span id="storage-engine"></span></p>
    <input id="task-input"></input>
    <input id="task-add" type="submit" value="Add task"></input>
    <input id="list-clear" type="submit" value="Remove all tasks"></input>
    <ul  id="task-list"></ul>
</body></html>

The HTML consists of form elements for manipulating the to-do list: a text field to input a task, and buttons for adding a task and deleting all tasks. The current tasks will be listed using an unordered list:

(function($) {
    $.jStore.ready(function(ev,engine) {
        engine.ready(function(ev,engine) {
            $('#storage-engine').html($.jStore.CurrentEngine.type);
            $('#task-list').append($.store('task-list'));
        });
    });

The jStore plugin provides two callbacks: jStore.ready() and engine.ready(). Much like jQuery’s ready() function, these allow us to do some initial setup once jStore and the current storage engine have completed their internal initialization. This opportunity is used to display the currently used storage engine and any saved to-do items on the page:

   $('document').ready(function() {
       $('#task-add').click(function() {
           var task = $('#task-input').val();
           var taskHtml = '<li><a href="#">done</a> ' + task + '</li>';
           $.store('task-list',$('#task-list').append(taskHtml).html());
           return false;
       });

Once the document is ready, click events are bound to the appropriate controls. When the “Add task” button is clicked, a list-item element is constructed with the contents of the task text field and a link to mark this task as done. The list item is then appended to the contents of the task list, and the task list is saved in local storage using the task-list key. At a later time, the list can be retrieved using this key, as is being done in the engine.ready() callback:

      $('#list-clear').click(function() {
          $('#task-list').empty();
          $.remove('task-list'),
          return false;
      });

When the “Remove all tasks” button is clicked, the element containing the to-do list is emptied of its contents. The task-list key and its associated value are then removed from local storage:

      $('#task-list a').live('click',function() {
          $(this).parent().remove();
          var taskList = $('#task-list').html();
          if( taskList ) { $.store('task-list',taskList); }
          else { $.remove('task-list'), }
          return false;
       });
   });
})(jQuery);

Lastly, a live event is bound to the done links for each item in the to-do list. By using the live() function, instead of the bind() function or a shortcut such as click(), all elements that match #task-list a will have the given function bound to the click event, even elements that do not yet exist at the time live() is called. This allows us to insert “done” links for each new item, without rebinding the click event each time the insertion occurs.

When an item is marked as done, it is removed from the list, and the updated list saved in local storage using the task-list key. Some care needs to be taken when saving the updated list:

            if( taskList ) { $.store('task-list',taskList); }
            else { $.remove('task-list'), }

In the case that the last item in the list is being removed, the taskList variable will be empty. This causes the store() function to be evaluated as if it were called with a single parameter, not two. When store() is passed a single parameter, the value held at that key is retrieved, and the saved list is unmodified. The goal is to save an empty list. The remove() function in the else clause removes the task-list key and its associated value. This meets the goal of setting the saved state to an empty list.

Discussion

Traditionally, the only option available to store data on the client was cookies. The amount of data that can be stored in a cookie is very limited. Better alternatives now exist. The following table contains currently available storage mechanisms and their browser compatibility.

 

Firefox

Safari

Internet Explorer

DOM Storage

2.0+

no

8.0+

Gears

yes

yes

yes

Flash

yes

yes

yes

SQL Storage API

no

3.1+

no

userData behavior

no

no

5.0+

DOM Storage and the SQL Storage API are part of emerging HTML standards. As such, they don’t yet enjoy thorough cross-browser support. Google Gears and Flash are browser plugins that can be used for client-side storage. Internet Explorer has, for some time, included the userData behavior for client-side storage. If a single mechanism to support all major browsers is needed, a Flash or Google Gears–based approach offers support for the widest variety. However, it requires users to have a browser plugin installed.

Warning

The 1.0.3 release of the jStore plugin contains a bug. A typo needs to be corrected. Line 403 of jquery.jstore-all.js should read as follows:

return !!(jQuery.hasFlash('8.0.0'));

Fortunately, jStore (available at http://plugins.jquery.com/project/jStore) affords a layer of abstraction, which enables cross-browser and client-side storage and, in most cases, doesn’t rely on browser plugins. jStore provides a unified interface to the storage mechanisms listed previously. While manual selection is supported, this example illustrates jStore’s ability to automatically select the appropriate storage mechanism for the browser currently in use. When viewed in different browsers, this recipe displays the currently selected storage mechanism.

17.2. Saving Application State for a Single Session

Problem

You want to persist data on the client only until the current session is ended, i.e., the window or tab is closed.

Solution

In this example there are two HTML pages. Each page contains a set of selectable elements. When elements are selected and deselected, the state of the page is persisted. By navigating to and from the two pages, you can see how the state of any given page can be maintained as a user navigates through a website. The sessionStorage object is used for data that doesn’t require persisting between a user’s subsequent visits:

<!DOCTYPE html>
<html><head>
    <title>17.2 Saving Application State for a Single Session</title>
    <style>
        .square {
            width: 100px; height: 100px; margin: 15px;
            background-color: gray; border: 3px solid white; }
        .selected {
            border: 3px solid orange; }
    </style>
    <script src="../../jquery-1.3.2.min.js"></script>
</head>

<body>
    <h1>17.2 Saving Application State for a Single Session</h1>
    <a href="one.html">page one</a>
    <a href="two.html">page two</a>
    <div id="one" class="square"></div>
    <div id="two" class="square"></div>
    <div id="three" class="square"></div>
</body></html>

Each of the two HTML pages (one.html and two.html) have the same content. The following JavaScript code takes care of managing the state of each page, such that each page reflects past user manipulation:

jQuery(document).ready(function() {
    $('.square').each(function(){
        if( sessionStorage[window.location + this.id] == 'true' ) {
            $(this).addClass('selected'),
        }
    });

    $('.square').click(function() {
        $(this).toggleClass('selected'),
        sessionStorage[window.location + this.id] = $(this).hasClass('selected'),
        return false;
    });
});

When the document is loaded, the sessionStorage object is queried for keys comprising the current URL and the id of each of the selectable squares. Each square has a CSS class applied if appropriate. When a square is clicked, its display is affected by toggling a CSS class, and its state is persisted accordingly. Each square’s state is persisted using a key generated from the current URL and current element id pair.

Discussion

Similar session-delimited client-side storage is available when using the jStore plugin from the previous recipe. By using jStore, you gain the benefits of cross-browser compatibility. This recipe will only work in Internet Explorer 8.0 and Firefox 2.0 or higher. Safari 3.1 doesn’t have this feature, though future versions are slated to include it.

The DOM storage API is attractive in cases where broad browser compatibility isn’t a concern. Applications developed for internal company intranets may fall into this category. It is also part of the upcoming HTML5 specification. In the future its availability is likely to spread. Using a built-in storage API has the benefit of incurring no overhead from additional JavaScript code. The minified jStore plugin and jStore.swf flash component are 20 KB in size.

17.3. Saving Application State Between Sessions

Problem

You want to persist data on the client between sessions. Recipe 17.1 saves the state of the to-do list in between sessions. This recipe illustrates how to enable similar functionality without using the jStore plugin.

Solution

For the HTML part of this solution, please refer to Recipe 17.1 (as it is identical). The JavaScript is listed here:

(function($) {
    $('document').ready(function() {
        if( window.localStorage ) { appStorage = window.localStorage; }
        else { appStorage = globalStorage[location.hostname]; }

        var listHtml = appStorage['task-list'];
        $('#task-list').append(listHtml.value ? listHtml.value : listHtml);

The initial setup is somewhat more verbose than the jStore-based solution. Firefox has a nonstandard implementation of the long-term storage portion of the DOM storage API. It uses the globalStorage array, as opposed to the localStorage object to persist data between sessions. Each storage object in the globalStorage array is keyed on the domain that the current document is being served from. This code will use localStorage if it is available. Otherwise, it will fall back to globalStorage.

In the next section of code, the unordered list is populated with any existing tasks. In the jStore-based example this was a single line of code. The additional complexity here is because of Firefox’s particular behavior. A string is returned from localStorage. But, an object with two attributes, value and secure, is returned when accessing globalStorage. The value attribute is used if present. Otherwise, a string returned from localStorage is assumed:

        $('#task-add').click(function() {
            var task = $('#task-input').val();
            var taskHtml = '<li><a href="#">done</a> ' + task + '</li>';
            appStorage['task-list'] = $('#task-list').append(taskHtml).html();
            return false;
        });

        $('#list-clear').click(function() {
            $('#task-list').empty();
            appStorage['task-list'] = '';
            return false;
        });

        $('#task-list a').live('click',function() {
            $(this).parent().remove();
            appStorage['task-list'] = $('#task-list').html();
            return false;
        });
    });
})(jQuery);

The remainder of the code adds new tasks, removes tasks when marked “done,” and clears the task list by attaching events to DOM elements like the previous jStore-based recipe. However, instead of using the jStore function-based interface for manipulating persisted data, values in the appStorage object created earlier can be assigned directly. This allows the code to remove a task to be simplified.

Discussion

The DOM Storage API consists of two interfaces: sessionStorage and localStorage. Firefox has included this feature since version 2.0, when the standard was still in development. Since then, the standard has undergone revision. Internet Explorer 8.0 has an implementation of the current API. Forthcoming versions of Safari and Firefox will conform to the current specification as well. That said, Firefox 2.0–3.0 browsers will persist for some time. Coding an application to support globalStorage will additionally serve these legacy browsers.

17.4. Using a JavaScript Template Engine

Problem

You want to use a JavaScript template engine to display JSON data.

Solution

This recipe is a book listing. It grabs information about a book from a server-side script and adds it to a list of books displayed in the browser. The book details are returned from the server as a JSON string. The Pure templating engine (available at http://plugins.jquery.com/project/pure) is used to format the data and insert it into the web page:

<!DOCTYPE html>
<html><head>
    <title>jQuery Cookbook - 17.4 Using a Javascript Template Engine</title>
    <style>.hidden { display: none }</style>
    <script type="text/javascript" src="../../jquery-1.3.2.min.js"></script>
    <script type="text/javascript" src="pure.js"></script>
</head>

<body>
    <h1>17.4 - Using a Javascript Template Engine</h1>
    <input type="button" id="add-book" value="Add book"></input>
    <input type="button" id="clear-list" value="Clear booklist"></input>
    <div id="book-list"></div>

There are two buttons. One will fetch book details from the server when clicked. The other will clear the locally displayed book list. The book list will be displayed inside a <div> element with an id of book-list. These elements are visible when the page is loaded:

     <div id="book-template" class="hidden book">
        <ul class="author-list"><li class="author"><span class="name"></span>
</li></ul>
        <p class="title"></p>
        <p class="year"></p>
        <div class='book-footer'>
            <div class="rating-div">Rating: <span class="rating"></span></div>
            <div>Location: <span class="location"></span></div>
        </div>
    </div>
</body></html>

The <div> with an id of book-template has a class hidden assigned to it. This <div> is not displayed. It will be used as a template for the data received from the server. The Pure templating engine associates attributes in a data structure with HTML elements that have the same class. Therefore, the contents of the paragraph element with class year will reflect the value of the year attribute in our data structure:

{
    "title": "Democracy and the Post-Totalitarian Experience",
    "author": [
        {
            "name": "Leszek Koczanowicz"
        },
        {
            "name": "Beth J. Singer"
        }
    ],
    "year": "2005",
    "rating": "3",
    "location": "Mandalay"
}

The preceding code is an example of the JSON data that is returned from the server. The title, year, rating, and location attributes have a single value and map directly to a single element in the HTML template. In order to repeat any of these values more than once, one only has to assign the appropriate class to additional elements in the template.

The author attribute contains an array of objects. Each object has a single attribute: name. Multiple authors are represented this way in order to illustrate the iteration capabilities of the templating engine. The template contains a single list item element with class author. The list item contains a <span> element with class <name>. For attributes within the data structure that have an array value, an instance of the associated HTML element will be created for each element of the array. In this way, an arbitrary number of list items can be created:

(function($) {
    $('document').ready(function() {
        $('#add-book').data('id',1);

Once the document is ready, the JavaScript code starts by using the jQuery data() function to store the current id of the book we will be requesting. This id will be incremented each time a book is requested. The data() function allows arbitrary data to be stored in DOM elements:

        $('#add-book').click(function() {
            var curId = $(this).data('id'),
            $.getJSON('server.php', {id: +curId}, function(data) {
                if( data.none ) { return false; }
                var divId = 'book-' + curId;
                $('#book-list').append($('#book-template').clone().attr('id',divId));
                $('#'+divId).autoRender(data).removeClass('hidden'),
                $('#add-book').data('id', curId + 1);
            });
            return false;
        });

When the “Add book” button is clicked, a request is made to the server using the jQuery getJSON() function. The templating process starts by making a clone of the hidden <div> in our HTML. The id of this clone must be changed before it is appended to the book list. If the id isn’t changed, then a DOM element with a non-unique id will have been introduced. The autoRender() function from the Pure plugin is then called with the JSON data as an argument. This renders the template using the provided data. Lastly, the hidden class is removed, making the book details visible:

        $('#clear-list').click(function() {
            $('#add-book').data('id',1);
            $('#book-list').empty();
            return false;
        });
    });
})(jQuery);

The function to clear the book list is fairly straightforward. The appropriate <div> element is emptied, and the book id counter is reset to 1.

Discussion

There are two benefits to using JavaScript-based templating engines. One is that they allow the transformation of a JSON data structure into styled and structured HTML without manually manipulating each element of the data structure. This benefit can be realized by applying a templating engine to the variety of small chunks of data that are commonly retrieved by Ajax calls, as this example illustrated.

The second benefit of using a JavaScript templating engine is that it produces pure HTML templates. These templates contain no traces of the scripting languages, which are usually used to denote the data to be templated, and implement functionality such as iteration. It’s difficult to take advantage of this when using the templating engine in the browser, as done in this recipe. The negative impact this has on a site’s appeal to search engines dissuades most people from going this route. However, jQuery and the Pure templating engine can be run in server-side JavaScript environments, as well. Jaxer, Rhino, and SpiderMonkey have all been known to work.

17.5. Queuing Ajax Requests

Problem

You want to have greater control over the order of many separate Ajax requests.

Solution

This recipe illustrates two different ways to queue Ajax requests. The first fills a queue with requests, sending subsequent requests once the previous request has returned a response. The second sends groups of requests in parallel. But, it doesn’t execute the callback functions for each request until all responses have returned. An example of normal unqueued requests is included for comparison:

<!DOCTYPE html>
<html><head>
    <title>jQuery Cookbook - 17.5 - Queuing Ajax Requests</title>
    <script type="text/javascript" src="../../jquery-1.3.2.min.js"></script>
    <script type="text/javascript" src="jquery-ajax-queue_1.0.js"></script>
</head>

<body>
    <h1>17.5 - Queuing Ajax Requests</h1>
    <input type="button" id="unqueued-requests" value="Unqueued requests"></input>
    <input type="button" id="queued-requests" value="Queued requests"></input>
    <input type="button" id="synced-requests" value="Synced requests"></input>
    <p id="response"></p>
</body></html>

The ajaxqueue jQuery plugin (available at http://plugins.jquery.com/project/ajaxqueue/) is used for queuing behaviors. Three buttons trigger each set of Ajax requests. A log of the responses is displayed in a paragraph element:

(function($) {
    $('document').ready(function() {
        $('#unqueued-requests').click(function() {
            $('#response').empty();
            $.each([1,2,3,4,5,6,7,8,9,10], function() {
                $.get('server.php',{ data: this }, function(data) {
                    $('#response').append(data);
                });
            });
            return false;
        });

The first button triggers normal Ajax requests. Ten requests are sent, each with a number for their position in the sequence. The server.php script simulates a server under load by sleeping random amounts of time before returning a response. When it arrives, the response is appended to the contents of the #response paragraph:

        $('#queued-requests').click(function() {
            $('#response').empty();
            $.each([1,2,3,4,5,6,7,8,9,10], function() {
                $.ajaxQueue({url: 'server.php',
                    data: { data: this },
                    success: function(data) { $('#response').append(data); }
                });
            });
            $.dequeue( $.ajaxQueue, "ajax" );
            return false;
        });

The “Queued requests” button adds each request to a queue by calling the ajaxQueue() function. Internally, the ajax() function is called with the provided options, each time a request is dequeued. After each of the requests has added to the queue, a call to dequeue() with the ajaxQueue function as a parameter triggers the first request. Each subsequent request will be sent in turn:

        $('#synced-requests').click(function() {
            $('#response').empty();
            $.each([1,2,3,4,5,6,7,8,9,10], function() {
                $.ajaxSync({url: 'server.php',
                    data: { data: this },
                    success: function(data) { $('#response').append(data); }
                });
            });
            return false;
        });
    });
})(jQuery);

The final set of requests use the ajaxSync() function to send the requests in parallel but synchronize the execution of the provided callbacks when the responses return.

Discussion

Responses from the unqueued requests come back out of order. This behavior is not necessarily undesirable and in many cases may be preferred. However, there are scenarios where one would like more control over Ajax requests and their responses. The functionality provided in ajaxQueue() suits the case where each subsequent request is dependent upon the response to the previous request, whereas ajaxSync() supports the use case of manipulating data, which is gathered from a variety of servers. In this scenario, processing is unable to commence until all servers have returned a response and the complete set of data is present.

17.6. Dealing with Ajax and the Back Button

Problem

Populating web pages using Ajax creates a convenient, interactive user experience, which can’t be replicated with traditional HTTP requests. Unfortunately, each time you update the contents of the browser window with Ajax, that content becomes inaccessible to the back and forward buttons of your browser. The bookmarking functionality found in most browsers is also rendered nonfunctional.

Solution

The solution to this problem is to relate each Ajax request to a unique URL. This URL can then be bookmarked and accessed by the back and forward browser buttons. One method for doing this is to use hash values. Hash values are generally used to link into a specific position within a document. http://en.wikipedia.org/wiki/Apple#History links to the history section of the Wikipedia page for Apple. For the purposes of this recipe, the hash value will refer to content loaded by an Ajax request.

In this example, the sample project is a small glossary. It has three entries. When you click each entry, the definition for the word is retrieved and displayed via Ajax. Granted, the content could easily be displayed all at once on a single page. However, this same approach is appropriate for larger, more varied data, such as the contents of each tab in a tabbed interface:

<!DOCTYPE html>
<html><head>
    <title>17.6 Dealing with Ajax and the Back Button</title>
    <script src="../../jquery-1.3.2.min.js"></script>
    <script src="jquery.history.js"></script>
</head>

<body>
    <h1>17.6 Ajax and the Back Button</h1>
    <a href="#apples" class='word'>apples</a>
    <a href="#oranges" class='word'>oranges</a>
    <a href="#bananas" class='word'>bananas</a>
    <p id='definition'></p>
</body></html>

Necessary JavaScript files are included in the head of the document. The jquery.history.js file contains the jQuery history plugin (available at http://plugins.jquery.com/project/history). There is an anchor element for each of the three entries in the glossary. The definition for each entry will be displayed in the paragraph with an id of definition:

(function($) {
    function historyLoad(hash) {
        if(hash) { $('#definition').load('server.php',{word: hash}); }
        else { $('#definition').empty(); }
    }

    $(document).ready(function() {
        $.history.init(historyLoad);
        $('a.word').click(function() {
            $.history.load($(this).html());
            return false;
        });
    });
})(jQuery);

The history plugin has two functions that are of concern: init() and load(). The init() function is called inside the ready function. A callback to handle Ajax requests is passed in as an argument. load() is bound to the word links. The content of each anchor tag is passed in as an argument. The callback historyLoad() takes care of requesting the content for the passed-in hash value. It also needs to be able to handle instances where there is no hash value.

Discussion

There are two instances when the historyLoad() callback is called. First, it is called inside the $.history.init() function, when the page is loaded. The hash value is stripped from the end of the URL and passed as the argument. If there is not a hash value present, the argument is empty. The load() function also calls historyLoad(). The argument we pass to $.history.load(), the word we clicked, in this case, is passed on as the hash argument to our callback.

In this solution, a jQuery plugin was used. It is relatively easy to implement similar functionality without a plugin, by using JavaScript’s window.location.hash object. The jQuery history plugin comprises only 156 lines of code. The reason it was chosen over writing a solution from scratch is that a large part of the plugin code handles cross-browser inconsistencies. When handling browser differences, it’s often more effective to draw from the communal pool of experience that accumulates in a plugin than try and to account for every implementation discrepancy oneself.

17.7. Putting JavaScript at the End of a Page

Problem

As a project grows in size, often the amount of JavaScript it contains grows as well. This results in slower page load times. Combining several disparate JavaScript files into one monolithic file, using minification, and using compression can help reduce the JavaScript size and reduce the number of HTTP requests made. But, one will always be left with some amount of code to load. It would be nice if the impact of this code on perceived load times could be reduced.

Solution

A user perceives load times based on what they see on the screen. A browser has a limited number of HTTP connections at its disposal to load external content, such as JavaScript, CSS stylesheets, and images. When JavaScript is placed at the top of the document, it can delay the loading of other visible resources. The solution is to place your JavaScript files at the end of your page:

<!DOCTYPE html>
<html><head>
    <title>17.7 Putting JavaScript at the End of a Page</title>
</head>

<body>
    <h1>17.7 Putting JavaScript at the End of a Page</h1>
    <p>Lorem ipsum dolor...</p>
    <script src="../../jquery-1.3.2.min.js"></script>
    <script type="text/javascript">
        jQuery(document).ready(function() {
            jQuery('p').after('<p>Ut ac dui ipsum...</p>').show();
        });
    </script>
</body></html>

Discussion

By placing the JavaScript just before the closing <body> tags, any images or CSS stylesheets that are referenced previously in the document are loaded first. This won’t cause the page to load any faster. However, it will decrease the perceived load time. Visible elements will be given priority over the JavaScript code. Loading the JavaScript files late in the page doesn’t incur any drawbacks because it generally shouldn’t be executed until the entire page is loaded.

No benefit is gained from putting the inline JavaScript at the end of the document. It is placed there in this example because the jQuery function can’t be called until jquery-1.3.2.min.js is loaded. If we placed the inline JavaScript in the <head> element, an error would be generated because of jQuery not being defined.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.200.14