Saturday, November 15, 2008

JSP-like templates in JavaScript

A template is a piece of (usually, but not necessarily, HTML) code that contains symbolic references to data variables. You can use a template, for example, when you need to create a table with a known, fixed layout, but you want to re-create its content with different data elements.

This post is focused on templating on the client side, where we have JavaScript code that needs to expand a template using a JavaScript data structure.There are several templating techniques out there that address this situation. For my part, I needed a template mechanism that looks like JSP, except that the code fragments must be written in JavaScript, and for one reason or another, none of these fit my needs.

In particular, I wanted a templating mechanism with the following features:

  • Templates that support arbitrary JavaScript data structures (nested maps and arrays) as data models.

  • Ability to create a pre-compiled template that I could then reuse many times, with different data models.

  • JSP-like syntax, where variables can be referenced via notation like ${variableName.itemName[index]}, and scriptlets can contain arbitrary JavaScript code. I also wanted JSP-style comments (delimited by <%-- and --%>) embeddable in the scriptlets.


To create a template, I wanted code such as this:
  
var tmpl = new Template (aTemplateString);

And then, later, to process a template with a data model:
   
var expandedResult = tmpl.process (dataModel);

For example:

// Sample template string:
var string = "\
<h2>${name}</h2>\
<ul>\
<% for (var i = 0; i < positions.length; i++) { %>\
<li>${positions[i].title}, ${positions[i].company}, ${positions[i].duration}</li>\
<% } %>\
</ul>\
";
var template = new Template (string);

// Data model that can be used with this template:
var model = {
name: "Jim Smith",
positions: [
{ title: "Programmer", company: "ACME Corp", duration: "1991-1994" },
{ title: "Analyst", company: "Fluor Corp", duration: "1995-2001" }
]
}

// Expand the template using the model, and obtain a string:
var expandedResult = template.process (model);


The technique I ended up with uses a regular expression to parse the given template into its code and data segments, and generates a JavaScript function via an eval. This function is then remembered, and called whenever a template must be expanded. Here is the code:


var Template = function (aString, templateName) {
var templateString = aString;
var dataParts = [], start = 0;
var codeParts = ["var _process = function (model) { \nvar result = [];\nwith (model){\n"];
var re = /<%([\s\S]*?)%>|\$\{(.*?[^\\])\}/g ;
aString.replace (re, function (fullMatch, g1, g2, index) {
dataParts.push (aString.substring (start, index));
if (g1) {
codeParts.push ("result.push (dataParts[" + (dataParts.length-1) + "]);\n");
if (g1.substring (0,2) != "--" && g1.substring(g1.length-2,g1.length) != "--") {
// It's not a comment, it's a code fragment
codeParts.push (g1 + "\n");
}
start = index + g1.length + 4;
} else { // g2 matched
codeParts.push ("result.push (dataParts[" + (dataParts.length-1) + "]);\n");
codeParts.push ("result.push (" + g2 + ");\n");
start = index + g2.length + 3;
}
});
dataParts.push (aString.substring (start));
codeParts.push ("result.push (dataParts[" + (dataParts.length-1) + "]);\n}\nreturn result.join ('');\n}");
var codeStr = codeParts.join ("");
try {
eval (codeStr);
} catch (e) {
alert ("Template '" + templateName + "' expansion error:\n" + e.message);
}
this.process = _process;
};


You can download the code for your enjoyment. Comments welcome!

No comments: