I was working on a BackboneJS project recently that was using UnderscoreJS for templating and started wondering if its possible to invoke UnderscoreJS recursively.
At face value, the _.template function simply takes a template string and converts it to a first class Javascript function so I figured it would be possible. Out of curiosity, I decided to take a look at how the _. function is actually implemented:
_.template = function(text, data, settings) {
settings = _.defaults(settings || {}, _.templateSettings);
// Compile the template source, taking care to escape characters that
// cannot be included in a string literal and then unescape them in code
// blocks.
var source = "__p+='" + text
.replace(escaper, function(match) {
return '\\' + escapes[match];
})
.replace(settings.escape || noMatch, function(match, code) {
return "'+\n_.escape(" + unescape(code) + ")+\n'";
})
.replace(settings.interpolate || noMatch, function(match, code) {
return "'+\n(" + unescape(code) + ")+\n'";
})
.replace(settings.evaluate || noMatch, function(match, code) {
return "';\n" + unescape(code) + "\n;__p+='";
}) + "';\n";
// If a variable is not specified, place data values in local scope.
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
source = "var __p='';" +
"var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n" +
source + "return __p;\n";
var render = new Function(settings.variable || 'obj', '_', source);
if (data) return render(data, _);
var template = function(data) {
return render.call(this, data, _);
};
// Provide the compiled function source as a convenience for build time
// precompilation.
template.source = 'function(' + (settings.variable || 'obj') + '){\n' +
source + '}';
return template;
};
Not any real surprises there - it basically does a series of string replaces on the template you pass in to interpolate any variables you want outputted, converts blocks you want evaluated to regular Javascript, and then injects the object you passed in into the local scope.
Then, the “magic” happens at “var render = new Function(settings.variable || ‘obj’, ‘_’, source);” where a new function is created with a “print” function locally defined and your template converted to a Javascript function.
Here is the “source” string that is passed into “new Function”:
var __p = '';
var print = function () {
__p += Array.prototype.join.call(arguments, '')
};
with(obj || {}) {
__p += '\n';
if (depth) {;
__p += '\n<div class=\'fib-box\' data-depth=\'' + (depth) + '\' style=\'width: ' + (val) + 'px; height: ' + (val) + 'px;\'></div>\n';
print(template(getFibObj(depth - 1)));
__p += '\n';
};
__p += '\n';
}
return __p;
That is the dynamic function generated for the following template:
<% if(depth){ %>
<div class='fib-box' data-depth='<%= depth %>' style='width: <%= val %>px; height: <%= val %>px;'></div>
<% print(template(getFibObj(depth-1))) %>
<% } %>
Since its looking for the “template” function in the global scope it looks like everything should work fine. To test it, I decide to take the Fibonacci sequence and generate boxes for each of the numbers in the sequence up to some N.
Theres a live demo running at http://twitlabs.net/us/ and a Gist of the code is replicated below:
https://gist.github.com/3223733.js
Just for fun, you can also change how the boxes are arranged by toggling between “Stacked” and “Cascaded”.
Anyway, fair warning - I imagine there are some performance implications regarding doing this as well as computational limits. You could potentially cause a stack overflow by consuming your entire stack via the recursion.
As always, questions, comments, concerns welcome!