Working with Events, Part 3: More Event Delegation with jQuery

Event delegation, as described in the first article of this series, is a way to take advantage of event bubbling to avoid binding an event listener more than once. jQuery 1.3 and the upcoming jQuery 1.4 have many features that make using event delegation in your web pages easier. The aim of this tutorial is to help you understand how these new features work.

From traditional event listening to event delegation

Since an event occurring on an element is propagated to all of its ancestors, an event listener can be bound to a single ancestor of numerous elements instead of being bound to all the elements individually.

Consider the following list items:

[html]
  • The first item.
  • The second item.
  • The third item.
  • The fourth item.

Class of the last clicked item:

[/html]

If we want to display the class of an item when it is clicked, a traditional event listener in jQuery would be written like this:

[js] $("li").click( function( event ) { $("#display").text(event.target.className); }); [/js]

The event object passed to the handler has a target property which corresponds to the element that has been clicked.

The equivalent using event delegation would look like this:

[js] $("ul").click( function( event ) { $("#display").text(event.target.className); }); [/js]
  • The first item.
  • The second item.
  • The third item.
  • The fourth item.

Class of the last clicked item:

Event delegation has two main advantages:

  1. setting a single event listener instead of multiple ones is obviously faster;
  2. any new element later added to the list will have the same behavior (as demonstrated in Working with Events, Part 1).

This translation to event delegation that we've just made is, however, slightly too simple, since our original aim was to display only the class of the list items. Using the previous snippet, the class of the unordered list itself can be displayed (by clicking to the left of a bullet-point). We thus have to make sure that the target of the click is a <li>.

[js] $("ul").click( function( event ) { if(event.target.nodeName == "LI") { $("#display").text(event.target.className); } }); [/js]
  • The first item.
  • The second item.
  • The third item.
  • The fourth item.

Class of the last clicked item:

Scanning the ancestors of the event.target: the .closest() method

With such a simple document, event delegation is really that easy to achieve. But things can get more complex if the items have children elements:

[html]
  • The first item.
  • The second item.
  • The third item.
  • The fourth item.

Class of the last clicked item:

[/html]

In this case, if the user clicks on one of the words item, the event.target will be a <u>. It is therefore necessary to loop through all of the ancestors of this original target to find the element that is "interesting" for us: the <li>

[js] $("ul").click( function( event ) { var elem = event.target; while( elem.nodeName != "LI" && elem.parentNode) { elem = elem.parentNode; } if(elem.nodeName == "LI") { $("#display").text(event.target.className); } }); [/js]

Notice that if the user clicks outside of an <li>, we have to stop looping through the ancestors at some point; in this case when we find the root of the document (which has no parentNode).

jQuery 1.3 introduced the .closest() method, which replaces this loop by a single line of code:

[js] $("ul").click( function( event ) { var $elem = $(event.target).closest("li"); if($elem.length) { $("#display").text($elem.attr("class")); } }); [/js]
  • The first item.
  • The second item.
  • The third item.
  • The fourth item.

Class of the last clicked item:

The parameter passed to the .closest() method is a CSS selector that will match the first interesting ancestor.

The context parameter

It can be noted that, when the click occurs outside of an <li>, we could stop looping through the ancestors before hitting the document root, but instead as soon as we hit the <ul>. jQuery 1.4 will introduce an optional context parameter to .closest() for this purpose:

[js] $("ul").click( function( event ) { $("#display").text($(event.target).closest("li", this).attr("class")); }); [/js]

Here this is the <ul> element to which the event listener has been bound. This optional parameter improves the performance of event delegation as it prevents the wasted resources of searching for an ancestor which cannot exist.

Event delegation in a single line: the .live() method

jQuery 1.3 also introduces .live(), a method which binds the event listener and implicitly calls the .closest() method to determine whether your event handler should be executed or not.

[js] $("li").live("click", function( event ) { $("#display").text( $(event.currentTarget).attr("class") ); }); [/js]

Note how different the syntax using the .live() method is. It seems that we have switched back to a traditional event binding. However, a big difference is that the event listener will work for any existing and future element matching our original selector. There is, of course, nothing magic: behind the scenes .live() simply binds the event listener to the document root and filters any event.target using the .closest() method.

A notable difference with the traditional event binding syntax is that we are not using the target property of the event object inside our event handler, but rather the currentTarget. Indeed, the target of the event is possibly a child of one <li>, whereas the currentTarget property refers to the element that has been found by the implicit .closest().

As explained on the Event Object documentation, the currentTarget property is supposed to be the current DOM element within the event bubbling phase, i.e. the element on which the event has been detected by the event listener, which will always be the element on which the event listener was bound. When using .live(), the currentTarget would therefore always be the document root and you would need to use .closest() once again on the original target to find an interesting ancestor.

[js] $("li").live("click", function( event ) { $("#display").text( $(event.target).closest("li").attr("class") ); }); [/js]

This kind of code is required with jQuery 1.3, but as of jQuery 1.4, the currentTarget is modified internally and can thus be used to avoid the extra .closest() inside the event handler.

The context parameter

In jQuery 1.3 all live event listeners were effectively bound to the document root. This can hurt the performance of a web page since all events detected by an event listener will trigger the execution of an implicit .closest(), even though the event target might be totally out of interest.

In jQuery 1.4, .live() should be able to bind the event listener to a specific and more focused element of the document by taking advantage of the context of your jQuery object:

[js] $("li", $("ul")[0]).live("click", function( event ) { $("#display").text( $(event.currentTarget).attr("class") ); }); [/js]

The context is the second parameter used to build our jQuery object. To be useful for .live() it has to be set as a pure DOM element, hence the [0] after $("ul"). Brandon Aaron has a useful article explaining the context parameter in detail.

Unbind for .live(): the .die() method

Just like .bind() has its .unbind() counterpart that allows for event listeners to be unbound, .live() has its .die() counterpart.

Unlike .bind(), .live() does not allow for namespaced events to be used.

Dealing with events that don't bubble

There are some events for which event delegation is traditionally not possible, simply because they do not bubble. The promise of jQuery 1.4 is nevertheless to make those events compatible with .live(). How this is achieved will be covered in the upcoming fourth part of this tutorial.