- Explain how to use Handlebars templates
- Describe Handlebars' built-in helpers (
if
andeach
) - Practice writing a template using Handlebars
- Write a custom Handlebars helper
Templates are a powerful way to dynamically create HTML content without having to hand-build and concatenate strings of HTML and data values within your JavaScript code.
A good template engine will allow you to create HTML templates and insert placeholders for dynamic data in the least obtrusive way possible, separating the definition of the template from the consumption of it so that our code stays readable, maintainable, and well-organized.
Today we're going to work with Handlebars, which is a powerful template engine. We'll see how to render simple templates and also learn how Handlebars makes tasks like working with collections of data easier.
Let's jump right in and create a Handlebars template and render it to
the page. Similar to some other template engines, we can define a
Handlebars template inside of a <script>
tag with a combination of
regular HTML and Handlebars-specific delimiters.
In our index.html
let's define a basic template to render a github
issue.
<!-- <body>... -->
<main id="main">
<a href="#" onclick="loadIssue();">Load Github Issue</a>
</main>
<script id="issue-template" type="text/x-handlebars-template">
<article>
<header><h3>Issue #{{number}} ({{state}})</h3></header>
<p>{{body}}</p>
<footer><a href="{{url}}">created {{created_at}}</a></footer>
</article>
</script>
We have a script
tag with an id
and a type
of
text/x-handlebars-template
. We need that type to be set for Handlebars
to know what to do with this template, and we need to give it an id
so
that we can easily select it and get its innerHTML
when we render it.
Inside the script
tag, we have some pretty basic HTML for the most
part, and then some variables marked by double curly brace delimiters,
like {{status}}
. What we're saying there is that, when we render this
template, we're going to have a variable called status
that we want to
go in that spot.
//Flat-fact: Handlebars uses the curly brace as a delimiter because if you turn it sideways it looks like a mustache, and Handlebars.js is an extension of the Mustache template language. One need not grow a mustache to be an effective user of Handlebars, but if you're also hoping to be a legendary relief pitcher, it couldn't hurt.
Finally, we included a simple link to fire a loadIssue
function that
we'll use to render our template, so let's get into our index.js
and
create that function.
//index.js
function loadIssue() {
var issue = {
state: "closed",
number: 5,
created_at: "2016-03-31 16:23:13 UTC",
body: "Instructions say GET /team and POST /newteam. Rspec wants GET/newteam and POST/team."
}
var template = Handlebars.compile(document.getElementById("issue-template").innerHTML);
var result = template(issue);
document.getElementsByTagName("main")[0].innerHTML += result;
}
We started by creating a simple issue
object to hold our Github issue
data. This will be used as the context
, or argument of our Handlebars
template. Note that the property names match the names of the variables
in our template. This is crucial. If your context object properties
don't match the template variables, you'll get unexpected results.
Next, we make a call to Handlebars.compile
with the innerHTML
of our
template. This will "compile" the markup and delimiters as part of a
function that we can call with our context to output a rendered HTML
string, which is what's happening in var result = template(issue)
. If
we had another issue
object with different data, we could call
template()
again and get the same template rendered with the new data.
Finally, we're taking our rendered template and putting it back into our
page so we can see it. If you load up your index.html
page, you should
be able to see this in action!
Already, Handlebars is giving us a powerful way to render templates with our data, but it offers a few more things that make it stand out even more.
Handlebars offers several built-in functions, or helpers, that provide
easy ways to work with code inside our templates. We'll explore two of
these helpers: if
and each
.
In the example above, we rendered a single issue to the page, but what
if we have a whole collection of issues? Take a look inside issues.js
and you'll see an array of about a thousand Github issues just waiting
to be displayed on our issue-tracking page.
Now we could take this data and render it using a for
loop. We already
know that our template
function can be reused with a different context
object, so, if we modify index.js
a little, we can render all our
issues.
function loadIssue() {
var template = Handlebars.compile(document.getElementById("issue-template").innerHTML);
for(var i=0;i<issues.length;i++) {
var result = template(issues[i]);
document.getElementsByTagName("main")[0].innerHTML += result;
}
}
Here, we're still compiling template
the one time, then using it
inside our loop to output to the DOM. If we reload index.html, it works
(albeit a little slowly, perhaps).
There's a couple of problems with this approach. The first is that it
was very slow. We're reallocating this result
variable every time,
then querying the DOM every time and adding to it every time through the
loop.
The second problem is that we're doing the heavy-lifting of iterating
over the collection ourselves, instead of letting our template do it for
us. With just a few modifications, we can take advantage of the built-in
each
helper and let Handlebars worry about building out the
collection.
First, let's alter the loadIssue
function.
function loadIssues() {
var template = Handlebars.compile(document.getElementById("issue-template").innerHTML);
var result = template(issues);
document.getElementsByTagName("main")[0].innerHTML += result;
}
Here we're just passing the whole issues
array to template
and
rendering it all at once. Also note that we changed the function name to
loadIssues
. Since we're dealing with a collection of issues, it's a
good practice to make the name plural. Don't forget to change it in
index.html
.
Now let's alter our template to handle the collection.
<main id="main">
<a href="#" onclick="loadIssues();">Load Github Issue</a>
</main>
<script id="issue-template" type="text/x-handlebars-template">
{{#each this}}
<article>
<header><h3>Issue #{{number}} ({{state}})</h3></header>
<p>{{body}}</p>
<footer><a href="{{url}}">created {{created_at}}</a></footer>
</article>
{{/each}}
</script>
Did you change loadIssues
?
Note the two new things in our template: {{#each this}}
and
{{/each}}
. The #
and /
are needed because each
is a
block-level helper, so we have to start (#
) and end (/
) the block
similar to open/close braces for a JavaScript block or open/close tags
for HTML.
The each
helper needs something to iterate on. In this case, we'll use
this
, because this
is set to whatever the context object is when we
call our template function. Here, it's an array of objects, so we're
saying "this
is an array, apply template to each
item".
Nothing else to do here, since each item of the array still has the
properties we used before. Let's reload index.html
and see what
happens.
Much faster, right? Taking advantage of the built-in each
helper not
only sped things up, but helped keep our code cleaner and easier to
read.
Handlebars gives us another block helper for making decisions in our
template - the if
helper.
Looking at our issues.js
, it seems like some of our issues have
comments, and some do not. Let's conditionally display the number of
comments if there are any. We'll use the if
block helper.
<script id="issue-template" type="text/x-handlebars-template">
{{#each this}}
<article>
<header><h3>Issue #{{number}} ({{state}})</h3></header>
<p>{{body}}</p>
{{#if comments_count}}
<p>{{comments_count}} Comments</p>
{{else}}
<p>No Comments Yet</p>
{{/if}}
<footer><a href="{{url}}">created {{created_at}}</a></footer>
</article>
{{/each}}
</script>
We added a block helper for {{#if comments_count}}
. You'll notice this
isn't a full expression like you might use in a regular if
statement.
You can only give if
a value from the context object, and if that
value evaluates falsy (false
, undefined
, null
, 0
, ""
, or
[]
) then Handlebars won't render the block. In this case, since
comments_count
is an integer, we can rely on a zero-comments object
not rendering this block.
The next thing to notice is that we provided an {{else}}
to default to
if there are no comments. We don't use the #
for else
, it only goes
at the very beginning of the block.
For simple manipulation of the template based on truthy/falsy values,
the if
helper is your friend!
We've seen some built-in Handlebars helpers that give us easy ways to iterate collections and make simple display decisions, but what if we want to encapsulate some more complex display logic into a function to use in our template?
Handlebars allows us to create and register our own custom helpers for exactly this purpose.
Let's say we want to apply a different style to the issue body if the issue is closed versus open.
We could probably use that built-in if
helper to draw out two
different template styles, the more we litter our templates with
decision-making logic, the less decoupled our presentation logic is from our
data.
Also they just get ugly with a bunch of if
statements all over the
place.
Instead, we want to encapsulate that logic into a function that will allow us to use a helper in our template that will do the work for us.
We can use Handlebars.registerHelper
to register our own helper for
use in our template. Let's add one to our index.js
above our
loadIssues
function.
Handlebars.registerHelper('comment_body', function() {
if(this.state === "closed") {
return new Handlebars.SafeString(this.body)
} else {
return new Handlebars.SafeString("<strong>" + this.body + "</strong>")
}
})
function loadIssues() {
//...
}
We are telling Handlebars to register a helper called comment_body
for
our use. Inside, we have access to the same this
as the template that
calls the helper, in this case, it will be our issue context object.
Top-tip: You can also register a helper and pass a value to the function, if you aren't 100% sure you can safely use the context object.
We use Handlebars.SafeString
to return a string that contains HTML so
that HTMl won't get escaped.
Then it's a simple matter of just wrapping this.body
inside strong
tags for unclosed issues, and we've created our first Handlebars helper!
Now to use it in our template:
<script id="issue-template" type="text/x-handlebars-template">
{{#each this}}
<article>
<header><h3>Issue #{{number}} ({{state}})</h3></header>
<p>{{comment_body}}</p>
{{#if comments_count}}
<p>{{comments_count}} Comments</p>
{{else}}
<p>No Comments Yet</p>
{{/if}}
<footer><a href="{{url}}">created {{created_at}}</a></footer>
</article>
{{/each}}
</script>
Here we've simply replaced {{body}}
with our helper, {{comment_body}}
. Now if we reload our index.html
and click our link, we should see closed issues with regular font and open issues with bold!
We've learned how to use Handlebars templates to render object, and
collections of objects with the built-in each
helper. We've made decisions on how to display things
using the built-in if
helper, and implemented and registered our own
custom helper to move logic out of the template.