Event Delegation Made Easy

I’m having a lot of fun poking around jQuery at the moment and came up with a cool little thing that’s going into Low Pro for jQuery but is a nice stand-alone little snippet for implementing event delegation. Since the Christian and the guys at YUI started talking about it event delegation has gone from being something that I’d use occasionally to the way I do nearly all my event handling. If you aren’t familiar with the technique go and click that previous link and read Christian’s article now – it’s important.

In most instances I end up writing a lot of event handlers that look like this:

$('#thing').click(function(e) {
  var target = $(e.target);

  if (target.hasClass('quit') return doQuitStuff();
  if (target.hasClass('edit') return doEditStuff();
  // and so on...
});

Obviously, writing a lot of the same kind of code is a warning sign that something needs refactoring but I’ve never come up with a nice way to abstract this. But with a little bit of functional magic I’ve just found with something I really like. Here’s what I came up with:

jQuery.delegate = function(rules) {
  return function(e) {
    var target = $(e.target);
    for (var selector in rules)
      if (target.is(selector)) return rules[selector].apply(this, $.makeArray(arguments));
  }
}

Using it is simple:

$('#thing').click($.delegate({
  '.quit': function() { /* do quit stuff */ },
  '.edit': function() { /* do edit stuff */ }
}));

The function simple runs through the rules checking if the element that fired the event belongs to that selector then calls the corresponding handler passing the original event object through. The great thing about it is that you can use it in Low Pro behavior classes:

DateSelector = $.klass({
  onclick: $.delegate({
    '.close': function() { this.close() },
   '.day': function(e) { this.selectDate(e.target) }
  }),
  selectDate: function(dayElement) {
    // code ...
  },
  close: function() {
    // code ...
  }
});

I’m not sure of the performance implications of using is() so heavily but some form of caching could be added if it was a problem. Still, it’s a really nice little bit of syntactic sugar that’s going into Low Pro for jQuery and I’ll be using it a lot.

UPDATE: I should have added that there’s a version of this in Low Pro for Prototype. In case you want to use it on its own:

Event.delegate = function(rules) {
  return function(e) {
      var element = $(e.element());
      for (var selector in rules)
        if (element.match(selector)) return rules[selector].apply(this, $A(arguments));
    }
}

Meanwhile, you might want to take a look at the patch by Peter Michaux.

30 Comments (Closed)

So now you’ve had a bit more time with jQuery, how do you like it compared with Prototype. Must admit after reading the jSkinny post was really tempted by the syntax

ChrisTChrisT at 08.02.08 / 14PM

Elegant! I like it.

silsil at 08.02.08 / 15PM

Great. Let us know when we can pick up the revised Low Pro for jQ source.

timothytoetimothytoe at 08.02.08 / 16PM

timothytoe: Now! It’s already commited.

DanDan at 08.02.08 / 16PM

Where is it? Here:

http://github.com/danwrong/low-pro-for-jquery/tree/master/src/lowpro.jquery.js?raw=true

What changed?

timothytoetimothytoe at 08.02.08 / 16PM

Oh I see…

delegate: function(rules) {
  return function(e) {
    var target = $(e.target);
    for (var selector in rules)
      if (target.is(selector)) return rules[selector].apply(this, $.makeArray(arguments));
  }
}

timothytoetimothytoe at 08.02.08 / 16PM

Very, very nice. This has been something I wanted to do recently with jQuery and hadn’t got round to it. This is going to be really useful. Many thanks.

I guess one crude way to test the performance difference might be using the profiler on Firebug to compare before and after?

AnupAnup at 08.02.08 / 21PM

Pretty cool. Your post gave me the idea to try to implement something similar for Prototype. A few hours later, I came up with this patch.

I was curious about whether or not your handler works with the onsubmit event, as it doesn’t bubble. I’ve got something close to ready that relies on Prototype’s custom events, but I don’t know if jQuery offers any similar API.

Again, nice work!

Pat NakajimaPat Nakajima at 09.02.08 / 05AM

Dan, I’ve played around with using global event delegation (i.e. listeners on body or window elements) as the exclusive technique for enabling a page. On pages that are primarily documents (rather than applications) and for event types that bubble, it seems like a viable technique and perhaps the ultimate in unobtrusive JavaScript. It avoids having to worry about when the DOM is ready to attach listeners with something like Dean Edward’s window.onload script which does leave the elements unenlivened for a short time during page load.

The events that don’t bubble (e.g. focus) can’t use global delegates without at least a gentle nudge. (It is unfortunate all event types don’t bubble.) I found one solution (http://peter.michaux.ca/article/3752) which forces the non-bubbling event to pseudo bubble. I’m curious what you have thought about delegates and non-bubbling events.

Peter MichauxPeter Michaux at 09.02.08 / 06AM

Dan, not the most concise syntax I have like your or jQuery but you may be interested into looking at this implementation of event delegates:

http://jsavascript.nwbox.com/NWEvents/delegates.html

this is what that thread on Perter Michaux blog produced in my code. No “onload” and a much faster way of comparing/matching elements with selectors have produced a nice implementation (thanks also to Peter suggestions and fast matcher skeleton).

The most important non bubbling events like “focus” and “blur” have been fixed cross-browser with a nice trick. And the matcher I use is blazing fast, with a very complete CSS3 selector/matcher engine.

http://jsavascript.nwbox.com/NWMatcher/

I am very interested into have feedback, I know the idea of event delegation is nothing new, however I believe the way I implemented it is somehow a real big improvement in unobtrusiveness.

Diego

Diego PeriniDiego Perini at 10.02.08 / 01AM

I badly mistyped the above links.

The correct links are:

Delegates an example of NWEvents capabilities.

NWMatcher the CSS3 Selector engine used above.

The latest versions of the above are always available in GoogleCode NWEvents, and they are maintained.

Notice that the “match()” method in NWMatcher just matches the element properties with the passed selector, it does not do any recursion or collection filtering, this is fast. No XPath is used (so cross-browser).

If a delegated object is not passed in during the event setup, by default I use the omnipresent “document.documentElement” to listen for bubbling events, then when the event fires, if the element source of the event matches the selector the bound function is executed.

This work on the basis that an element can not trigger any event until it is actually inserted in the DOM, so this technique can completely eliminate the exposure of functional elements we talked about in Peter blog.

Diego PeriniDiego Perini at 10.02.08 / 22PM

Diego, I did look at your stuff and I was impressed. Clean. Speedy. I’m going to have to spend more time with it. Maybe in a few weeks when I get a chance I can build a small project with it and get a feel for how it goes.

I like how everyone is pushing and pulling at JavaScript to get what they want.

timothytoetimothytoe at 10.02.08 / 23PM

This is wicked stuff and the kinda thing that I always thought felt dirty when I’ve been using jQuery to associate events to things.

A very usable piece of code!

Ross BrunigesRoss Bruniges at 11.02.08 / 09AM

Instead of:

$('#thing').click($.delegate({
  '.quit': function() { /* do quit stuff */ },
  '.edit': function() { /* do edit stuff */ }
}));

why not:

$('#thing').delegate({
  '.quit': function() { /* do quit stuff */ },
  '.edit': function() { /* do edit stuff */ }
}));

This is one case where the typical jQuery.fn pattern works quite nicely. Or is there a consideration I’m missing?

Yehuda KatzYehuda Katz at 13.02.08 / 04AM

Whoops… little typo in the last one. I’m taking the opportunity in the correction post to explain how I would do this the way I suggested:

jQuery.fn.delegate = function(eventType, rules) {
  return this.bind(eventType, function(e) {
    var target = $(e.target);
    for(var selector in rules)
      if(target.is(selector)) 
        return rules[selector].apply(this, arguments)
  })
}
And the way you would use it:
$("#thing").delegate("click", {
  ".quit": function() { /* do quit stuff */ },
  ".edit": function() { /* do edit stuff */ }
})

Yehuda KatzYehuda Katz at 13.02.08 / 04AM

Yehuda: Yes, there is one thing. It’s designed to work with Low Pro behavior classes and that way wouldn’t work in that case. However, the way you detailed is nice as well so I think I might include both in Low Pro.

DanDan at 13.02.08 / 08AM

Hey Dan I bought a book today and guess what—you wrote one of the chapters.

timothytoetimothytoe at 14.02.08 / 01AM

Hey Dan,

Why did you choose to use $.klass instead of $.Class ?

Cameron WestlandCameron Westland at 17.02.08 / 19PM

What exactly does is() do? I cannot find anything about it. Search engines don’t index the word because it’s too short.

Bart FeenstraBart Feenstra at 28.02.08 / 16PM

‘is()’ runs an expression against the current selection, and return true if the expression matches something.

See: http://docs.jquery.com/Traversing/is#expr

JakeJake at 28.02.08 / 19PM

Thank you very much for that link, Jake!

Bart FeenstraBart Feenstra at 28.02.08 / 21PM

that is really “event delegation – made easy”, especially the easy to follow instructions and code-snippets. good job dan!

Paul PusheePaul Pushee at 01.03.08 / 13PM

Hey Dan, I tried the delegate code you posted for Prototype, but I didn’t get the result I expected (e.g., the following didn’t trace anything):
$('pagination').observe('click', Event.delegate({
    '.prev': function() { console.log('prev') },
    '.next': function() { console.log('next') }
}));
I noticed that target is used, but not referenced:
        if (target.match(selector)) ...
... should instead be element?
        if (element.match(selector)) ...
Making that change gave me the traces I expected.

EricEric at 06.03.08 / 16PM

This approach to delegation is quite exiting! I was brainstorming if it is possible to use delegation in place of the current jQuery#click, mouseover, mouseout, etc. functions. I came up with the following, but it doesn’t work because jQuery#is does not accept jQuery objects. Is there another function I can use? Or something like jQuery#getSelectorText()? (I’m new to jQuery.) Perhaps a backwards approach won’t work :)

var events = 'click mouseover mouseout'.split(' ');
for (var i = 0; i < events.length; i++) {
  var eventName = events[i];
  jQuery.fn[eventName] = function(observer) {
    var selector = this;
    $(document).bind(eventName, function(e) {
      if ($(e.target).is(selector)) 
        return observer.apply(this, $.makeArray(arguments));
    });
  }
}

Ken SnyderKen Snyder at 11.03.08 / 21PM

dan…i thought i’d let you know that your RSS seems to be messed up.

-jonathan

DanDan at 11.04.08 / 22PM

I think there’s a small bug in the delegate function of the current release:

:line 82

var target = $(e.target);
for (var selector in rules) {
should be
for (var selector in rules) {
  var target = $(e.target);
maybe?

AlanAlan at 21.04.08 / 13PM

Hi Dan, This is my first visit to your blog and I have to say, i like it! Really nice design / look and feel, love the simple use of JS to spcie it up and of course being a CSS freak myself, love the fluid layout.

I’m more of a mootool girl myself so can’t comment too much on jQuery but your code looks nice, neat and very similair to mootools.

Chloe BabyChloe Baby at 27.04.08 / 02AM

@alan: I suppose you are right

ynwynw at 30.04.08 / 00AM

@alan + vnw – why is this a bug? if that code is in the for in loop then it has to execute each time as opposed to just once?

RobRob at 02.05.08 / 02AM

@Rob, because the next line contains
((target = target.parents(selector))
so target needs to be reset at each iteration of the loop.

AlanAlan at 04.05.08 / 13PM

About This Article