Tuesday, January 20, 2015

jQuery and event delegation

Here's a trick that I had forgotten, but that can make a difference: the delegation of event handlers in jQuery.

The Usual Way


So, let's say you have a listing and its content will be refreshed programatically. A common use case would be a search function that generates a list of results, such as listviews being populated in a mobile app (think jQuery Mobile here for the moment).

If the LI elements were already on the page, then you'd simply do this to assign an event handler to all LI items in the result listing:
$('#results li.result a.moreinfo').click(function () {
    var data = $(this).closest('li').data('rawinfo');
    showMoreInfo(data);
});
But as you know, when the app first starts there are no LIs nor any a.moreinfo elements, so in fact nothing is assigned thosre event handlers. Besides, performing a new search would blow away those LI elements and their event handlers, replacing them with brand new ones that don't have event handlers. So, you assign event handlers to the elements as they are loaded into the result listing:
var listing = $('#results').empty();
for (var i=0, l=results.length; i<l; i++) {
    var li = $('<li></li>').data('rawdata', results[i]).text(results[i]).appendTo(listing);
    li.click(function () {
        var data = $(this).closest('li').data('rawinfo');
        showMoreInfo(data);
    });
}
So, it's no huge bother to assign those event handlers in the loop. After all, that's where you're creating the elements so it's natural that you would define the handlers there. Having those two pieces of code together really does make sense.

But that easy way does involve some runtime overhead. If you have 500 search results, then during your loop you instantiated 500 anonymous functions, and performed 500 event-listener bindings. That consumed very little bit of time, perhaps 1-2 ms apiece... hey wait, that's a significant fraction of a second!

Do it At The End


An optional approach, is to assign your event handlers at the end of your search-and-make-listing function, like this:
// load the listing with event-less LI elements of your results...
var listing = $('#results').empty();
for (var i=0, l=results.length; i<l; i++) {
    var li = $('<li></li>').data('rawdata', results[i]).text(results[i].name).appendTo(listing);
}

// now at the end re-scan that section of the DOM and assign event handlers
$('#results li').click(function () {
    var data = $(this).closest('li').data('rawinfo');
    showMoreInfo(data);
});
This method also works well: one bulk assignment, and in a position where you know the elements are in the DOM. Not bad. Not quite as clean and organized as having defined event handlers back when the page first loaded, but it does keep the two chunks side-by-side (mostly) and does avoid a step of processing 500 times.

The Delegated Way


But you can also give this a try. Using the on function and specifying a selector element in between the event and the function, the parent itself will listen for clicks... on the specified elements.
// in the application initialization, the #results panel hosts the event handler
// the LIs don't have their own handlers, so destroying them when you empty() will not destroy the event handlers
// assign once and forget it!
$('#results').on('click', 'li', function () {
    var data = $(this).closest('li').data('rawinfo');
    showMoreInfo(data);
});

//some time later ...  our make-a-list-of-results function is slimmer, noww that we're not
// instantiating functions and setting event bindings
var listing = $('#results').empty();
for (var i=0, l=results.length; i<l; i++) {
    var li = $('<li></li>').data('rawdata', results[i]).text(results[i].name).appendTo(listing);
}
The key is that small syntax change: if you stick a jQuery selector in between the event type and the callback function, then it changes the meaning.
// all LI elements under #results get a click handler
$('#results li').on('click', function () {});

//  #results gets a click handler, with an additional check that the event was on a LI element
$('#results').on('click', 'li', function () {});

This technique combines the best of both: you define the event handler at the start of your code, and have only one event handler running, but when time comes to populate the loop that handler will automagically be effective on newly-created LIs as they enter the #results element.

Potential for Confusion


The down side here, is the up side of the original way:
  • Having the elements created in the loop, and their handlers created in the loop, has both bits of code right next to each other where they're obviously related and are visually together.
  • Having the event handlers set up on line 10 and the elements created in the loop on line 1450, means some guesswork for the dev who inherits your work. "I see the span.maplink but I don't see where it had the click behavior asssigned." This is especially true if you didn't use an explicit class name, e.g. something really generic like $('#results li')
So, as always, be generous with commenting your code. A simple note like "click handlers on the result panel are defined up in initResultListing()" can save a half-hour of searching and head-scratching.

Learn More


http://learn.jquery.com/events/event-delegation/

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.