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.
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.
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.
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.
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.
You want to persist data on the client only until the current session is ended, i.e., the window or tab is closed.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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>
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.
3.135.247.68