So one day you decided to write a web application where the rendering of templates is shared between the server and client and you wonder to yourself "how in the world can I share templates between my server and client without needlessly duplicating code and/or templates?". Today we're going to look at how to address this problem using Underscore's (or Lo-Dash if you prefer) templating engine.
Templates in Underscore
For the uninformed, Underscore (and Lo-Dash, which is a fork of Underscore that has more functionality and is allegedly faster) is a Javascript utility library that provides a crap-load of useful (and cross-browser) helper functions, one of which is a templating system that is similar to both EJS and ERB for those of you that maybe have used Ruby. The even better thing is that Underscore and Lo-Dash work not only in the browser but in NodeJS as well making their templating system perfect for this use-case.
Templates look a like this:
<div class="my-super-awesome-div"><%= mySuperAwesomeVariable %></div><ul class="things-that-are-awesome"><% _.each(thingsThatAreAwesome, function(thing){ %><li><%= thing %></li><% }); %></ul>
Unlike templating languages like Mustache/Handlebars, you can use all of the features of Javascript in your templates. This is entirely up to you and really depends on your general idea of the purpose of templates and if logic, let alone ALL of Javascript's features should be accessible.
Generating templates
To begin with, we're going to start on the server-side. We're going to assume that our template above lives in a file on the filesystem exactly as you see it in the block above. We're going to read it and feed it as a string into the template engine.
var _ = require('lodash'); // or 'underscore' if you so choosevar fs = require('fs');
fs.readFile('/path/to/your/template', function(err, data){
data = data.toString();
var template = _.template(data);
});
The result of _.template()
is a function that you can then pass a block of data to. To use our template we would do something like this:
var templateString = template({
mySuperAwesomeVariable: 'Im super awesome',
thingsThatAreAwesome: ['This is awsome', 'So is this', 'This is too!']
});
The interesting thing to note here is that we passed an object into our template, but we're referencing variables in the template itself. Turns out, when you evaluate your template, it will take the keys of the object you passed in and create variables out of those keys in the scope of your template. If we do a console.log(template);
we can kinda see what is going on (Ive formatted it to be a little more readable):
{ [Function]
source: 'function(obj) {
obj || (obj = {});
var __t, __p = \'\', __e = _.escape, __j = Array.prototype.join;
function print() { __p += __j.call(arguments, \'\') }\n
with (obj) {
__p += \'<div class="my-super-awesome-div">\\n\\t\' +\n
((__t = ( mySuperAwesomeVariable )) == null ? \'\' : __t) +\n\ '\\n</div>\\n<ul class="things-that-are-awesome">\\n\\t\';\n
_.each(thingsThatAreAwesome, function(thing){ ;\n
__p += \'\\n\\t\\t<li>\'
+\n((__t = ( thing )) == null ? \'\' : __t) +\n\ '</li> \\t\\n\\t\';\n
}); ;\n
__p += \'\\n
</ul>\\n\';\n\n}\n
return __p\n
}'
}
In short, it creates a function that that takes a single argument (the object that we pass in), and builds a string withing a with
block. The with
block is the magic that takes our arguments object and creates variables in the template's scope from the keys and values of the object.
Using your template in the client
Now that we have a compiled template, how in the hell do we get it to the client? As we just saw, our template is just a function that returns an evaluated string. All we really need to do is serve up the "source" function to the client. Lets take a look at how we can do that:
var viewString = 'var __views = {};';
viewString += '__views["ourCoolView"] = ' + template.source;
What we're doing here is programmatically building the source of a Javascript file that we're going to serve up to the client. If we view the whole thing as if we wrote it by hand, it would look something like this:
var __views = {};
__views["ourCoolView"] = function(obj) {\nobj || (obj = {});\nvar __t, __p = \'\', __e = _.escape, __j = Array.prototype.join;\nfunction print() { __p += __j.call(arguments, \'\') }\nwith (obj) {\n__p += \'<div class="my-super-awesome-div">\\n\\t\' +\n((__t = ( mySuperAwesomeVariable )) == null ? \'\' : __t) +\n\'\\n</div>\\n<ul class="things-that-are-awesome">\\n\\t\';\n _.each(thingsThatAreAwesome, function(thing){ ;\n__p += \'\\n\\t\\t<li>\' +\n((__t = ( thing )) == null ? \'\' : __t) +\n\'</li> \\t\\n\\t\';\n }); ;\n__p += \'\\n</ul>\\n\';\n\n}\nreturn __p\n};
When sent to the client, the variable __views
will be placed in the global scope (window.__views
). To evaluate our template to get the string output like we did before we would do:
var renderedTemplate = window.__views['ourCoolView']({
mySuperAwesomeVariable: 'Im super awesome',
thingsThatAreAwesome: ['This is awsome', 'So is this', 'This is too!']
});
$('.someDomElement').html(renderedTemplate);
Thats pretty much it! In the next week or two, I will be following up this post with a post on how to extend this system even further by introducing a small library I built called node-partials that introduces inter-file partials as well as compiling multiple files and partials together.