Patrick Galbraith

Web developer - Adelaide, Australia

Simple Template Engine 1

An exploration of frontend template engines

There are a number of different JavaScript template engines. However in this post I just wanted to explore how you would go about evolving from a simplistic approach into something more complex.

Faux-Mustache

So the simplest approach to converting the template and data into the output we need is to just use String.prototype.replace(). Note I am using mustache/handlebars syntax for the template syntax in this section. None of the template variables escape html so don’t use in production with unsafe data.

var tpl    = "<p>Is this a template? {{test}}.</p>";
var output = tpl.replace('{{test}}', 'Yes it is!');

However this will only work if we have a single {{test}} template variable in our template because String.prototype.replace() only replaces the first item. You can use the third flags parameter to do a global replace using a string input but it is non-standard. So we have to use a regex instead.

var tpl    = "<p>Is this a template? {{test}}.</p>";
var re     = /\{\{test\}\}/gmi;
var output = tpl.replace(re, 'Yes it is!');

This works and for really simple applications you could easily just use this method instead of a template engine. But the data that we want to replace is hard-coded which means we can’t update it very easily and we need a new replace call for each additional variable. So what we want is to be able to pass in any data as properties of an object like this.

var Template = function(tpl, data){
    return tpl.replace(/\{\{(.*?)\}\}/gmi, function(match, p1) {
        var prop = p1.trim();
        return data.hasOwnProperty(prop) ? data[prop] : '';
    });
};
var tpl    = "<p>Is this a template? {{test}} {{test2}}.</p>";
var data   = {
    test: 'Yes',
    test2: 'it is!'
};
var output = Template(tpl, data);

This is better but it still has some issues. Most notably we have no way to access descendant properties. It would be great to be able to do this {{user.name}}. So lets make it happen.

var Template = function(tpl, data){
    return tpl.replace(/\{\{(.*?)\}\}/gmi, function(match, p1) {
        var prop = p1.trim().split('.'),
            obj  = data;
        while(prop.length && typeof(obj) != 'undefined')
            obj = obj[prop.shift()]
        return typeof(obj) != 'undefined' ? obj : '';
    });
};
var tpl    = "<p>Hi {{user.name}}! You are from {{user.location}}.</p>";
var data   = {
    user: {
        name: 'Pat',
        location: 'Australia'
    }
};
var output = Template(tpl, data);

Great so that works pretty well and it’s only nine lines of code to boot. However there are still two big features missing and they are template compilation and the ability to work with arrays.

Compilation

Template compilation allows us to avoid re-executing the regex and looping through every property every time we use the template. So lets look at how we could update the previous example to return a compiled function.

var Template = function(tpl){
    var cursor = 0,
        code = "var r=[];\n", 
        re = /\{\{(.*?)\}\}/gmi;
    
    var add = function(line, tag) {
        if(tag) {
            code += "r.push(data."+line+");\n";
        } else {
            code += line != '' ? 'r.push("' + line.replace(/"/g, '"') + "\");\n" : '';
        }
    };
    
    while(match = re.exec(tpl)) {
        add(tpl.slice(cursor, match.index));
        add(match[1], true);
        cursor = match.index + match[0].length;
    }
    
    add(tpl.substr(cursor, tpl.length - cursor));
    code += 'return r.join("");';

    try {
        result = new Function('data', code); 
    } catch(err) {
        console.error("'" + err.message + "'", " in \n\nCode:\n", code, "\n");
    }

    return result;
};
var tpl    = "<p>Hi {{user.name}}! You are from {{user.location}}.</p>";
var data   = {
    user: {
        name: 'Pat',
        location: 'Australia'
    }
};
var compiledTemplate = Template(tpl);
var output = compiledTemplate(data);

So now we have a function we can call to parse the template with different data whenever we need to. Note the above is based on the blog post by John Resig (jQuery) here and the template engine from Krasimir Tsonev here.

How it works is that it generates a new Function() that looks like this:

function(data) {
    var r=[];
        
    r.push("<p>Hi ");
    r.push(data.user.name);
    r.push("! You are from ");
    r.push(data.user.location);
    r.push(".</p>");

    return r.join(""); 
}

Lets update it to work with arrays. Ideally it would be great to be able to use a template like this {{#items}}{{.}}{{/items}} which loops through an array of items just like you would when using mustache.

var Template = function(tpl, data){
    var cursor = 0,
        code = "var r=[];\n", 
        re = /\{\{(.*?)\}\}/gmi;
    var add = function(line, tag) {
        if(tag) {
            if(line.charAt(0) === '#') {
                code += "for(var i=0, len=data."+line.slice(1)+".length; i<len; i++){\n";
                code += "var el=data."+line.slice(1)+"[i];\n";
            } else if (line.charAt(0) === '/') {
                code += "};\n";
            } else {
                code += line === '.' ? "r.push(el || data);\n" : "r.push(data."+line+");\n";
            }
        } else {
            code += line != '' ? 'r.push("' + line.replace(/"/g, '"') + "\");\n" : '';
        }
    };
  
    while(match = re.exec(tpl)) {
        add(tpl.slice(cursor, match.index));
        add(match[1], true);
        cursor = match.index + match[0].length;
    }
    
    add(tpl.substr(cursor, tpl.length - cursor));
    code += 'return r.join("");';
    try { result = new Function('data', code); }
    catch(err) { console.error("'" + err.message + "'", " in \n\nCode:\n", code, "\n"); }
    return result;
};
var tpl    = "<p>Hi {{user.name}}! You like:</p>"+
             "<ul>{{#user.likes}}<li>{{.}}</li>{{/user.likes}}</ul>";
var data   = {
    user: {
        name: 'Pat',
        likes: ['Code', 'Boardgames', 'Cooking']
    }
};
var output = Template(tpl)(data);

So as you can see this is a little more complex. Of course it is still missing a lot of features but the point isn’t to create a fully featured template engine (plenty of those exist).

Unfortunately it isn’t possible to re-create all of the mustache features using this approach simply because of the ambiguity in the way mustache handles objects, arrays, and variables with the hash (#) prefix. The alternative is to use something like this http://jsfiddle.net/a9o9Lw0x/1/ however this will likely perform a lot worse since we need to dynamically handle the block (#) tags and at that point you may as well use a proper template engine.

Embedded Javascript (EJS)

Now that we have template compilation it is fairly trivial to make a Embedded Javascript (EJS) style template engine. Let’s have a look at how it is done.

var Template = function(tpl){
    var cursor = 0,
        code   = "with(data) { var r=[];\n", 
        re     = /<%(.*?)%>/gmi;
    
    var add = function(line, tag) {
        if(tag) {
            code += line.charAt(0) === '=' ? "r.push("+line.slice(1).trim()+");\n" : line+"\n";
        } else {
            code += line != '' ? 'r.push("' + line.replace(/"/g, '"') + "\");\n" : '';
        }
    };
    
    while(match = re.exec(tpl)) {
        add(tpl.slice(cursor, match.index));
        add(match[1], true);
        cursor = match.index + match[0].length;
    }
    
    add(tpl.substr(cursor, tpl.length - cursor));
    code += 'return r.join(""); }';
    try { result = new Function('data', code); }
    catch(err) { console.error("'" + err.message + "'", " in \n\nCode:\n", code, "\n"); }
    return result;
};
var tpl    = "<p>Hi <%= user.name %>! You are from <%= user.location %>.</p>"+
             "<ul>"+
             "<% for(var i=0; i < user.likes.length; i++) { %>"+
                 "<li><%= user.likes[i] %></li>"+
             "<% } %>"+
             "</ul>";
var data   = {
    user: {
        name: 'Pat',
        location: 'Australia',
        likes: ['Code', 'Boardgames', 'Cooking']
    }
};
var output = Template(tpl)(data);

Well that’s it for the rundown on how to create a simple template engine.

COMMENTS

Leave a reply

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>