Let's Make a Framework: Chaining Events

2010-09-09 00:00:00 +0100 by Alex R. Young

Welcome to part 29 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 demonstrated a jQuery-like API for working with DOM finders. This week
we'll build on that to add event support.

API Design

We want to be able to do this:

turing('#element').bind('click', function(e) {
  alert('Stop clicking me!');

If you haven't read the other chaining tutorials, this might not seem
interesting. The reason we're doing this is to get a chainable API for
DOM finders, like jQuery. So multiple finders could be called:

turing('#element').find('.element li a').bind('click', function(e) {
  alert('Stop clicking me!');

Adding events with Turing is performed with
turing.events.add(element, 'event name', callback). I'll
use the method name bind instead of add so it
doesn't look confusing next to DOM-manipulation code.


We need this test to pass (in

should('bind events using the chained API', function() {
  var clicks = 0;
  turing('#events-test a').bind('click', function() { clicks++; });
  turing.events.fire(element, 'click');
  return clicks;

Running it right now results in an error:

should bind events using the chained API: 1 does not equal: TypeError: Result of expression 'turing('#events-test a').bind' [undefined] is not a function.


It seems like we can just alias bind to add
with a bit of currying, but that doesn't fit with our style of keeping
each module independent (else turing.dom will rely on

However, last week I exposed the object that is returned through the
chained DOM calls: turing.domChain. Let's try extending
that from the events API if it's available.


events.addDOMethods = function() {
  // If there's no domChain then the DOM module hasn't been included
  if (typeof turing.domChain === 'undefined') return;

  // Else it's safe to add the bind method
  turing.domChain.bind = function(type, handler) {
    var element = this.first();
    if (element) {
      turing.events.add(element, type, handler);

      // NOTE: "this" refers to the current domChain object,
      //       which contains the stack of elements
      return this;

// It's safe to always run addDOMethods when
// the events module is loaded

I've commented each part, but it's fairly straightforward.


What's interesting about this approach is that
events.addDOMethods is not private. People could include
scripts in any order, and get the functionality as long as they call

Obviously load order is important here, so the script that "builds"
Turing into a single file
(Jakefile) should be aware that
turing.dom.js should come before

jake.task('concat', function(t) {
  var output = '',
      files = ('turing.core.js turing.oo.js turing.enumerable.js '
              + 'turing.functional.js turing.dom.js turing.events.js turing.alias.js turing.anim.js').split(' ')

I've written each module name by hand in the correct order, rather than
reading the file list from the file system.


Adding events support to our chainable DOM API was easier than I
expected. This could be expanded on with aliases to make it more
user-friendly (jQuery makes using events more intuitive through