Event delegation with jQuery1.3

The ability to bind event listeners to DOM elements using JavaScript is a feature that is the base of elaborated interactions in Web pages. Using this mechanism, it is for example possible to submit a form, load another page or updating the current one when a user clicks on a specific button.

Simple events

The code required to bind a single listeners to an element is simple and works across browsers:

[code lang="js"]
document.getElementById("myButton").onclick = function() {
alert("My button has been clicked");
};
[/code]

When a second listeners is bound to the same event on this element, it will however override the first one.

[code lang="js"]
document.getElementById("myButton").onclick = function() {
alert("Only this alert will now be displayed");
};
[/code]

Clean event listeners

Current Web browsers offer a clean way to add event listeners to an element without overriding the previous ones. Using a JavaScript library such as jQuery is especially useful in this case since their are two different ways to do so, one specific to Internet Explorer and one defined by the w3c otherwise and adopted by every other browser vendor[1. attachEvent in Internet Explorer and addEventListener]. jQuery offers an unified way to deal with those two different implementations:

[code lang="js"]
jQuery("#myButton").click( function(event) {
alert("My button has been clicked");
});
// And later...
jQuery("#myButton").click( function(event) {
alert("And this alert will also be displayed");
});
[/code]

Event bubbling

Another useful characteristic of events in Web pages is that most of them bubble in the DOM: once an event occurs on an element, it will also occur on its direct ancestor, then its ancestor’s ancestor until it hits the document root or an event listener prevents it to bubble. For example, with the following unordered list and script:

[code lang="html"]
<ul id="myUnorderedList">
<li class="blue">A first <i>list item</i>.</li>
<li class="red">A second <i>list item</i>.</li>
</ul>
[/code]

[code lang="js"]
jQuery("#myUnorderedList").click( function(event) {
alert("The list has been clicked");
});
[/code]

No matter where the user clicks in the list, being on a list-item, an element nested in a list-item or in the gap between two list-items, the click event will still be detected by the listener set at the list level (on the <ul> tag). The event parameter that is passed to the function bound to the item carries information about the exact place where the click event occurred in its target property:

[code lang="js"]
jQuery("#myUnorderedList").click( function(event) {
alert("You have exactly clicked a " + event.target.nodeName + " element.");
// event.target.nodeName will be I, LI or UL depending where the cursor was.
});
[/code]

This is especially useful to avoid adding an event listener for each item of the list.

Event delegation

Using the target attribute, it is also possible to know in which particular <li> did the click occurred, even if the click occurred on an element nested in the <li>:

[code lang="js"]
jQuery("#myUnorderedList").click( function(event) {
var target = event.target;
// If the element clicked was nested in the li,
// it is necessary to loop through its ancestors
// until a li is found.
// The second test ends the loop when no li is found.
while(target.nodeName != "LI" && target.ownerDocument)
target = target.parentNode;
// If a li has actually been found...
if(target.nodeName == "LI")
alert("The colour of this li is " + target.className);
// target.className will be either "red" or "blue"
});
[/code]

Another advantage of event delegation, beside requiring to add an event listener only once, is that its logic will still work for any item appended later to the list. In dynamic web pages where additional content can be loaded arbitrary (by an ajax request for example), this proves to be an invaluable mechanism.

jQuery1.3 helpers

The 1.3 version of jQuery introduce two helpers which make event delegation a breeze:

the .closest( selector ) function

This function is an equivalent to the loop that has been written in the previous code snippet to look for a specific ancestor of an event target. The equivalent of the above code with the .closest() is as short as:

[code lang="js"]
jQuery("#myUnorderedList").click( function(event) {
var $target = $(event.target).closest("li");
// if a li has actually been found
if($target.length)
alert("The colour of this li is " + $target.attr("class"));
});
[/code]

The function can take any type of CSS selector as argument, in a pure jQuery fashion:

[code lang="js"]
jQuery("#myUnorderedList").click( function(event) {
var $target = $(event.target).closest("li.blue");
if($target.length)
alert("The colour of this li is blue");
});
[/code]

The upcoming jQuery1.3.3 adds a second optional argument to closest, the context. Previously, when an element clicked was not nested in an element matching the selector, every ancestor of the target where still tested up to the root of the page. It is now possible to end the search for a target ancestor when a given element is hit. For example, in the previous snippet of code, there is no need to search for a list-item in the unordered list’s ancestors. In most case, this will be as simple as using the context of the event listener itself:

[code lang="js"]
jQuery("#myUnorderedList").click( function(event) {
var $target = $(event.target).closest("li.blue", this);
if($target.length)
alert("The colour of this li is blue");
});
[/code]

the .live() function

This function has to be used just after a DOM query to filter events occurring on the matched set of elements, even if other elements matching the selector are added later to the document.

[code lang="js"]
jQuery("li.blue").live().click( function(event) {
alert("The colour of this li is blue");
});
[/code]

Although this might seem like a magic and somewhat obscure way of achieving event delegation, the underlying principle is actually exactly the same as the one used in the previous code snippet. Once again, it is possible to improve the performance of this function by providing a context to the selector. In this case the context of the jQuery object is used:

[code lang="js"]
jQuery("#myUnorderedList").find("li.blue")
.live("click", function(event){ ... } );
// or
jQuery("li.blue", jQuery("#myUnorderedList"))
.live("click" function(event){ ... } );
[/code]

It is important to note that both .closest() and .live() will only be useful for events which actually bubble in the DOM. Events such as focus, blur, mouseenter and mouseleave thus cannot be used with .live().

Dealing with mouseover/mouseout instead of mouseenter/mouseleave

mouseenter and mouseleave are events originally specific to Internet Explorer (but available in all browsers using jQuery) which do not bubble. This specificity is very useful to prevent listeners from being triggered by events happening inside nested elements. With the following list:

[code lang="html"]
<ul id="myUnorderedList">
<li>A first <i>item</i>.<em class="tips">Hidden info</em></li>
<li>A second <i>item</i>.<em class="tips">Hidden info</em></li>
</ul>
[/code]

Trying to display the hidden information only when the cursor is over a list-item could be written as follows:

[code lang="js"]
$("li").mouseover( function() {
$(this).find(".hidden").show();
}).mouseout( function() {
$(this).find(".hidden").hide();
});
[/code]

With such code, the hidden info would flickr every time the cursor is moved from or to the text wrapped in a <i>, because a mouseout event occurs even when the cursor is moved to a children of the previous element. Using mouseenter and mouseleave events effectively solves this problem. However this advantage makes event delegation impossible at the same time. The issue can actually be solved by combining the use of .live() on the mouseover and mouseout events with .closest() to filter out events for which the origin of the cursor is another element in the same <li>, using the relatedTarget attribute of the event:

[code lang="js"]
$("li").live("mouseover", function(event) {
var $related = $(event.relatedTarget).closest("li");
if(!$related.length || $related[0] != this)
$(this).find(".hidden").show();
});
$("li").live("mouseout", function(event) {
var $related = $(event.relatedTarget).closest("li");
if(!$related.length || $related[0] != this)
$(this).find(".hidden").hide();
});
[/code]

Using event delegation with mouseover and mouseout however has a non-negligible impact on performance, since a significant amount of code is run every time the cursor enters a nested element, just to check if the bound function should be executed. This issue and a possible improvement of jQuery’s code are detailed in a thread in jQuery’s Google group.

Conclusion

The helpers added in jQuery 1.3 make it really easy to implement event delegation and understanding how they work allows for more advanced problems to be easily tackled. As a general rule, it can be considered that an optimisation is always possible using event delegation wherever the same event listener is used for two different elements. An event listener should be bound only once, using event delegation if necessary. Using it with mouseover and mouseout should however be done carefully.

It is also recommended to read the documentation page of .live() to understand the limitations of this function. One might also want to know how to .unbind() events (.die() being the equivalent when using .live()).

2 thoughts on “Event delegation with jQuery1.3

  1. Thomas

    “since their are two different ways to do so” hahaha.

    Anyway, very interesting article. I have a question: you start some variables with an “$” like $target. Is there a difference with “target”? Some magic properties? Or is it just a coding convention?

  2. Louis-Rémi

    This is a coding convention indeed, to easily spot the vars which are jQuery object.
    In code using JavaScript libraries you’ll often find lines with var $this = $(this); or var $myDiv = $(“#myDiv”);
    This can be a bit confusing since in PHP and other programming languages, the $ is a special character with a particular purpose.
    In Javascript it is like any other letter of the alaphabet: var zo$y = “the fly”;

    I’m seeing “to do so” everywhere now, it’s not such an odd construction actually.

Comments are closed.