jQuery 1.3 delegation

This post will most likely be useful only to me, but I'm extremely excited about the new version of jQuery that's coming out, and I wanted to put down some of its new features before I forget, and to make sure I understand them completely. If you aren't familiar with javascript event delegation, this probably won't mean a lot to you (here's a quick tutorial).

I went poking around in the jQuery revisions because I haven't found hardly any details on what exactly is changing with the new version (other than speedups and such, but those don't excited me nearly as much as other people -- probably because I tend to write my code for the speed of "right now" instead of for a future version, so if something is changing from slow to fast, I'm probably not using it anyway). There's some good stuff going into this new version.

And more than just Sizzle, which I'm not going to spend any time on.

So I've been using the jQuery event delegation plugin on my most recent project, and it works nicely, but there are a few problems. First is that your selectors have to match on the target of the event, not an ancestor. So if I have a list of "div.child"s inside of a "div.parent", I call:

$("div.parent").delegate('click', 'div.child', function(e) { });


to watch for clicks on any of the div.child's. The problem here is that if I have an image tag inside of the div.child, and the user clicks on the image instead of the whitespace in the div, then the delegation plugin won't trigger the event. I actually did a quick rewrite of the plugin on my own to check against parents as well, changing:


return this.bind(type, function(event) {
var target = $(event.target);
if (target.is(delegate)) {
return handler.apply(target, arguments);
}
});


to:


return this.bind(type, function(event) {
var target = $(event.target);
if (target.is(delegate)) {
return handler.apply(target, arguments);
}
var $container = $(this);
var $parent_target = null;
target.parents().each(function() {
if($(this).is(delegate)) {
$parent_target = $(this);
return false;
}
if($(this)[0] == $container[0]) {
return false;
}
});
if($parent_target) {
return handler.apply($parent_target, arguments);
}
});

Problem solved -- for me, anyway. But the better news is that jQuery's new delegation mechanism takes care of this for you! It's got a .closest method that matches on the element or its closest matching ancestor. Pretty vital, if you ask me, for any actually usable delegation system. In that case you could replace my code with:


return this.bind(type, function(event) {
var closest = $(event.target).closest(delegate);
if (closest.length > 0) {
return handler.apply(closest, arguments);
}
});

So that's cool, and it contented me for a while, but then I ran into another problem: bubbling. Normal user events bubble just fine, but jQuery also lets you trigger artificial events using .click, .submit, or .trigger(type) methods -- these don't bubble. At least, they didn't used to. Resig fixed that as well in the new version, for which I am extremely grateful. Hours of conniving workarounds were wasted, but I can handle that.

As it turns out, I may still be using the delegation plugin, as enhanced by the new changes in jQuery 1.3. I wasn't entirely sure how jQuery's new .live and .die functions behaved, so I didn't know how applicable they'd be, but John Resig himself left a nice comment on Ajaxian's post:

We’re not using the name ‘delegate’ because it isn’t normal event delegation (not to mention that there are already a number of delegate plugins using that name). Typically when someone wants to do full delegation they also want to specify the root element - e.g.: $(”#someroot”).delegate(”div”, “click”, someFn); We wanted to provide something that was much simpler than normal delegation but still leave room for people to write their own full delegation code/plugins.

What he means about it not being "normal event delegation" is that jQuery's .live and .die methods don't let you specify who you're delegating to. It's always a the document (root) level. So if I call:

$("div.child").live('click', function(e) { });


(which, by the way, would be the appropriate way to listen for clicks on any div.child's in the document) then it always bubbles all the way up to the document. What if you just want to catch clicks on div.child's that are inside a "div.parent"? Not possible with .live and .die. That's ok, though, because I'm willing to bet most people won't care, and if nothing else this addition offers a great starting point for those new to event delegation. This is a great piece of functionality, and the fact that it handles both inner-element events and triggered-event bubbling makes me a very happy programmer.

As a side note, you can kill all delegated functions of a given type with:

$("div.child").die('click');


or just a specific function with:

$(".div.child").die('click', function_to_stop_calling);


Resig is calling this a livequery/event delegation hybrid, although it's really more delegation than anything. I think the reasoning there is that more people know of and use livequery than the delegation plugins, so it's more familiar, and in fact the syntax is more like livequery than delegation -- although honestly there's not that much difference between the two. Either way, it's a great addition to a great library, and a useful inclusion of a great new feature in web programming.

Comments

Popular Posts