Monthly Archives: August 2009

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()).

Server-side JavaScript and working with ActiveJs

In my effort to make it easy to contribute to ToDoSo, I have chosen JavaScript as the server side technology for this project.

Although Ruby on Rails is the technology of choice for most Web applications that appeared lately on the Internet [1. twitter - most job offers require to be familiar with this framework -, github, uservoice, zendesk, lighthouseapp - all of them using the Ruby on Rails cloud engineyard - and getsatisfaction - created by rubyredlabs - to name a few], it requires to learn not only HTML, CSS and JavaScript for the client side development but an additional technology for the server side development (a new language and a complete Framework in the case of Ruby on Rails).

Some would argue that Ruby is a really simple and rather self explaining language, that Ruby on Rails is a mature framework and altogether they make Web development a breeze once you know how to use it. I agree, but I also know that JavaScript is a great language as well and that it would lower the technological barrier if it was possible to use it on the server side with a framework comparable to Ruby on Rails.

There are already a large number of server-side JavaScript interpreter listed on wikipedia and on the ServerJs project page of Mozilla. There is also Jaxer, developed recently by Aptana. I recommend to watch the introductory videos on aptana.tv to have a brief overview of what is possible with it, but in brief its main features are:

  • the possibility to access and modify the DOM on the server before it is sent to the client (you can even use the canvas API to dynamically create images)
  • the possibility to share code between the client and the server sides (you only have to write your form validation logic once)
  • the possibility to call server side function from the client side (using invisible ajax requests)

Most of those server-side interpreters are based on open-source JavaScript interpreters that are to be found in modern web browsers [2. SpiderMonkey and V8 mainly, I haven't heard about any based on SquirrelFish or TraceMonkey so far but it seems that Jaxer will switch to the latter] which means that they benefit from the same speed improvements that we saw recently in Web browsers.

There are also few JavaScript MVC frameworks similar (at least in the spirit) to Ruby on Rails built on top of this server technologies: the most advanced ones seem to be Helma NG and ActiveJS. Both projects however, still seem to be in an early stage of development. Once again it could take ages to make a deep comparison of those and it wouldn’t help to get ToDoSo real. Instead you’d rather choose one and stick with it for better or for worse.

I’ve chosen ActiveJS because it is built originally for Jaxer and thus doesn’t require any configuration to get started if you are developing with Aptana Studio. Moreover, the ORM layer of ActiveJS (ActiveRecordJS) offers an abstraction not only to MySQL and SQLite databases (on the server side) but also to client-side persistence such as Google Gears or the SQLite database that is to be found in Safari and the iPhone Web browsers. Its route component is similar to the Ruby on Rail’s one but also offers deep linking on the client side, in the Jaxer spirit.

Hacking into ActiveJS

Because ActiveJS is still young, it is really likely to miss some features that might be important for your project and the best solution will be to hack into it instead of waiting for someone else to do the job. So here is how to configure a development environment to be able to build ActiveJS from sources (as a reminder for myself):

  1. I assume you are developing with Aptana Studio and you have the Jaxer and Git plugins installed.
  2. Fork the ActiveJS repository on Github.
  3. Import it as a project in Aptana (File > Import > Git Repository, choose git+ssh as the protocol and don’t change the username)
  4. Ruby needs to be installed in its 1.8 version because the build script depends on ftools, which is deprecated in ruby1.9. With Ubuntu and probably other linux distribution, the ruby-dev package is required as well.
  5. The package rubygem is then required to install most dependencies of the build script: json, packr (beware, this might soon be replaced by YUI) and rdiscount. In any case look at the beginning of build.rb to find the dependencies of the script.
    [code lang="shell"]$ sudo gem install rdiscount [/code]
  6. Remember to configure your editor to use 4 spaces instead of tabs and try to respect the coding style.

Et voilà! You’re ready to change the files in the src folder, build active.js, test your modifications and send pull requests to the original branch.

[code lang="shell"]
$ ruby1.8 ./build.rb compress
[/code]

My first tasks will be to improve the relationship features because it doesn’t behave as a proper ORM layer currently and to add support of HTML5 elements to the view component.