<<  Change Style
  • Normal
  • |
  • Widen Column
  • |
  • Larger Text
Oct
04
Spicy Javascript Event Delegation "Curry"

Lets make a yummy recipe for organizing your event delegation methods. You will need the following ingredients:

  • A good cupful of understanding of javascript event-delegation.
  • Some curry for that elegant javascript programming goodness
  • For jQuery developers (who have never written javascript), you should know this.

Because this is another way you could write (and organize) your event methods. Shown below (in snippet 1) is the core function that 'brews' the curry:

Snippet 1:

// Adding delegate function to the 'MyApp' namespace
MyApp.delegate = function delegate(elem, eventType, methods) {
    var slice = Array.prototype.slice, // store reference to Array's slice method
        a = slice.apply(arguments), // store reference to all the function arguments
        l = a.length,
        obj = methods ? methods : {}, // store all the event handlers
        that = this;  // Why? See http://www.crockford.com/javascript/private.html

    if (!methods) {
        /* If no methods have been passed, return a function that will
         * accept a hash of methods
         */
        return function(){
            var args = a.concat(slice.apply(arguments));
            obj = args[2];
            $(args[0]).unbind(args[1]).bind(args[1], _observers);
            return that;
        }
    }
    else {
        $(elem).unbind(eventType).bind(eventType, _observers);
    }

    function _observers(e){
          e.stopPropagation();
           var target = e.target || e.srcElement;
           
        for(var selector in obj){
            if($(target).is(selector)) {
                return obj[selector].call(target, e);
            }
        }
    }

    return this;
} // delegate function ends

And the way its *savored* is as follows:

Snippet 2:

Usage 1 (without currying)
MyApp.delegate('#parent-div', 'click', {
   '.child-div a' : function(){
        // handle click event on '.child-div a' link here
  },

  '#child-div2' : function(){
       // handle click event on '#child-div2' id element here
   }
// add more comma-separated functions to handle
// click events on child-nodes of '#parent-div'
});

Usage 2 (with currying)
var delegateToParent = MyApp.delegate('#parent-div', 'click');
/* NOTE: a good naming convention could be 'delegateTo'
 * For e.g delegateToContent  for '#content'
*/
...
delegateToParent({
    '.child1' : function(){},
   '#child2': function(){},   
});

Now to help you better understand this, let me tell where this could be useful. Imagine a section of a webpage where a lot of interactions happen. I like to consider that section as a UI block or module that you can repeat across the site. As an example, a Facebook update could be a UI module and each update is independent of the updates above and below it. A single Facebook update has lot of things going on in it - the hide or report buttons, ajax comment submit, lots of mouseover effects etc. Instead of binding events to each child-element of the UI block, you could (and ideally should) delegate event handling to that UI block for any event that happens inside it. And if you've read the above 'pre-requirements' links, you'll know why this is better.

So, lets quickly look at some parts of the code. For the first case (without currying), we're sending a hash of methods and each method deals with a particular child element in the parent block. So if an event (say click) happens on one of the child elements, the event bubbles up to the parent ('#parent-div' in the code above) and is handled by the delegate method, wherein the '_observers' function will deal with the hash and run the appropriate handler function. (See comments in snippet 3 below for explanation of each step):

Snippet 3:

function _observers(e){
        // keep event confined to the element that was clicked
          e.stopPropagation();

        // capture the child element that "registered the event"
           var target = e.target || e.srcElement;
           
        /* 'obj' is the key-value hash of handler methods
         *   where the key is the css/tag selector and
         *   value is the function that will handle
         *   the event on the element defined by the selector
         */
        for(var selector in obj){
            if($(target).is(selector)) {
                /* Apply the function handler on the child element.
                 * I am sending the jQuery event object 'e' to the handler,
                 * so that you can work with the event object as required
                 */
                return obj[selector].call(target, e);
            }
        }
    }

That is easy to understand. But whats happening in the second case? Consider the following line:

var delegateToParent = MyApp.delegate('#parent-div', 'click');

Note that i am passing only 2 parameters instead of 3 - the parent UI block which is listening to its children and the type of event its listening for. What happens then? The 'methods' argument is undefined (i.e the third parameter in MyApp.delegate). So the following block is called:

if (!methods) { ...
...

And this returns a function (we'll soon see what that function does) which is assigned to 'delegateToParent' variable:

if (!methods) {
        return function(){ ...
           // this function block now belongs to 'delegateToParent'
...

This returned function (lets call it the 'currying' function) works as follows:

 

var args = a.concat(slice.apply(arguments));

The 'arguments' is a single-element array containing the event listeners that we will pass as a hash (or an object or collection - call it Murphy if you will. A Murphy of event listeners, eh? Not bad! ). I combine it with 'a' (available via closure) which were the 2 parameters that were passed initially. So 'args' completes the set of parameters that MyApp.delegate method expects.

 

obj = args[2];

Then we assign the Murphy to our 'obj' variable which the '_observers' function will use via closure (as described here).

 

$(args[0]).unbind(args[1]).bind(args[1], _observers);

Finally we use jQuery's event binding to listen to the children of the parent UI block.

So 'delegateToParent' has the 'currying' function. Now all you need to do is pass (as shown in the second line above) a Murphy of event listeners that 'should react' when something happens inside the parent UI block. You could say to yourself, "Delegate to Parent, this set of functions" :

var contentListeners = { 
    'a.report' : function(e){ e.preventDefault(); // foo bar },
    'div.myprofile' : function(){},
     ...
};

delegateToContent(contentListeners); // I replaced 'Parent' with 'Content' in this example

-

Pros:

  • Better code organization
  • Encourages 'modular' web programming
  • You can reuse 'delegateTo<selector>' and the Murphy code blocks
  • Event delegation is sweet

Cons:

  • Cannot pass arguments other than 'e' to the Murphy functions. So the following code:
    { '.child-div' : function(e, col){ $(this).css({'color' : col }); } ... }

    would not work. The problem could have been solved if it were not for the deprecation of Function arguments.
  • Danger of polluting the global namespace with global variables like 'delegateToParent' or 'contentListeners' if developers don't use everyone's new favorite js pattern
-

And that is all. Use it at your own convenience and utility. Do post your criticisms if you find something fundamentally wrong. Cheers!

Short URL:

How about some bookmarking love?

Post To Twitter Bookmark on Delicious Bookmark on Digg Share on Facebook Update On Friendfeed Bookmark on Reddit Bookmark on Mixx
---
0 comments for this article ( 0 pending )

Leave a comment:

  • Captcha ImageRefresh Captcha