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>
As with a Bottle template, the placeholders reference values passed in to the template from Javascript code. Where in Python we passed a dictionary into the template, in Javascript we pass 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. 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:
let source = "<div class=entry><h1>{{title}}</h1><p>{{body}}</p></div>"
let template = Handlebars.compile(source)
let context = {
title: 'Hello World',
body: 'This is the text.'
}
let content = template(context)
Note that the template source string has been written in one line here because Javascript doesn't allow newlines in strings.
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>
Working with Templates
Handlebars provides the basic mechanism for using templates in Javascript but we need to develop a way of working with this as part of an application. Recall that the goal was to keep the Javascript code separate from the HTML templates. Keeping templates in strings does not achieve this but if we could store the template in the HTML file this would go some way to achieve this goal.
The key to storing templates in the HTML file is using the <script>
tag to
contain the template in an HTML page. The contents of a script tag
are not parsed by the HTML parser in the browser, instead they are treated
as a string but remain accessible from the DOM. So, we can include the
following fragment in the HTML page:
<script id='entry-template' type='text/x-handlebars-template'></script>
Note that the type attribute here (text/x-handlebars-template
) is not
meaningful to the HTML parser or the browser. It's recommended by the
Handlebars developers but is effectively just a made-up name. The main
thing is that it is not application/javascript
which would instruct
the browser to try to execute the block as Javascript code.
The Javascript code can now retrieve this from the page and compile it as a Handlebars template:
let source = document.getElementById('entry-template').textContent
let template = Handlebars.compile(source)
let context = {
title: 'Hello World',
body: 'This is the text.'
}
let content = template(context)
The final step would be to insert the generated HTML into the page. We can use jQuery to do this as well:
document.getElementById('target').innerHTML = content;
Since the task of finding the template and compiling it with Handlebars is one we will repeat a lot, it makes sense to write utility function to avoid repeating ourselves. Here's an implementation:
function get_template(id) {
return Handlebars.compile(document.getElementById(id).textContent);
}
We can then use this function to retrive the template:
let template = get_template('entry-template');
let context = {
title: 'Hello World',
body: 'This is the text.'
};
let content = template(context);
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:
let 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, a template is included in the HTML page:
<script id="person-template" type="text/x-handlebars-template"></script>
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) {
let template = get_template('person-template'));
let content = template(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
:
let 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 a list of people objects and a view function:
let 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'
}
]
function peopleView(id, people) {
let template = get_template('people-template');
let content = template({people: people});
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 people
to contain the list of people.
The template must now present all of the entries in the list. This can be done
in Handlebars using the #each
block helper. For example:
<script id="people-template" type="text/x-handlebars-template"></script>
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:
<script id="person-template" type="text/x-handlebars-template"></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) {
let template = get_template('people-template');
/* set a property on person */
person.sydneyResident = person.city === 'Sydney'
let content = template(person);
document.getElementById(id).innerHTML = content;
}
Another conditional operator provides the opposite test with #unless
:
<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.
It's also possible to extend Handlebars with your own helpers in a few different ways. You can define helper functions to generate particular display elements that can be re-used and also define new block helpers to achieve different effects.