2012-06-07

A closer look at Underscore templates

Underscore.js is a highly useful complement to JavaScript’s rather sparse standard library. In a pinch, Underscore gives you simple templating, too. This post explains how it works and gives tips.

Preliminaries

All interactions in this post are done in the Node.js REPL, which has the advantage of Underscore.js being easy to install [1]:
    npm install underscore
The only drawback is that you can’t use the name _ in the REPL. You need to use a name such as u:
    > var u = require("underscore");
In modules, you can use the name _ and in the following examples, we pretend that that is possible in the REPL, too.

Working with Underscore templates

The template function has the following signature:
    _.template(templateString, data?, settings?) 
templateString holds the template. settings allow you to override the global settings. If you omit data, you compile a template that has to be applied to data as a function:
    > var t1 = _.template("Hello <%=user%>!");  // compile
    > t1({ user: "<Jane>" })  // insert data
    'Hello <Jane>!'
First, you compile a template function t1, then you use that function to produce a string. You can also specify the parameter data and directly insert it into the template, without an extra compilation step:
    > _.template("Hello <%=user%>!", { user: "<Jane>" })
    'Hello <Jane>!'

Inserting data

Data can be inserted dynamically in three ways:
  • <%=interpolate%>
    Insert the result of an expression. The properties of the data object are all available as variables (see property user, above). No escaping happens, values are inserted verbatim.
  • <%-escape%>
    Insert the result of an expression, but escape the following characters via _.escape():
        & < > " ' /
    
    Example:
        > _.template("Hello <%-user%>!", { user: "<Jane>" })
        'Hello &lt;Jane&gt;!'
    
  • <%evaluate%>
    Evaluate the given code. This allows you to do loops and conditions (see next section).

Loops and conditions

The escape directive lets you insert arbitrary code. In the following template, we iterate over an array of names that is stored in property users:
    var t2 = _.template(
        "Users: <%_.forEach(users, function (u) {%>"
        + "<%=u%>, "
        + "<%})%>"
    );
t2 is used as follows.
    > t2({ users: [ "Huey", "Dewey", "Louie" ]})
    'Users: Huey, Dewey, Louie, '
By referring to the index of the current element, you can avoid the trailing comma:
    var t2 = _.template(
        "Users: <%_.forEach(users, function (u,i) {%>"
        + "<%if (i>0) {%>, <%}%>"
        + "<%=u%>"
        + "<%})%>"
    );
Interaction:
    > t2({ users: [ "Huey", "Dewey", "Louie" ]})
    'Users: Huey, Dewey, Louie'

Printing content

You can use the print function to imperatively insert content. For example, the previous example could be rewritten as follows:
    var t2 = _.template(
        "Users: <%_.forEach(users, function (u,i) {%>"
        + "<%if (i>0) { print(', ') }%>"
        + "<%=u%>"
        + "<%})%>"
    );

Referring to the data object

You can also refer to the properties of the data object via that object, instead of accessing them as variables. Example: The following two expressions are equivalent.
    _.template("Hello <%=user%>!", { user: "<Jane>" })
    _.template("Hello <%=obj.user%>!", { user: "<Jane>" })
Using obj makes it easier to check whether a property exists. Compare – the following two templates are equivalent.
    <%if (typeof title !== 'undefined') {%>Title: <%=title%><%}%>
    <%if (obj.title) {%>Title: <%=title%><%}%>
The variable holding the data object is obj by default, but can be configured. Below, we use the name data, instead.
    _.template("<%if (data.title) {%>Title: <%=title%><%}%>", null,
                { variable: "data" });
If you specify a variable name in this manner, the properties of data won’t be available as variables. That has the advantage that Underscore won’t have to use a with statement (see Sect. 3) and the template function will be faster.

Passing meta-data to a template

Sometimes, you want to hand meta-data to the template that is independent of the data to be displayed. Example: You iterate over objects, apply a template to each one and want to tell that template when to show a separator. There is no direct way of doing this, but you can extend the data with the meta-data:
    var tmpl = _.template("...");
    _.forEach(objects, function (obj, index) {
        tmpl(_.extend({ _showSeparator: index > 0 }, obj));
    });

Changing the syntax

The settings (global or per-template) allow you to change the syntax for inserting data. For example, you can use Mustache-style curly braces instead of angle brackets:
    _.templateSettings = {
        interpolate : /\{\{(.+?)\}\}/g
    };
Interaction:
    >  _.template("Hello {{user}}!", { user: "<Jane>" })
    'Hello <Jane>!'
You can change the syntax of each kind of insertion. Note, though, that syntax will be handled in the order escape, interpolate, evaluate. Hence, you cannot use more specific syntax for a “later” kind. For example, additionally introducing {{!...}} for evaluation would not work, because it would be preempted by interpolation.

Pre-compilation

You have the option to compile a template once, in advance, instead of doing so many times in the clients. Each compiled template function has the property source which holds the source of that function. The following is a fragment of server-side template that is used to produce a web page. The result could be stored or cached somewhere.
    ...
    <script>
        var clientTmpl = <%= _.template(clientTmplDef).source %>;
    </script>
    ...
One of the parameters of the above server-side template is the property clientTmplDef which holds the definition of a client-side template.

The internals

Compiling a template works as follows: The given template string is transformed to JavaScript code which is then evaluated into a function. For example:
    _.template("templateString")
The above template is compiled to the function
    function (obj) {
        var __p = [];
        // (Some code omitted here)
        with (obj || {}) {
            __p.push('templateString');
        }
        return __p.join('');
    }
Let’s look at a more complex example that shows you how insertion works:
    _.template("a <%-esc%> b <%=inter%> c <%eval%> d");
Compiled into a function:
    function (obj) {
        var __p = [];
        function print() {
            __p.push.apply(__p,arguments);
        }
        with (obj || {}) {
            __p.push('a ', _.escape(esc), ' b ', inter, ' c ');
            eval;
            __p.push(' d');
        }
        return __p.join('');
    }

Style

Underscore’s templates are very simple. Their disadvantage is that they always look a bit ugly. Their advantage is that they are dead-simple to understand once you’ve got the basics figured out. In other templating languages, you have to learn the syntax for loops etc. With Underscore templates, you simply use JavaScript syntax. That also means that you should embrace this rather imperative templating style and not try to make it declarative. If you want something more declarative, take a look at alternatives such as Mustache. For debugging, the aforementioned property source is very useful.

Conclusion

This blog post explained how Underscore’s templates work. In many projects, Underscore is already there, which means that you don’t need a more sophisticated templating engine if your needs are simple.

Related

  1. Trying out Underscore on Node.js

No comments: