Templating in Javascript with Handlebars
When building a web application with Python on the server side we used the Bottle template system to generate HTML pages. This formed the View part of the MVC architecture and meant that we could separate out the design of the page from the logic of the application and the database access layer. The template is HTML code with bits of Python used only to insert data into the page. This provides a good alternative to generating HTML code as part of the Python application; this can lead to messy code and makes it hard to see where each part of the page comes from.
As we move towards front-end based applications that construct HTML pages in the browser, we started by reverting to the model of generating HTML as part of the output of our code. For example in this code from an earlier chapter:
let text = "";
let result = JSON.parse(this.responseText);
for(let i=0; i<result.likes.length; i++) {
text += "<li>" + result.likes[i] + "</li>";
}
document.getElementById('things').innerHTML = text;
The code is again messy and makes it harder to see how the final HTML fragment comes together. It is clearly desirable to use some kind of templating engine again from Javascript. However, the requirements now are different; when we are constructing pages on the server side we construct the entire HTML page before delivering it to the client. In Javascript we are usually updating a part of the page with new data to display in response to interactions. So, rather than needing a templating system for whole pages we want something we can use to create fragments of pages.
Handlebars
Handlebars is a templating system for Javascript. It is
an extended implementation of the Moustache family of
templating systems which are available in many langauges (there is an official Javascript
implementation of Moustache but Handlebars has
a few advantages). The name comes from
the use of curly braces {{ }}
to delimit variables to be inserted into
the template. This will be familiar from our use of the Bottle templating engine, however
whereas Bottle templates allowed you to include Python code in the template, Handlebars
is deliberately more limited in what templates are able to do. We are limited
to basic conditionals (if statements) and loops over collections.
Here is an example Handlebars template:
<div class=entry>
<h1>{{title}}</h1>
<p>
{{body}}
</p>
</div>
The placeholders reference values passed in to the template from Javascript code as an object. The placeholder names used in the template are properties of the object.
A template is a string value in Javascript so a simple example of using a template would just use a string constant in the code. Since we often want to include newlines in our template, it is easiest to use the backtick string delimiters. Assuming that the Handlebars library has been loaded, the following code fragment will create an HTML string that could then be inserted into the page:
const source = `<div class=entry>
<h1>{{title}}</h1>
<p>
{{body}}
</p>
</div>`
const template = Handlebars.compile(source)
const context = {
title: 'Hello World',
body: 'This is the text.'
}
const content = template(context)
The first step is to compile the template source string. This generates a
function that can be applied to a context (an object) containing values for
the different placeholders. The result in the content
variable will be:
<div class=entry>
<h1>Hello World</h1>
<p>
This is the text.
</p>
</div>
Writing View Functions
The Model-View-Controller structure that we talked about in the context of Python applications is equally relevant to Javascript front-end applications. It is a useful way to think about the structure of an application and separate out the application logic, data access and interface generation componenets. Handlebars can be used to write the View component by encapsulating the code above into a view function and associating this with a template.
For example, if we have a data object representing a person:
const bob = {
first_name: 'Bob',
last_name: 'Bobalooba',
dob: '1994-03-10',
city: 'Sydney'
};
we can write a view function to display objects of this type. First we define a template that will generate the HTML we want to display:
const personTemplate = Handlebars.compile(`
<div class="person">
<dl>
<dt>Name</dt> <dd>{{first_name}} {{last_name}}</dd>
<dt>Date of Birth</dt><dd>{{dob}}</dd>
<dt>City</dt><dd>{{city}}</dd>
</dl>
</div>
`)
Then we write a function to realise the view passing in a selector that will be used to select the HTML element to insert the result into and the person object to be displayed:
function personView(id, person) {
const content = personTemplate(person);
document.getElementById(id).innerHTML = content;
}
This function can then be called from our Controller code to include the
display of a particular person's data in the page in the element with id userinfo
:
const user = get_person(id) /* assume some kind of model function */
personView('userinfo', user)
More on Handlebars Templates
You can learn more about the capabilities of Handlebars from the documentation but there are a few further features worth pointing out here.
Looping
The first is a way to handle collections of things - eg. a list of people. In a Bottle
template we'd write a for
loop to iterate over the list and create the right HTML
content for each element. In Handlebars we can do the same but it doesn't quite
look like a for loop.
As an example, consider an array of people objects:
const members = [
{
first_name: 'Bob',
last_name: 'Bobalooba',
dob: '1994-03-10',
city: 'Sydney'
},
{
first_name: 'Mary',
last_name: 'Contrary',
dob: '1994-02-10',
city: 'Sydney'
},
{
first_name: 'Jim',
last_name: 'James',
dob: '1983-12-11',
city: 'Melbourne'
}
]
To present this data we could use a template for a single person and call it
multiple times but Handlebars gives us an iteration construct that we can use.
The following template and view function illustrates this.
const peopleTemplate = Handlebars.compile(`
<div class="people">
{{#each array}}
<div class="person">
<dl>
<dt>Name</dt> <dd>{{first_name}} {{last_name}}</dd>
<dt>Date of Birth</dt><dd>{{dob}}</dd>
<dt>City</dt><dd>{{city}}</dd>
</dl>
</div>
{{/each}}
</div>
`)
function peopleView(id, peopleArray) {
const content = peopleTemplate({array: peopleArray});
document.getElementById(id).innerHTML = content;
}
Note that since the argument to the template
function needs to be an object I've
created an in-line object with one property array
to contain the array of people.
The template uses the #each
construct to iterate over the array.
I can then use this view function to insert content based on the members
global
variable into the element with id target
as follows:
peopleView('target', members)
Note that here there is no loop variable, we just refer to the properties of the
current object each time. You can refer to {{this}}
to get the current object and
insert it as a whole - for example if the input was a list of strings or numbers.
The each block also supports special variables @index
- the current loop index and
@first
and @last
which are booleans that are true on the first and last
iterations respectively.
Conditionals
There are two conditional blocks in Handlebars templates.
An #if
block can be
used to test a value but expressions are not allowed. Hence the main use is to
handle cases where data is missing:
const personTemplate = Handlebars.compile(`
<div class="person">
<dl>
<dt>Name</dt> <dd>{{first_name}} {{last_name}}</dd>
<dt>Date of Birth</dt><dd>{{dob}}</dd>
{{#if city}}
<dt>City</dt><dd>{{city}}</dd>
{{#else}}
<dt>City</dt><dd>No City Recorded</dd>
{{/if}}
</dl>
</div>
</script>`)
If you wanted to include a comparison test you'd need to do the test in your view function before passing it to the template. This reinforces the idea that templates are about presentation and all of the logic should go into the Javascript code. For example:
function personView(selector, person) {
/* set a property on person */
person.sydneyResident = person.city === 'Sydney'
const content = personTemplate(person);
document.getElementById(id).innerHTML = content;
}
Another conditional operator provides the opposite test with
#unless
:
const licenceWarningTemplate = Handlebars.compile(`
<div class="entry">
{{#unless license}}
<h3 class="warning">WARNING: This entry does not have a license!</h3>
{{/unless}}
</div>`)
This block will be rendered if there is no value for licence
or if the value is
'falsy' ([]
, ""
, null
or []
).
There are a few other block helpers in Handlebars but these are the main ones you will use.
Partials
A very useful feature is the ability to use one template inside another. In our people example we might want to write one template for a person and then re-use it in the list of people. This can be done with partials in Handlebars. Here's a simple example that defines a partial template for a person:
Handlebars.registerPartial('person', '<li>Name: {{name}}</li>')
const peopleTemplate = Handlebars.compile(`
<ul class="people">
{{#each people}}
{{> person}}
{{/each}}
</ul>
`)
The effect of this is as if the partial template is just pasted into the containing template - the variables it can refer to are those in the main template.