Pretending that Javascript is XSL, part 2: jQuery++

If you’re here, then you probably came from here, and you want to make Javascript more functional. If you didn’t come from there—and this is getting a bit like a Choose-Your-Own-Adventure book, isn’t it?—then you might want to go there first, to see if you want to be here.

So: functional Javascript. Not just functional, but with all the automation of XSL transformations and CSS applications, where you can set it all running and it’ll produce something and hopefully throw no errors. Let’s start with jQuery.

jQuery provides Javascript with a functional framework. Here’s the equivalent of the examples in XSL and CSS, supported by the inclusion of jquery.js:

jQuery("p.intro").each(
  function() { this.style.color = green; }
);

I hope the similarities are clear, and not too strained. Now all three languages do implicit looping over sets of element nodes, and no longer require checks for missing elements; that’s evidence that it’s starting to behave functionally. There’s still a few pieces missing, though. We’d like to be able to iterate over the tree with a set of default rules, and also replace the default rules with our own where necessary.

What would the default rule look like? Well, we can pass around all sorts of objects—this being an object-oriented language—but for now let’s play it safe and follow XSL’s lead, and have each node return the concatenated text returned by all its child nodes. That means that, by default, the whole HTML document would return an empty string. It might be nice to return an array of equivalent objects, or even some transformed tree, but let’s remain old-skool. Anyway, we can always serialize any HTML elements we want to include as text, and then stick them back into the DOM later. There’s probably a way of doing some of these tasks with core jQuery, but as we’re also passing result data around as well as input data, I’m going to step outside the framework (its extension model typically takes a jQuery object in, and returns a modified jQuery object, which isn’t quite what we’re after).

Here’s a default rule: it says “call the default rule (i.e. me) on all my children”. We’ll call this rule treewalker, because that’s what it does. We’ll also assume that we’re going to assign this function as the .treewalk method on each element:

var treewalker = function(i) {
  var t = "";
  $(this).children().each(function(i) { t += this.treewalk(); } );
  return t;
}

And here’s a way of assigning rules to elements. It’ll assign the rule as the .treewalk method unless we specify otherwise.

var template = function(specifier, fn, property) {
  if (typeof property == "undefined") property = "treewalk";
  $(specifier).each(function(i) { this[property] = fn; });
}

It looks a bit clunky, because falling back on the default property means we have to have an if-exists check. That’s to be avoided where possible in functional programming, but bear in mind that we’re still looking under the bonnet (or “hood” if you like), not at the actual functional code. We’ll get to the fully-functional bit shortly.

We’ve got one last bit and we’re done. We want to put the default rule on every element within a certain scope: we’ll assume for now that the whole HTML document body is to be treated; that might be computationally heavy for big documents, but we could change that. We’ve already defined a way of putting rules onto things, so let’s use that to put the treewalker function in as both .treewalk and .default. That way, we have a copy of the method hanging around, that we can fall back on if we overwrite it:

template("body, body *", treewalker);
template("body, body *", treewalker, "default");

That’s it. We’re now ready to pretend our Javascript is XSL. Here’s how we run it:

var result = document.body.treewalk();

Try it. “But that’s just an empty string!” you might, if you were feeling ungrateful, complain. Are you never satisfied? More on this later.