Event Delegation Made Easy
08 FEB
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
ChrisT at 08.02.08 / 14PM
Elegant! I like it.
sil at 08.02.08 / 15PM
Great. Let us know when we can pick up the revised Low Pro for jQ source.
timothytoe at 08.02.08 / 16PM
timothytoe: Now! It’s already commited.
Dan 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?
timothytoe 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)); } }
timothytoe 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?
Anup 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 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 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 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 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.
timothytoe 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 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 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 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.
Dan at 13.02.08 / 08AM
Hey Dan I bought a book today and guess what—you wrote one of the chapters.
timothytoe at 14.02.08 / 01AM
Hey Dan,
Why did you choose to use $.klass instead of $.Class ?
Cameron 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 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
Jake at 28.02.08 / 19PM
Thank you very much for that link, Jake!
Bart 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 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 thattarget
is used, but not referenced:if (target.match(selector)) ...... should instead beelement
?if (element.match(selector)) ...Making that change gave me the traces I expected.
Eric 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 Snyder at 11.03.08 / 21PM
dan…i thought i’d let you know that your RSS seems to be messed up.
-jonathan
Dan 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 befor (var selector in rules) { var target = $(e.target);maybe?
Alan 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 Baby at 27.04.08 / 02AM
@alan: I suppose you are right
ynw 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?
Rob 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.
Alan at 04.05.08 / 13PM
About This Article
- Posted on: 08.02.08 / 13PM
- Categories: Low Pro
- Tags: javascript, jquery, lowpro