Event Delegation versus Event Handling

Spoiler: This is not a new trick and has been in use in several big projects already. Talking to some people at dconstruct I found out however that some people know about it (ppk, to be exact), and others were still quite amazed by it. So I thought, why not explain it quickly.

Check out the following nested lists. When your browser supports JavaScript, you will be able to click the links and expand and collapse nested list items.

The difference between the two of them is that the "Handlers" example is rather slow when the list is very large while the "Delegation" example can be extended as much as you want to without adding more overhead. The latter example is also a lot easier to change. Try the following button to dynamically add another list item with a nested list to each of them (this is done by changing the innerHTML property and appending a string - much like you'd do it with an Ajax call were you to use AHAH).

You'll see that while you can expand and collapse the new nested list in the "Delegation" example, that doesn't work in the "Handlers" one.

The classic "Event Handling" example

The first example uses a classic way of event handling (Ok, really classic would be the onclick=function() method, but you get it):

YAHOO.example.toggleEventListen = {
  hide:function(){
    // add a CSS class to the main UL to hide all nested list items
    YAHOO.util.Dom.addClass(this, 'dynamic');

    // get all ULs in this one and loop over them
    var uls = this.getElementsByTagName('ul');
    for (var i=0; i<uls.length; i++){

      // get the link above this UL and add an event listener pointing to the toggle method
      var parentLink = uls[i].parentNode.getElementsByTagName('a')[0];
      YAHOO.util.Event.addListener(parentLink,'click',YAHOO.example.toggleEventListen.toggle);
    }
  },

  toggle:function(e){

    // get the first nested UL and toggle its display property 
    var ul = this.parentNode.getElementsByTagName('ul')[0];
    if(ul.style.display == 'none' || ul.style.display == ''){
      ul.style.display = 'block';
    } else {
      ul.style.display = 'none';
    }      

    // stop the link from being followed
    YAHOO.util.Event.preventDefault(e);
  }
};

This means that you loop through all the elements, and add one event handler for each of them, which can get rather slow. It also means that if you add new items, you need to set the event handlers again.

The lesser known "Event Delegation" example

The second example takes advantage of something browsers provide us with: Event Bubbling. In essence this means that whenever you do something to an element in the page (like clicking it), this element gets very excited and talkative about it - especially to its parent elements. The link clicked in the example will tell the LI it resides in, the UL it resides in and so on and so forth up until the BODY of the document. And this is what you can use to your advantage; all you need to do is to set one event handler on a main element, and use the getTarget() method of the library of your choice. Compare the event target to what you want to react to and off you go:


YAHOO.example.toggleEventDelegation = {
  hide:function(){

    // add a CSS class to the main UL to hide all nested list items
    YAHOO.util.Dom.addClass(this,'dynamic');

    // Add one event listener to the element pointing to the toggle method
    YAHOO.util.Event.addListener(this,'click',YAHOO.example.toggleEventDelegation.toggle);
  },
  toggle:function(e){
    // Check where the event came from
    var origin = YAHOO.util.Event.getTarget(e);

    // Compare with the right node name and test if the parent LI contains a list
    if(origin.nodeName.toLowerCase()==='a' && 
       origin.parentNode.getElementsByTagName('ul').length>0){
	   
      // get the first nested UL and toggle its display property 
      var ul = origin.parentNode.getElementsByTagName('ul')[0];
      if(ul.style.display == 'none' || ul.style.display == ''){
        ul.style.display = 'block';
      } else {
        ul.style.display = 'none';
      }      
      // stop the link from being followed
      YAHOO.util.Event.preventDefault(e);
    }
  }
};

The other beauty of this approach is that you can react to the same event differently according to which element reports it. You can easily extend the toggle() method to check for the LI and the UL and do other things without having to add extra handlers.

What to use when

For really small event handling efforts, the classic solution gives you more control and it is pretty easy to hand over to other developers without much explanation. Event delegation however is probably the only way to keep a large app with a lot of elements to apply handling to (or dynamically loaded elements) in check.

Comment on the blog | Digg this | Add to deli.cio.us | Technorati, Thanks to Media Temple for hosting