Let's Make a Framework: Event Delegation 2

23 Sep 2010 | By Alex Young | Tags frameworks tutorials lmaf dom events

Welcome to part 31 of Let’s Make a Framework, the ongoing series about building a JavaScript framework.

If you haven’t been following along, these articles are tagged with lmaf. The project we’re creating is called Turing.

Last week I wrote about event delegation in popular frameworks. This week I’ll demonstrate a simple implementation for Turing.

dom.findElement

This method will make dealing with event delegation much easier. Recall last week’s tutorial:

$('navigation').observe('click', function(event) {
  var element = event.findElement('a');
  if (element) {
    // Handler
  }
});

This Prototype code simply uses the event’s target element to see if it matches a selector. Turing’s delegation API could wrap this up a method with a signature like this:

turing.events.delegate(document, selector, 'click', function(e) {
  // Handler
});

The body of delegate will look a bit like the Prototype example above.

There’s a few obstacles to building findElement with Turing’s event library as it stands though. A few months ago we built a class called Searcher that can recurse through the DOM to match tokenized CSS selectors.

The Searcher class could be reused to implement findElement, the matchesAllRules method in particular is of interest.

Tests

I’d like the following tests to pass (events.js):

given('a delegate handler', function() {
  var clicks = 0;
  turing.events.delegate(document, '#events-test a', 'click', function(e) {
    clicks++;
  });

  should('run the handler when the right selector is matched', function() {
    turing.events.fire(turing.dom.get('#events-test a')[0], 'click');
    return clicks;
  }).equals(1);

  should('only run when expected', function() {
    turing.events.fire(turing.dom.get('p')[0], 'click');
    return clicks;
  }).equals(1);
});

To get there, we need findElement and corresponding tests (dom.js):

given('a nested element', function() {
  var element = turing.dom.get('#dom-test a.link')[0];
  should('find elements with the right selector', function() {
    return turing.dom.findElement(element, '#dom-test a.link', document);
  }).equals(element);

  should('not find elements with the wrong selector',function() {
    return turing.dom.findElement(turing.dom.get('#dom-test .example1 p')[0], 'a.link', document);
  }).equals(undefined);
});

This test depends on the markup in dom_test.html.

Adapting the Searcher Class

After I looked at matchesAllRules, in turing.dom.js I realised it shouldn’t be too hard to make it more generic. It previously took an element and searched its ancestors, but we need to include the current element in the search.

To understand why, consider how findElement should work (this is simplified code):

dom.findElement = function(element, selector, root) {
  while (element) {
    if (matchesAllRules(selector, element)) {
      // We've found it!
      return element;
    }
    // Else try again with the parent
    element = element.parentNode;
  }
};

All I had to do was refactor code that relies on matchesAllRules to pass an element’s parentNode instead of the element itself.

The start of the matchesAllRules method now looks slightly different:

Searcher.prototype.matchesAllRules = function(element) {
  var tokens = this.tokens.slice(), token = tokens.pop(),
      matchFound = false;

The code that refers to the ancestor element has been removed and the element argument is used instead.

The Event Delegation Method

We need to wrap the user’s event handler with one that checks the element is one we’re interested in, and other than that it’s standard Turing event handling:

if (turing.dom !== 'undefined') {
  events.delegate = function(element, selector, type, handler) {
    return events.add(element, type, function(event) {
      var matches = turing.dom.findElement(event.target, selector, event.currentTarget);
      if (matches) {
        handler(event);
      }
    });
  };
}

This code checks to see if dom is available because we don’t want interdependence between the modules. Then it sets up a standard event handler and uses findElement to ensure the event is one we’re interested in.

Conclusion

This implementation is very naive and there’s no easy delegate event removal, but it illustrates the fundamentals: event delegation depends on existing DOM methods and event handling to be developed in a reusable style.


blog comments powered by Disqus