Let's Make a Framework: Chaining

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

Welcome to part 28 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 was talking about chaining DOM finders, jQuery style. I demonstrated a
little script that duplicated a fake version of this functionality. This
week I'll start building it for real.

Please read last week's
if any of
this sounds confusing, everything here draws on that.


What we want to be able to do is chain finder methods:


This would find things with the class example, then the associated
paragraphs. We can use turing.dom.get to implement the core
functionality, but get() does not accept a "root" element,
so we'll need to add that.

Another thing is, calling turing() makes no sense, because
it isn't a function. Let's address that while we're at it.

The alias module will also have to be changed, because it currently
wraps turing.dom.get anyway.


The implementation should satisfy the following test in

given('chained DOM calls', function() {
  should('find a nested tag', turing('.example3').find('p').length).equals(1);

I should cover more methods and cases, but I'm on a tight schedule here!

Updating Core

This is simpler that you might expect. The core module currently exposes
turing as an object with a bunch of metadata properties.
This can be changed to a function to get the jQuery-style API. The only
issue is I don't want to make turing.dom a core

To get around that I'm going to allow an init method to be overridden
from outside core. This could be handled in a better way to allow other
libraries to extend the core functionality, but let's do it like this
for now:

function turing() {
  return turing.init.apply(turing, arguments);

turing.VERSION = '0.0.28';
turing.lesson = 'Part 28: Chaining';
turing.alias = '$t';

// This can be overriden by libraries that extend turing(...)
turing.init = function() { };

Then in the DOM library:

turing.init = function(selector) {
  return new turing.domChain.init(selector);

This last snippet is based on the
fakeQuery example from last week.

Updating turing.dom

This is all completely taken from the fakeQuery example. The real
find method in turing.domChain (which came
from fakeQuery.fn) looks like this:

find: function(selector) {
  var elements = [],
      ret = turing(),
      root = document;

  if (this.prevObject) {
    if (this.prevObject.elements.length > 0) {
      root = this.prevObject.elements[0];
    } else {
      root = null;

  elements = dom.get(selector, root);
  this.elements = elements;
  ret.elements = elements;
  ret.selector = selector;
  ret.length = elements.length;
  ret.prevObject = this;
  return ret;

It depends on dom.get for the real work, which I covered
way back in part 6
(and onwards).

The writeElements method sets each element to a numerical
property, so the Array-like API is available:


I also added a shorthand first() method to the same class
while I was at it.

DOM Root

Setting a "root" element for dom.get looks like this:

dom.get = function(selector) {
  var tokens = dom.tokenize(selector).tokens,
      root = typeof arguments[1] === 'undefined' ? document : arguments[1],
      searcher = new Searcher(root, tokens);
  return searcher.parse();

An undefined property will become document, which means it
can accept null. I had to make the existing
find methods check for null as a special case.


These two chainer tutorials illustrate:

Adapting the fakeQuery prototype was actually surprisingly easy. And now
the chainer returns domChain, we can decorate it with lots
of helper methods like first() to make DOM traversal very

This is far from a complete solution, but the foundations are now there.