Intro to jazz templates for node
I wrote the initial implementation of the jazz template engine for nodejs about a year ago for one of Shine’s clients (Sensis). Back then, template engines for node were somewhat lacking. An analysis by one of our client’s devs found json-template to be a little too restrictive, while Mu (mustache for nodejs) would inexplicably result in segfaults. Not to say these aren’t now fine projects, but we needed a way forward. It is still in use at Sensis to this day and, largely thanks to its small feature set, has required minimal maintenance.
Despite the project being a year old and now reasonably stable, I’ve neglected to write a proper tutorial for it yet. Let’s fix that.
Overview
Jazz has a fairly traditional interface as far as template engines go. You bind values to template variables, call a render function with the name of your template, and dump the result to the browser.
The template language itself is simple with few surprises, except perhaps where the surprises are warranted by the event-driven nature of node.
Installing jazz with npm
Cliff Subagio (another Shiner) kindly spent the time to integrate jazz with the npm package manager for node. So if you have npm, installing jazz is really as simple as running the following from the command line:
$ npm install jazz
Rendering a jazz template
Once installed, you can try jazz with just a few lines of code:
var jazz = require("jazz");
var sys = require("sys");
var template = jazz.compile("Hello, World");
template.eval({}, function(data) { sys.puts(data); });
// output: Hello, World
Calling jazz.compile(…) compiles your template code to a JavaScript Function object, which is in turn executed by template.eval(…). This is for performance reasons: you can compile your templates once at application startup & reuse them with different sets of template variables.
Rendering Template Variables
var jazz = require("jazz");
var sys = require("sys");
var template = jazz.compile("Hello, {name}");
template.eval({name: "You"}, function(data) { sys.puts(data); });
// output: Hello, You
As you can see by the template code we pass to jazz.compile(…), variables can be referenced by simply using curly brackets.
The first argument to template.eval(…) is an object describing your global namespace. Everything outside the core jazz syntax must be supplied using this object, including any helper methods or input data.
The second argument to template.eval(…) is a callback that is executed when the template has finished rendering. In this case we’re simply printing the template output to the screen.
You can use similar syntax to access object attributes too:
var jazz = require("jazz");
var sys = require("sys");
var template = jazz.compile("Hello, {person.name}");
template.eval({person: {name: "You"}}, function(data) { sys.puts(data); });
// output: Hello, You
Calling Helper Functions
Calling functions from a jazz template is very similar to rendering variables. Jazz supports two type of function call: asynchronous and synchronous.
Jazz will not wait for asynchronous function calls to return during template rendering, instead continuing with execution until the result of the async call returns. Due to their asynchronous nature, async calls cannot be used as expressions (e.g. as the test within an “if” statement).
On the JavaScript side, an asynchronous helper function must take a callback function as the final argument. This callback must be invoked by the asynchronous helper function upon completion, with the output value passed as its only argument.
var jazz = require("jazz");
var sys = require("sys");
function noop(value, cb) {
cb(value);
}
var template = jazz.compile("Hello, {getWorld()}. "
"And {noop('thanks for using jazz ')}.");
template.eval({
getWorld: function(cb) { cb("World"); },
noop: noop
}, function(data) { sys.puts(data); });
// output: Hello, World. And thanks for using jazz .
Synchronous calls on the other hand can be used as expressions, but you must take care that the call does not block the execution thread lest you suffer a performance hit.
Synchronous functions do not need the extra callback parameter used by asynchronous functions.
var jazz = require("jazz");
var sys = require("sys");
var template = jazz.compile("Hello, {@name()}")
template.eval({name: function() { return "You"; }}, function (data) { sys.puts(data); });
if/elif/else
Jazz supports conditionals, with simple expressions and comparisons available within tests.
var jazz = require("jazz");
var sys = require("sys");
var template = jazz.compile("{if active}You are lazy!{else}You are not lazy!{end}");
template.eval({active: false}, function(data) { sys.puts(data); });
template.eval({active: true}, function(data) { sys.puts(data); });
// output:
// You are not lazy!
// You are lazy!
else/if is provided with the elif keyword:
var jazz = require("jazz");
var sys = require("sys");
var template = jazz.compile("{if a}A{elif b}B{else}C{end}");
template.eval({a: true}, function(data) { sys.puts(data); });
template.eval({b: true}, function(data) { sys.puts(data); });
template.eval({}, function(data) { sys.puts(data); });
// output:
// A
// B
// C
Comparisons available include eq, neq and gt. (lt would also be trivial to add — taking patches!).
var jazz = require("jazz");
var sys = require("sys");
var template = jazz.compile("{if a eq 1}A{elif a eq 2}B{else}C{end}");
template.eval({a: 1}, function(data) { sys.puts(data); });
template.eval({a: 2}, function(data) { sys.puts(data); });
template.eval({a: 3}, function(data) { sys.puts(data); });
// output:
// A
// B
// C
Logical and and or operators are also available:
var jazz = require("jazz");
var sys = require("sys");
var template = jazz.compile("{if a and b}Both{elif a or b}One{else}None{end}");
template.eval({a: true, b: true}, function(data) { sys.puts(data); });
template.eval({a: true}, function(data) { sys.puts(data); });
template.eval({}, function(data) { sys.puts(data); });
// output:
// Both
// One
// None
Expressions can also be grouped using brackets:
var jazz = require("jazz");
var sys = require("sys");
var template = jazz.compile("{if (a and b) or c}true{else}false{end}");
template.eval({a: true, b: true, c: false}, function(data) { sys.puts(data); });
template.eval({a: true, b: false, c: true}, function(data) { sys.puts(data); });
template.eval({a: false, c: false}, function(data) { sys.puts(data); });
// output:
// true
// true
// false
foreach
Jazz also supports iteration over array-like objects:
var jazz = require("jazz");
var sys = require("sys");
var template = jazz.compile("{foreach item in items}Item {item}n{end}");
template.eval({items: [1, 2, 3]}, function(data) { sys.puts(data) });
// output:
// Item 1
// Item 2
// Item 3
You can also iterate over the attributes of an object:
var jazz = require("jazz");
var sys = require("sys");
var template = jazz.compile("{foreach item in items}{item.key}: {item.value}n{end}");
template.eval({items: {Apples: 1, Oranges: 3}}, function(data) { sys.puts(data) });
// output:
// Apples: 1
// Oranges: 3
Types
Jazz supports strings, integers and hashes at the syntax level. Support for arrays would be fairly trivial to implement, but is not currently present.
Summary
Looking at github, you would be forgiven for thinking that jazz was an inactive project. This couldn’t be further from the truth. Shine and Sensis are using jazz on a daily basis to build real software. Jazz simply meets all our needs for the time being & is very stable. Consequently, there are few commits because it meets our needs and works well. (Disclaimer: I found a bug while I was writing this article: synchronous method calls could not be used in an echo context — we’ve not had to do that before, but it should still be possible! The fix has since been committed.)
All that said, we’re always eager for useful patches and/or bug fixes. So if you like playing with compilers and have a bit of time to burn, feel free to shoot us a pull request on github with your improvements and/or new features!
Happy to address any questions in the comments for this post.