Chronicling the trials and tribulations of developing for the modern web.



Basic event delegation in Prototype

Posted by Pat Nakajima on February 13, 2008 in Javascript.
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:
// Introduces Event delegation (http://icant.co.uk/sandbox/eventdelegation)
Object.extend(Event, {
  delegate: function(element, eventName, targetSelector, handler) {
    var element = $(element);

    function selectorMatch(element) {
      return element.match(targetSelector);
    }

    function validateTarget(origin) {
      if ( origin.match(targetSelector) ) { return origin; }
      var ancestors = origin.ancestors();
      return ancestors.find(selectorMatch);
    }

    function createDelegation(_delegatedEvent) {
      var rawOrigin = _delegatedEvent.element();
      var origin = validateTarget(rawOrigin);
      if ( origin != null && (typeof handler == 'function') ){ 
        _delegatedEvent.element = function() { return origin; }
        return handler(_delegatedEvent);
      }
    };

    element.observe(eventName, createDelegation);
    return element;
  },

  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.