Code

JavaScript: Handlebars API Example

posted in: JavaScript | 0

This example uses the Handlebars API to display data. I’ll use a Web API (created by Daniel Schmitz, full database here) to get data from Microsoft’s venerable Northwind sample database, and Handlebars to display the data. Specifically, I’ll display a report that lists all of the orders for one customer in the customers table.

Using async/await to fetch data with a Web API is incidental to the example. Handlebars doesn’t require any specific data format: it can accept object literals, or even string literals, as easily as JSON data fetched with a Web API.

The Handlebars template setup

A Handlebars template setup has a minimum of four steps (“Setting up the header” below provides an applied example of these steps, if you prefer to skip to that):

  1. In an HTML file, create a tag inside the body (a “static tag”), any tag that is used to contain other HTML. This tag will serve as a container for the dynamically created HTML created by Handlebars. (This is not strictly necessary; you can use the body as the container. But it makes things easier to organize.)
  2. Put the HTML that Handlebars will dynamically create in a script tag with the type attribute set to "text/x-handlebars". For each of the dynamic elements, create a variable using the {{variable}} Handlebars syntax.
  3. In the JavaScript script, create a variable, assigning to it the return value of a call to Handlebars.compile, to which is passed a reference to the innerHTML property of the Handlebars script created in step 1.
  4. Call the function created in step 3. To that function call, pass as an argument contained in curly braces (e.g. myFunction({myArgument})) the data that will be dynamically rendered in the Handlebars script tag. This data must contain field names that match the variables in the template in step 2 (see the example below for an exception). Assign the function’s return value to the innerHTML property of the static tag created in step 1. (e.g. myStaticTag.innerHTML = myFunction({myArgument}).)

An example will help make sense of these steps. To keep it simple, I’ll explain them in the context of setting up the header (leaving out the order list display until later). The header displays the customer’s company name.

Setting up the header

Adding this header is pretty straightforward, and a good way to demonstrate a basic Handlebars setup.

The header’s static HTML is an empty <header> tag. To this HTML, we’ll add the Handlebars <script> tag that will insert into the <header> tag a dynamically created h4 tag containing the name of the company.

I’ve noted where the code implements the above four steps on the lines that implement them.

Here’s the HTML:

And here’s the JavaScript:

This is how the above example implements the four basic steps:

  1. Step 1 is the <header> tag of the HTML.
  2. Step 2 is the script tag with the id of header. The variable is called {{customer}}.
  3. Step 3 is near the top of the JavaScript, set up as the headerTemplate variable.
  4. Step 4 is near the bottom of the JavaScript. The header variable is a reference to the static tag created in step 1. Note also that the customer variable is assigned a string (customer.companyName as returned from the call to fetchCustomerName) rather than an object. Handlebars can handle either.
Linking non-matching variable names

You will notice that the script in step 2 contains a {{customer}} variable, and the JavaScript code in step 4 contains a matching {customers} variable that gets passed in to the HTML template. This is as stated in Step 4. However, it is possible to link non-matching variable names.

Suppose step 2 of the HTML had this:

Then we would have a variable name that didn’t match the one the JavaScript code passes in ({customer}). To link them, I can change line 18 of the JavaScript code to this:

This maps the {{custName}} variable in the HTML to the {customer} variable in the JavaScript code.

The async IIFE (lines 15-19), the top-level or main function, calls a helper function (fetchCustomerName, line 16) to fetch the customer name data. I used this main/helper structure so I could use a separate helper function for each data call, and call them all from the main function. (So far, there’s only a fetchCustomerName function, but we’re adding a fetchOrders function in the next step.)

After the main (IIFE) function calls the helper function to retrieve data from the web, it then calls the appropriate template function (line 18), passing the data, and assigning the result to a static HTML element’s innerHTML property.

This IIFE also takes a string argument ('ALFKI', line 19) that is the ID for a single customer in the customers table.

The result of all this is to show the name of the company whose orders are to be listed. The next step is to create the order list.

Setting up the order list

The next task is to display the list of orders, a more complex task than displaying the header. For each order, we will list an order date, a ship date, and the name of the person making the sale. We will also create a sublist of the product names of all the individual line items in the order.

The orders’ static HTML tag is a dl tag. The Handlebars template will plug in dt and dd tags containing the order data.

The actual order object exposed by the Web API doesn’t have all of the information we need. Although it has the order and ship dates, it has only the foreign keys for the employee who sold the order and the products sold (called employeeId and productId, respectively). So, for each order, we will have to use those foreign keys to look up the name of the employee in the employess table (Mr. Schmitz has misspelled “employees” so we will have to do the same), and the name of each product sold in the products table.

To do this, we will need to iterate through the orders collection. For each order:
1. Use the employee id to look up the name of the employee who sold the order.
2. Iterate through the products in the order’s details collection. For each product, look up the product name.

This API allows us to accomplish these lookups in either of two ways:
1. In each iteration, use the employess\id and products\id routes to look up individual matching employee and product names.
2. Use the employess and products routes to pull all of the employees and products into a pair of arrays before starting the iterations, and then in each iteration use the find method to look up matching employee and product names in the arrays.

Performance concerns

Which process is faster depends on the size of the the lookup tables (employees and products). Individual round trips to fetch data are expensive, but so is fetching a large number of records on a single round trip. Because there are only 77 products and nine employees in the database, the approach in option 2 works best here. About 50 round trips are required to implement the first option, while implementing the second option fetches fewer than 100 superfluous records. (I tested both options, and found that option 2 runs considerably faster than Option 1.)

Here is the complete HTML for the orders list:

I’ve broken up the orders into two scripts, orderList and orderInstance, to demonstrate (“figure out” is perhaps more accurate) partial scripts. In this case, orderInstance is a partial script. The {{#each list}} helper iterates through each member of the list collection; {{> orderInstance}} renders the orderInstance HTML template once per member. The > character indicates a call to a partial list.

It isn’t really necessary to use a partial script; we could have everything in one script by substituting the contents of orderInstance for the {{> orderInstance}} line in orderList. But it does separate concerns a bit.

Here’s the full JavaScript code:

Explanation

To create a partial template, I need to first compile the template and then register it as a partial. With a little help from line 8, lines 9 and 10 create the partial template. (Line 8 just stores document.querySelector('#orderInstance').innerHTML to a variable to avoid having to write it twice.) Line 9 compiles the script in the usual manner, and line 10 uses Handlebars.registerParital to register the script as a partial.

Next, line 12 registers a custom helper called dateFormat. The orders object has dates as strings in 'MM-DD-YYYY HH:MM:SS' format. All this helper does is use String.prototype.slice to remove the time, in effect changing the date to 'MM-DD-YYYY' format. The helper gets called on lines 18 and 19 in the HTML script template.

The fetchOrders function (line 21) creates three datasets, assigning them to the orders, employees and products variables respectively:
1. The orders for the customer whose ID is passed in to the IIFE (lines 22-3).
2. All of the products in the database (lines 25-6).
3. All of the employees in the database (lines 28-9).

The function then loops through the orders array (line 31), to add the soldBy and productName properties to each order object in the array. For each order, the function:
1. Uses find to look up the employee name in the employees dataset, and adds it to the current order object as a soldBy property.
2. Loops through the array of order items assigned to the details property. For each detail item, the function uses find to look up the product name in the products dataset and assign the result to a productName property.

After Handlebars adds the additional information to the objects in the orders array, the function returns the array.

The async IIFE (lines 43-52) now additionally calls the fetchOrders function asynchronously, and passes the resulting orders object to a call to the listTemplate function. The return value of that call is the dynamic HTML that is inserted into the dl tag.