If your site's data regularly changes, then you might want to take a look at Handlebars. Handlebars is a template processor that dynamically generates your HTML page, saving you time from performing manual updates. In this tutorial, I'll introduce you to Handlebars, and teach you how to create a basic template for your site.
Site Template
There are two primary reasons why you'd want to make a template for your site. First of all, building a template encourages you to separate the logic-based code from the actual view, helping you adhere to a View/Controller pattern. Secondly, templates keep your code clean and maintainable, which, in turn, makes the process of updating your site a breeze. You don't create a site with Handlebars. Instead, you create guidelines and structures that dictate how the site should look without focusing on a page's data. Let's cover some of the basics.
The Basics
Handlebars generates your HTML by taking a JSON structure and running it through a template. These templates are written mostly in regular HTML, and are peppered with placeholders that allow you to inject data, as needed. For example, the following template greets the user when they log in:
<h1>Welcome back, {{name}}</h1>
The {{name}}
attribute is where the user's name will be injected into the page. This placeholder corresponds with a property in the data's JSON structure. This is the most basic example possible, but you will soon see that everything else basically boils down to this simple concept. Let's move on to handling arrays.
Arrays
Handlebars comes with some built-in helpers to assist you in working with more complex data. One of these helpers is the each
helper. This helper iterates through an array and allows you to create dynamic HTML, per array element. For example, the following template displays an array's data that contains a list of the local concerts playing in my area:
<table> <tr> <th>Local Concerts</th> </tr> {{#each Concerts}} <tr> <td>{{this}}</td> </tr> {{/each}} </table>
As you can see, this code is much cleaner than conventional code, such as using a loop in PHP or JavaScript to append HTML to a variable. Handlebars is not intrusive, and this is what makes Handlebars so accessible. You may also notice that we use the attribute name, this
, to retrieve the current array element in the each
loop.
This example is good for an array of simple values, but how do you handle more complex data? Well, you essentially do the same thing. For example, we're going to write a template for the following data:
[ { Name : "Band", Date : "Aug 14th, 2012", Albums : [ { Name : "Generic Name" }, { Name : "Something Else!!" } ] }, { Name : "Other Guys", Date : "Aug 22nd, 2012" Albums : [ { Name : "Album One" } ] } ]
We can easily display this information using the following template:
<table> <tr> <th>Band Name</th> <th>Date</th> <th>Album Name</th> </tr> {{#each Bands}} <tr> <td>{{Name}}</td> <td>{{Date}}</td> <td>{{Albums.0.Name}}</td> </tr> {{/each}} </table>
You can store your template in a
<script />
element and load it with JavaScript.
In Handlebars, you can even access nested properties, like in the example above (Albums.0.Name
), and of course, you could have used another each
loop to iterate over a band's albums. It's worth noting that besides the dot notation to access nested properties, you can also use "../" to access a parent's properties.
What if there aren't any bands playing? You certainly don't want an empty table, and Handlebars thankfully provides if
, else
and unless
helpers. The if
and else
statements work like most programming languages: if the object you pass is false
or falsey, then the else
statement executes. Otherwise, the if
statement executes. The unless
statement is pretty interesting; it's essentially an inverted if
statement. If the expression is true
, the unless
block will NOT run. So let's incorporate these helpers into our code:
{{#if Bands}} <table> <tr> <th>Band Name</th> <th>Date</th> <th>Album Name</th> </tr> {{#each Bands}} <tr> <td>{{Name}}</td> <td>{{Date}}</td> <td>{{Albums.0.Name}}</td> </tr> {{/each}} </table> {{else}} <h3>There are no concerts coming up.</h3> {{/if}}
Custom Helpers
Handlebars gives you the ability to create your own custom helper. Simply register your function into Handlebars, and any template you compile afterwards can access your helper. There are two kinds of helpers that you can make:
- Function helpers are basically regular functions that, once registered, can be called anywhere in your template. Handlebars writes the function's return value into the template.
-
Block helpers are similar in nature to the
if
,each
, etc. helpers. They allow you to change the context of what's inside.
Let me show you a quick example of each. First, I'll register a function helper with the following code:
Handlebars.registerHelper("Max", function(A, B){ return (A > B) ? A : B; });
The first argument passed to registerHelper()
is the name of my customer helper; I'll use this name in the template. The second argument is the function associated with this helper.
Using this helper in a template is extremely simple:
{{Max 12 45}}
This template uses the Max
helper, and passes the values 12 and 45 to the associated function. Handlebars function helpers support multiple parameters. You can directly insert numbers into the template itself, or you can use attributes from a JSON structure.
Now let's look at a custom block helper. Block helpers allow you to set the context before running the code contained within the block. For example, consider the following object:
{ Name: "Parent", Sub: { Name: "Child" } }
In order to display both names, you can write a block helper that runs the template once with the parent's context, and once with the child's context. Here is the helper:
Handlebars.registerHelper("BothNames", function(context, options){ return options.fn(context) + options.fn(context.Sub); });
And the template looks like this:
{{#BothNames this}} <h2>{{Name}}</h2> {{/BothName}}
The hash tag before the helper's name tells Handlebars that this is a block helper, and you close the block not unlike you would an HTML tag. The options.fn
function runs the section of template inside the block with whatever context you give it.
Now that we have the basics down, let's start creating a full demo.
Building a Site Template
You don't create a site with Handlebars.
The template we will build is for a recipe site. This will give you a good understanding of Handlebars, as it encompasses getting data from an API and passing it through a template.
Setting up a Handlebars project
We must first load our template script, but in order to do that, we need to create a new HTML file and include our Handlebars library:
<html> <head> <title>Handlebars Demo</title> <script type="text/javascript" src="Handlebars.js"></script> </head> <body> <script id="Handlebars-Template" type="text/x-handlebars-template"> </script> </body> </html>
For convenience, you can store your template in a <script />
element and load it with JavaScript. This is much cleaner than storing it directly in a JavaScript variable.
Now let's discuss how this app is going to work. First, the app connects to an API (I'm using Yummly) to pull in information on some recipes. Next, we pass this info to Handlebars and run it through the template. Finally, we replace the body section with the newly generated HTML. It's a fairly straight forward process; so, let's start by adding a second script
block right before the closing body
tag, and instantiate an Ajax
variable:
<script> var Ajax = (window.XMLHttpRequest) ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"); Ajax.onreadystatechange = function(){ if (Ajax.readyState == 4 && Ajax.status == 200) { //Parse the JSON data var RecipeData = JSON.parse(Ajax.responseText); //Get the Template from above var Source = document.getElementById("Handlebars-Template").textContent; //Compile the actual Template file var Template = Handlebars.compile(Source); //Generate some HTML code from the compiled Template var HTML = Template({ Recipes : RecipeData }); //Replace the body section with the new code. document.body.innerHTML = HTML; } } Ajax.open("GET","Recipe.php", true); Ajax.send(); </script>
If your site’s data regularly changes, then you might want to take a look at Handlebars.
This is the complete code for compiling and generating HTML code from a template. You can technically pass the JSON data from the API directly into Handlebars, but you run into cross origin issues. Instead of performing some sort of hack or using PHP to "echo" the data into a JavaScript variable, I decided to put all of that into a separate file: Recipe.php
. So before we start building the template, let's go take a look at that PHP file.
Getting The Data
The Yummly API is pretty simple. There is no elaborate authentication system; you just have to sign up, get some credentials, and insert them into the URL. You can directly echo the data if you want to, but I want a bit more detailed info on each recipe. Therefore, I will process the data from the first API call and make a second request for every recipe. Here is the complete PHP script:
<?php //Empty Array to hold all the recipes $Json = []; $UserID = //Your ID Here; $UserKey = //Your Yummly key; //This searches Yummly for cake recipes $Recipes = file_get_contents("http://api.yummly.com/v1/api/recipes?_app_id=" . $UserID . "&_app_key=" . $UserKey . "&maxResult=2&requirePictures=true&q=Cake"); //Decode the JSON into a php object $Recipes = json_decode($Recipes)->matches; //Cycle Through The Recipes and Get full recipe for each foreach($Recipes as $Recipe) { $ID = $Recipe->id; $R = json_decode(file_get_contents("http://api.yummly.com/v1/api/recipe/" . $ID . "?_app_id=" . $UserID . "&_app_key=" . $UserKey . "&images=large")); //This is the data we are going to pass to our Template array_push($Json, array( Name => $R->name, Ingredients => $R->ingredientLines, Image => $R->images[0]->hostedLargeUrl, Yield => $R->yield, Flavors => $R->flavors, Source => array( Name => $R->source->sourceDisplayName, Url => $R->source->sourceRecipeUrl ) )); } //Print out the final JSON object echo json_encode($Json); ?>
By building your site with a Handlebars template, you can produce a full site's worth of code in only a few lines. Here is the entire template:
<script id="Handlebars-Template" type="text/x-handlebars-template"> <div id="Content"> <h1>ΞRecipeCards <span id='BOS'>Recipe search powered by <a id='Logo' href='http://www.yummly.com/recipes'> <img src='http://static.yummly.com/api-logo.png'/> </a> </span> </h1> {{#each Recipes}} <div class='Box'> <img class='Thumb' src="{{{Image}}}" alt="{{Name}}"> <h3>{{Name}} <a id='Logo' href="{{Source.Url}}"> - {{Source.Name}}</a></h3> <h5>{{getFlavor Flavors}}</h5> <h5>{{Yield}}</h5> <p>Ingredients:</p> <ul> {{#each Ingredients}} <li>{{this}}</li> {{/each}} </ul> </div> {{/each}} </div> </script>
Let's run through this code. The first seven lines are just the logo at the top of the page. Then for each recipe, we create a recipe 'card' with a picture, name, and ingredients.
The Yummly API returns a list of flavor data (i.e. how sweet, sour, spicy, etc..) for each item. I wrote a function helper, called getFlavor
that takes this info and returns the most dominant flavor in the dish. In order for this template to work, we need to load in the getFlavor
helper into Handlebars before parsing the template. So at the beginning of the second script section, add the following code before the Ajax code:
Handlebars.registerHelper("getFlavor", function(FlavorsArr){ var H = 0; var Name = ''; for(var F in FlavorsArr) { if(FlavorsArr[F] > H) { H = FlavorsArr[F]; Name = F; } } return "This Dish has a " + Name + " Flavor"; });
Now, whenever Handlebars sees getFlavor
, it calls the associated function and retrieves the flavor information.
At this point, you are free to play around and design the template however you wish, but you will most likely see that this process is slow. This is primarily due to the three API calls before Handlebars loads the page. Obviously, this is not ideal, but precompiling your template can help.
Precompiling
You have two different options, when it comes to Handlebars. The first is to just precompile the actual template. This reduces the loading time, and you won't have to include the Handlebars compiler with your page.
This also results in a smaller file size, but this doesn't really help in our scenario.
Our problem is the communication between the browser and the API. If you did want to precompile your template, you can download the Node.js package through npm
with the following command:
npm install handlebars -g
You may need to do this as root (i.e. add 'sudo' before the command). Once installed, you can create a file for your template and compile it like so:
handlebars demo.handlebars -f demo.js
You should give your template file a .handlebars
extension. This is not mandatory, but if you name it something like demo.html
, then the template's name will be "demo.html" as apposed to just "demo". After naming your template, simply include the output file along with the run-time version of Handlebars (you can use the regular version, but it's larger) and type the following:
var template = Handlebars.templates['demo']; var html = template({ Your Json Data Here });
The
unless
statement is...essentially an invertedif
statement.
But, as I mentioned before, this doesn't really help us in this scenario. What then can we do? Well, we can precompile and output the entire file. This makes it so that we can run the template with data and save the final HTML output - caching, in other words. This drastically speeds up the load time of your application. Unfortunately, client-side JavaScript doesn't have file IO capabilities. So, the easiest way to accomplish this is to just output the HTML to a text box and manually save it. Be aware of an API's guidelines on caching. Most APIs have a maximum amount of time that data can be cached for; make sure to find that information before saving static pages.
Conclusion
This has been a quick introduction to Handlebars. Moving forward, you can look into "Partials" - small templates that can be used like functions. As always, feel free to leave a comment or question in the comment section below.
Comments