Event delegation is a technique that I wish I picked up a long time ago. If you’re unfamiliar with it, check out
this post from the YUI blog for a terrific explanation. Then check out
Dan Webb’s implementation for jQuery. Then come on back here to see how we can do the same with Prototype.
Event delegation is made possible by the fact that certain
DOM events “bubble” up through their ancestors. So if we listen for
onclick events in the body, we’ll should hear every one that occurs throughout the page. And if we want to assign certain behaviors to certain elements’
onclick event, we just have to check to see if the target element of the event matches the group of elements to which we want to assign a particular behavior. Make sense? Not so much? Here’s some code:
Object.extend(Event, (function(){
return {
// Delegates a single behavior to elements that
// match the targetSelector
delegate: function(element, eventName, targetSelector, handler) {
var element = $(element);
function createDelegation(_delegatedEvent) {
var origin = _delegatedEvent.element();
if ( origin.match(targetSelector) ){ return handler(_delegatedEvent); }
};
element.observe(eventName, createDelegation);
return element;
},
// Delegates multiple behaviors for a single event name,
// LowPro style.
delegators: function(element, eventName, rules) {
var element = $(element);
function delegateRule(rule) {
element.delegate(eventName, rule.key, rule.value)
}
$H(rules).each(delegateRule)
return element;
}
}
})())
Element.addMethods({
delegate: Event.delegate,
delegators: Event.delegators
})
Object.extend(document, {
delegate: Event.delegate,
delegators: Event.delegators
})
With that snippet, we have two new methods available to our page elements:
delegate and
delegators (this could definitely be refactored into a cleaner implementation, but for the sake of illustration, it’s pretty good). So now if we had a
div with some links in it, we could delegate behaviors within that
div like so:
ElementBehaviors = {
// I create an alert message out of my target element's innerHTML
alertify: function(event) {
var element = event.element();
alert(element.innerHTML);
event.stop();
},
// I remove my target element
removify: function(event) {
var element = event.element();
element.remove();
event.stop();
}
}
$('div_id').delegators('click', {
'.alert': ElementBehaviors.alertify,
'.remove': ElementBehaviors.removify
})
Now, links within $('div_id') with the class name .alert will have their innerHTML alerted when clicked, and elements with the class name remove will just be removed when clicked, even if they are added to the page dynamically. No reloading or reassignmening of handlers is necessary.
So that’s great. But what if you want to event delegation for events that don’t bubble, such as form submissions?
Simulating event bubbling with Prototype’s custom events.
To simulate event bubbling, we have to resort to listening for
bubbling events that can cause non-bubbling events. By checking the circumstances surrounding these “trigger” events, we can determine whether or not to fire a custom event. Here’s some code:
var Bubbler = {
// Checks to see whether or not this element will submit
// a form if the Enter key is pressed within it.
submittableInput: function(element) {
var element = $(element);
return ( element.match('input[type=text]') || element.match('input[type=password]') )
},
// Checks to see whether or not this element will submit a
// form if clicked.
submitButton: function(element) {
var element = $(element);
return ( element.match('input[type=submit]') || element.match('input[type=image]') )
},
Behaviors: {
// Fires the 'form:submitted' custom event if the Enter key was
// pressed while the cursor was within a input that would submit
// a form.
keypress: function(event) {
if ( event.keyCode == 13 ) {
var element = event.element();
if ( Bubbler.submittableInput(element) ){
element.form.fire('form:submitted', { 'originalEvent': event });
}
}
},
// Fires the 'form:submitted' custom event if an element that
// would submit the form was clicked.
click: function(event) {
var element = event.element();
if ( Bubbler.submitButton(element) ) {
element.form.fire('form:submitted', { 'originalEvent': event });
}
}
}
}
// Always remaining vigilant.
Event.observe(document, 'keypress', Bubbler.Behaviors.keypress)
Event.observe(document, 'click', Bubbler.Behaviors.click)
The above code continually listens to all keypress and onclick events in the entire document. When one of them matches the conditions required to submit a form, it’s smart enough to find that particular form element, and fires the ‘form:submitted’ custom event from it. Custom events bubble up through the DOM.
Prototype’s custom events also have a “memo” hash which can be used to store additional information about the event. This code makes use of it by stashing the original trigger element with the key ‘originalElement’ where it can be accessed by whatever handler (or delegator) takes the custom event. Let’s take a look at a delegator here:
var FormBehaviors = {
// Takes a custom event, submit's the event's target (a form)
// via AJAX and stops the trigger event if it exists.
remotify: function(event) {
var element = event.element();
element.request();
if ( event.memo['originalEvent'] != null )
event.memo['originalEvent'].stop()
event.stop();
}
}
document.delegate('form:submitted', '.remotify', FormBehaviors.remotify)
So now, any form with the class name “remotify” will be submitted via AJAX, again including those added dynamically. And again, no handler refreshes or reassignments were necessary.
If you haven’t played with event delegation yet, give it a try. And if you’re way better than me at Javascript, and can point out some better ways for what I’ve described above, please do share in the comments.