Learning jQuery

0
1225
9 min read

 

(For more resources on jQuery, see here.)

Custom events

Learn Programming & Development with a Packt Subscription

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.

 

NO COMMENTS

LEAVE A REPLY

Please enter your comment!
Please enter your name here