Learning jQuery

Exclusive offer: get 80% off this eBook here
Learning jQuery, Third Edition

Learning jQuery, Third Edition — Save 80%

Create better interaction, design, and web development with simple JavaScript techniques with this book and ebook

₨739.00    ₨147.80
by Jonathan Chaffer Karl Swedberg | September 2011 | Open Source Web Development

jQuery provides a wide range of features, an easy-to-learn syntax, and robust cross-platform compatibility in a single compact file. This article by Jonathan Chaffer and Karl Swedberg, authors of Learning jQuery, Third Edition, covers the following topics:

  • Custom events
  • Throttling events
  • Deferred objects
  • The jqXHR object

 

(For more resources on jQuery, see here.)

Custom events

The events that are triggered naturally by the DOM implementations of browsers are crucial to any interactive web application. However, we are not limited to this set of events in our jQuery code. We can freely add our own custom events to the repertoire.

Custom events must be triggered manually by our code. In a sense, they are like regular functions that we define, in that we can cause a block of code to be executed when we invoke it from another place in the script. The .bind() call corresponds to a function definition and the .trigger() call to a function invocation.

However, event handlers are decoupled from the code that triggers them. This means that we can trigger events at any time, without knowing in advance what will happen when we do. We might cause a single bound event handler to execute, as with a regular function. We also might cause multiple handlers to run or even none at all.

In order to illustrate this, we can revise our Ajax loading feature to use a custom event. We will trigger a nextPage event whenever the user requests more photos and bind handlers that watch for this event and perform the work previously done by the .click() handler as follows:

$(document).ready(function() { $('#more-photos').click(function() { $(this).trigger('nextPage'); return false; }); });

The .click() handler now does very little work itself. After triggering the custom event, it prevents the default behavior by returning false. The heavy lifting is transferred to the new event handlers for the nextPage event as follows:

(function($) { $(document).bind('nextPage', function() { var url = $('#more-photos').attr('href'); if (url) { $.get(url, function(data) { $('#gallery').append(data); }); } }); var pageNum = 1; $(document).bind('nextPage', function() { pageNum++; if (pageNum < 20) { $('#more-photos') .attr('href', 'pages/' + pageNum + '.html'); } else { $('#more-photos').remove(); } }); })(jQuery);

The largest difference is that we have split what was once a single function into two. This is simply to illustrate that a single event trigger can cause multiple bound handlers to fire.

The other point to note is that we are illustrating another application of event bubbling here. Our nextPage handlers could be bound to the link that triggers the event, but we would need to wait to do this until the DOM was ready. Instead, we are binding the handlers to the document itself, which is available immediately, so we can do the binding outside of $(document).ready(). The event bubbles up and, so long as another handler does not stop the event propagation, our handlers will be fired.

Infinite scrolling

Just as multiple event handlers can react to the same triggered event, the same event can be triggered in multiple ways. We can demonstrate this by adding an infinite scrolling feature to our page. This popular technique lets the user's scroll bar manage the loading of content, fetching additional content whenever the user reaches the end of what has been loaded thus far.

We will begin with a simple implementation, and then improve it in successive examples. The basic idea is to observe the scroll event, measure the current scroll bar position when scrolling occurs, and load the new content if needed, as follows:

(function($) { var $window = $(window); function checkScrollPosition() { var distance = $window.scrollTop() + $window.height(); if ($('#container').height() <= distance) { $(document).trigger('nextPage'); } } $(document).ready(function() { $window.scroll(checkScrollPosition).scroll(); }); })(jQuery);

The new checkScrollPosition() function is set as a handler for the window's scroll event. This function computes the distance from the top of the document to the bottom of the window, and then compares this distance to the total height of the main container in the document. As soon as these reach equality, we need to fill the page with additional photos, so we trigger the nextPage event.

As soon as we bind the scroll handler, we immediately trigger it with a call to .scroll(). This kick-starts the process, so that if the page is not initially filled with photos, an Ajax request is made right away.

Custom event parameters

When we define functions, we can set up any number of parameters to be filled with argument values when we actually call the function. Similarly, when triggering a custom event, we may want to pass along additional information to any registered event handlers. We can accomplish this by using custom event parameters.

The first parameter defined for any event handler, as we have seen, is the DOM event object as enhanced and extended by jQuery. Any additional parameters we define are available for our discretionary use.

To see this action, we will add a new option to the nextPage event allowing us to scroll the page down to display the newly added content as follows:

(function($) { $(document).bind('nextPage', function(event, scrollToVisible) { var url = $('#more-photos').attr('href'); if (url) { $.get(url, function(data) { var $data = $(data).appendTo('#gallery'); if (scrollToVisible) { var newTop = $data.offset().top; $(window).scrollTop(newTop); } checkScrollPosition(); }); } } ); });

We have now added a scrollToVisible parameter to the event callback. The value of this parameter determines whether we perform the new functionality, which entails measuring the position of the new content and scrolling to it. Measurement is easy using the .offset() method, which returns the top and left coordinates of the new content. In order to move down the page, we call the .scrollTop() method.

Now we need to pass an argument into the new parameter. All that is required is providing an extra value when invoking the event using .trigger(). When newPage is triggered through scrolling, we don't want the new behavior to occur, as the user is already manipulating the scroll position directly. When the More Photos link is clicked, on the other hand, we want the newly added photos to be displayed on the screen, so we will pass a value of true to the handler as follows:

$(document).ready(function() { $('#more-photos').click(function() { $(this).trigger('nextPage', [true]); return false; }); $window.scroll(checkScrollPosition).scroll(); });

In the call to .trigger(), we are now providing an array of values to pass to event handlers. In this case, the value of true will be given to the scrollToVisible parameter of the event handler.

Note that custom event parameters are optional on both sides of the transaction. We have two calls to .trigger() in our code, only one of which provides argument values; when the other is called, this does not result in an error, but rather the value of null is passed to each parameter. Similarly, the lack of a scrollToVisible parameter in one of our .bind('nextPage') calls is not an error; if a parameter does not exist when an argument is passed, that argument is simply ignored.

Throttling events

A major issue with the infinite scrolling feature as we have implemented it is its performance impact. While our code is brief, the checkScrollPosition() function does need to do some work to measure the dimensions of the page and window. This effort can accumulate rapidly, because in some browsers the scroll event is triggered repeatedly during the scrolling of the window. The result of this combination could be choppy or sluggish performance.

Several native events have the potential for frequent triggering. Common culprits include scroll, resize, and mousemove. To account for this, we need to limit our expensive calculations, so that they only occur after some of the event instances, rather than each one. This technique is known as event throttling.

$(document).ready(function() { var timer = 0; $window.scroll(function() { if (!timer) { timer = setTimeout(function() { checkScrollPosition(); timer = 0; }, 250); } }).scroll(); });

Rather than setting checkScrollPosition() directly as the scroll event handler, we are using the JavaScript setTimeout function to defer the call by 250 milliseconds. More importantly, we are checking for a currently running timer first before performing any work. As checking the value of a simple variable is extremely fast, most of the calls to our event handler will return almost immediately. The checkScrollPosition() call will only happen when a timer completes, which will at most be every 250 milliseconds.

We can easily adjust the setTimeout() value to a comfortable number that strikes a reasonable compromise between instant feedback and low performance impact. Our script is now a good web citizen.

Other ways to perform throttling

The throttling technique we have implemented is efficient and simple, but it is not the only solution. Depending on the performance characteristics of the action being throttled and typical interaction with the page, we may for instance want to institute a single timer for the page rather than create one when an event begins:

$(document).ready(function() { var scrolled = false; $window.scroll(function() { scrolled = true; }); setInterval(function() { if (scrolled) { checkScrollPosition(); scrolled = false; } }, 250); checkScrollPosition(); });

Unlike our previous throttling code, this polling solution uses a single setInterval() call to begin checking the state of the scrolled variable every 250 milliseconds. Any time a scroll event occurs, scrolled is set to true, ensuring that the next time the interval passes, checkScrollPosition() will be called.

A third solution for limiting the amount of processing performed during frequently repeated events is debouncing. This technique, named after the post-processing required handling repeated signals sent by electrical switches, ensures that only a single, final event is acted upon even when many have occurred.

Deferred objects

In jQuery 1.5, a concept known as a deferred object was introduced to the library. A deferred object encapsulates an operation that takes some time to complete. These objects allow us to easily handle situations in which we want to act when a process completes, but we don't necessarily know how long the process will take or even if it will be successful.

A new deferred object can be created at any time by calling the $.Deferred() constructor. Once we have such an object, we can perform long-lasting operations and then call the .resolve() or .reject() methods on the object to indicate the operation was successful or unsuccessful. It is somewhat unusual to do this manually, however. Typically, rather than creating our own deferred objects by hand, jQuery or its plugins will create the object and take care of resolving or rejecting it. We just need to learn how to use the object that is created.

Creating deferred objects is a very advanced topic. Rather than detailing how the $.Deferred() constructor operates, we will focus here on how jQuery effects take advantage of deferred objects.

 

Learning jQuery, Third Edition Create better interaction, design, and web development with simple JavaScript techniques with this book and ebook
Published: September 2011
eBook Price: ₨739.00
Book Price: ₨1,232.00
See more
Select your format and quantity:

 

(For more resources on jQuery, see here.)

Every deferred object makes a promise to provide data to other code. This promise is represented as another object, with its own set of methods. From any deferred object, we can obtain its promise object by calling its .promise() method. Then, we can call methods of the promise to attach handlers that are executed when the promise is fulfilled:

  • The .done() method attaches a handler that is called when the deferred object is resolved successfully.
  • The .fail() method attaches a handler that is called when the deferred object is rejected.
  • The .always() method attaches a handler that is called when the deferred object completes its task, either by being resolved or by being rejected.

These handlers are much like the callbacks we provide to .bind(), in that they are functions called when some event happens. We can also attach multiple handlers to the same promise, and all will be called at the appropriate time. There are a few important differences, however. Promise handlers will only ever be called once; the deferred object cannot resolve a second time. A promise handler will also be called immediately if the deferred object is already resolved at the time we attach the handler.

Now we can put this powerful tool to use by investigating one of the deferred objects created by jQuery for us.

Animation promises

Every jQuery collection has a set of deferred objects associated with it, tracking the status of queued operations on the elements in the collection. By calling the .promise() method on the jQuery object, we get a promise object that is resolved when a queue completes. In particular, we can use this promise to take an action upon the completion of all of the animations running on any of the matched elements.

Just as we have a showDetails() function to display a member name and location information, we can write a showBio() function for bringing the biographical information into view. First, we will append a new <div> to the <body> and set up a couple of map options as follows:

var $movable = $('<div id="movable"></div>')
.appendTo('body');
var bioBaseStyles = {
display: 'none',
height: '5px',
width: '25px'
},
bioEffects = {
duration: 800,
easing: 'easeOutQuart',
specialEasing: {
opacity: 'linear'
}
};

This new "movable" <div> is the one that we will actually animate after injecting it with a copy of a biography. Having a wrapper element such as this is particularly useful when animating an element's width and height. We can set its overflow property to hidden and set an explicit width and height for the biographies within it to avoid a continual reflowing of text that would have occurred if we had instead animated the biography <div>s themselves.

We will use the showBio() function to determine what the movable <div>'s starting and ending styles should be based on the member that is clicked. Note that we are using the $.extend() method to merge the set of base styles that remain constant with the top and left properties that vary depending on the member's position. Then, it is just a matter of using .css() to set the starting styles and .animate() for the ending styles as follows:

function showBio() {
var $member = $(this).parent(),
$bio = $member.find('p.bio'),
startStyles = $.extend(bioBaseStyles, $member.offset()),
endStyles = {
width: $bio.width(),
top: $member.offset().top + 5,
left: $member.width() + $member.offset().left - 5,
opacity: 'show'
};
$movable
.html($bio.clone())
.css(startStyles)
.animate(endStyles, bioEffects)
.animate({height: $bio.height()}, {
easing: 'easeOutQuart'
});
}

We are queuing two .animate() methods, so that the biography first flies from the left as it grows wider and fully opaque and then slides down to its full height once it is in position.

We want to show the member's biography after the other <div> elements appear. Before jQuery introduced the .promise() method, this would have been an onerous task, requiring us to count down from the total number of elements for each time the callback was executed until the last time, at which point we could execute the code to animate the biography. Now we can simply chain the .promise() and .done() methods to the .each() method inside our showDetails() function as follows:

function showDetails() {
var $member = $(this).parent();
if ($member.hasClass('active')) {
return;
}
$movable.fadeOut();
$('div.member.active')
.removeClass('active')
.children('div').fadeOut();
$member.addClass('active');
$member.find('div').css({
display: 'block',
left: '-300px',
top: 0
}).each(function(index) {
$(this).animate({
left: 0,
top: 25 * index
}, {
duration: 'slow',
specialEasing: {
top: 'easeInQuart'
}
});
}).promise().done(showBio);
}

The .done() method takes a reference to our showBio() function as its argument. Now clicking on an image brings all of that member's information into view with an attractive animation sequence as shown in the following screenshots:

 

Notice that we also slipped in $movable.fadeOut() near the top of the function. This has no visible effect the first time showDetails() is called, but in subsequent calls, it nicely fades the currently visible biography away along with the other information before the new information is animated into view.

The jqXHR object

When an Ajax request is made, jQuery determines the best mechanism available for retrieving the data. This transport could be the standard XMLHttpRequest object, the Microsoft ActiveX XMLHTTP object, or a <script> tag.

As the transport used can vary from request to request, we need a common interface in order to interact with the communication. The jqXHR object provides this interface for us: it is a wrapper for the XMLHttpRequest object when that transport is used, and in other cases it simulates XMLHttpRequest as best it can. Among the properties and methods it exposes are:

  • .responseText or .responseXML, containing the returned data
  • .status and .statusText, containing a status code and description
  • .setRequestHeader(), to manipulate the HTTP headers sent with the request
  • .abort(), to prematurely halt the transaction

This jqXHR object is returned from all of jQuery's Ajax methods, so we can store the result if we need access to any of these properties or methods.

Ajax promises

Perhaps a more important aspect of jqXHR than the XMLHttpRequest interface, however, is that it also acts as a promise. Ajax calls are just such an operation, and the jqXHR object provides the methods we expect from a deferred object's promise.

Using the promise's methods, we can rewrite our $.ajax() call to replace the success and error callbacks with an alternate syntax, as follows:

$.ajax({
url: 'http://api.jquery.com/jsonp/',
dataType: 'jsonp',
data: {
title: $('#title').val()
},
timeout: 15000
})
.done(response)
.fail(function() {
$response.html(failed);
});

At first glance, calling .done() and .fail() doesn't seem any more useful than the callback syntax we used previously. However, the promise methods offer several advantages. First, the methods can be called multiple times to add more than one handler if desired. Second, if we store the result of the $.ajax() call in a variable, then we can attach the handlers later if that makes our code structure more readable. Third, the handlers will be invoked immediately if the Ajax operation is already complete when they are attached. Finally, we should not discount the readability advantage of using a syntax that is consistent with other parts of the jQuery library.

As another example of using the promise methods, we can add a loading indicator when a request is made. Since we want to hide the indicator when the request completes successfully or otherwise, the .always() method will come in handy:

$ajaxForm.bind('submit', function(event) {
event.preventDefault();
$response.addClass('loading').empty();
$.ajax({
url: 'http://api.jquery.com/jsonp/',
dataType: 'jsonp',
data: {
title: $('#title').val()
},
timeout: 15000
})
.done(response)
.fail(function() {
$response.html(failed);
})
.always(function() {
$response.removeClass('loading');
});
});

Before we issue the $.ajax() call, we add the loading class to the response container. Once the load is complete, we remove it again. In this way, we have further enhanced the user experience.

To really get a grasp of how the promise behavior can help us, though, we need to look at what we can do if we store the result of our $.ajax() call in a variable for later use.

Caching responses

If we need to use the same piece of data repeatedly, then it is wasteful to make an Ajax request each time. To prevent this, we can cache the returned data in a variable. When we need to use some data, we can check to see if the data is already in the cache. If so, then we act on this data. If not, then we need to make an Ajax request, and in its .done() handler we store the data in the cache and act on the returned data.

This is a lot of steps. If we exploit the properties of promises, though, then it can be quite simple, as shown in the folllowing code snippet:

var api = {};
$ajaxForm.bind('submit', function(event) {
event.preventDefault();
$response.empty();
var search = $('#title').val();
if (search == '') {
return;
}
$response.addClass('loading');
Advanced Ajax
[ 12 ]
if (!api[search]) {
api[search] = $.ajax({
url: 'http://api.jquery.com/jsonp/',
dataType: 'jsonp',
data: {
title: search
},
timeout: 15000
});
}
api[search].done(response).fail(function() {
$response.html(failed);
}).always(function() {
$response.removeClass('loading');
});
});

We've introduced a new variable named api to hold the jqXHR objects we create. This variable is an object, with keys corresponding to the searches being performed. When the form is submitted, we look to see if there is already a jqXHR object stored for that key. If not, then we do the query as before, storing the resulting object inside api.

The .done(), .fail(), and .always() handlers are then attached to the jqXHR object. Note that this happens regardless of whether an Ajax request was made. There are two possible situations to consider here.

First, the Ajax request might be sent, if it hasn't before. This is just like the previous behavior: the request is issued, and we use the promise methods to attach handlers to the jqXHR object. When a response comes back from the server, the appropriate callbacks are fired, and the result is printed to the screen.

On the other hand, if we have performed this search in the past, the jqXHR object is already stored in api. In this case no new search is performed, but we still call the promise methods on the stored object. This attaches new handlers to the object, but as the deferred object has already been resolved, the relevant handlers are fired immediately.

The jQuery deferred object system handles all of the hard work for us. With a couple of lines of code, we have eliminated duplicated network requests from the application.

Summary

In this article, we have covered Custom events, Throttling events, Deferred objects, and the jqXHR object used in jQuery.



Learning jQuery, Third Edition Create better interaction, design, and web development with simple JavaScript techniques with this book and ebook
Published: September 2011
eBook Price: ₨739.00
Book Price: ₨1,232.00
See more
Select your format and quantity:

About the Author :


Jonathan Chaffer

Jonathan Chaffer is a member of Rapid Development Group, a web development firm located in Grand Rapids, Michigan. His work there includes overseeing and implementing projects in a wide variety of technologies, with an emphasis in PHP, MySQL, and JavaScript. He also leads on-site training seminars on the jQuery framework for web developers.

In the open source community, he has been very active in the Drupal CMS project, which has adopted jQuery as its JavaScript framework of choice. He is the creator of the Content Construction Kit, a popular module for managing structured content on Drupal sites. He is also responsible for major overhauls of Drupal's menu system and developer API reference.

He lives in Grand Rapids with his wife, Jennifer.

Karl Swedberg

Karl Swedberg is a web developer at Fusionary Media in Grand Rapids, Michigan, where he spends much of his time making cool things happen with JavaScript. As a member of the jQuery team, he is responsible for maintaining the jQuery API site at api.jquery.com. He is also a member of jQuery's Board of Advisors and a presenter at workshops and conferences. When he isn't coding, he likes to hang out with his family, roast coffee in his garage, and exercise at the local CrossFit gym.

Books From Packt


jQuery 1.4 Reference Guide
jQuery 1.4 Reference Guide

jQuery Mobile First Look
jQuery Mobile First Look

jQuery UI 1.8: The User Interface Library for jQuery
jQuery UI 1.8: The User Interface Library for jQuery

jQuery 1.4 Animation Techniques: Beginners Guide
jQuery 1.4 Animation Techniques: Beginners Guide

jQuery UI Themes Beginner's Guide
jQuery UI Themes Beginner's Guide

PHP jQuery Cookbook
PHP jQuery Cookbook

Django JavaScript Integration: AJAX and jQuery
Django JavaScript Integration: AJAX and jQuery

ASP.NET jQuery Cookbook
ASP.NET jQuery Cookbook


Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software