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):
- 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.) - Put the HTML that Handlebars will dynamically create in a
script
tag with thetype
attribute set to"text/x-handlebars"
. For each of the dynamic elements, create a variable using the{{variable}}
Handlebars syntax. - 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 theinnerHTML
property of the Handlebars script created in step 1. - 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 Handlebarsscript
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 theinnerHTML
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.7.7/handlebars.js"></script> <script src="test.js"></script> </head> <body> <header></header> <!-- Step 1 --> <script id="header" type="text/x-handlebars"> <!-- Step 2 --> <h4>Order List for {{customer}}</h4> <!-- will be inserted in the <header> tag --> </script> </body> </html> |
And here’s the JavaScript:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
document.addEventListener('DOMContentLoaded', () => { const NWIND_URI = 'https://northwind.vercel.app/api/'; const headerTemplate = Handlebars.compile(document.querySelector('#header').innerHTML); // Step 3 async function fetchCustomerName(custId) { const custResponse = await fetch(`${NWIND_URI}customers/${custId}`); const customer = await custResponse.json(); return customer.companyName; } (async (custId) => { const customer = (await fetchCustomerName(custId)); const header = document.querySelector('header'); header.innerHTML = headerTemplate({customer}); // Step 4 })('ALFKI'); }); |
This is how the above example implements the four basic steps:
- Step 1 is the
<header>
tag of the HTML. - Step 2 is the
script
tag with the id ofheader
. The variable is called{{customer}}
. - Step 3 is near the top of the JavaScript, set up as the
headerTemplate
variable. - 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 thecustomer
variable is assigned a string (customer.companyName
as returned from the call tofetchCustomerName
) 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:
1 2 3 |
<h4>Order List for {{custName}}</h4> |
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:
1 2 3 |
header.innerHTML = headerTemplate({custName: customer}); |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.7.7/handlebars.js"></script> <script src="orders.js"></script> </head> <body> <header></header> <dl></dl> <script id="header" type="text/x-handlebars"> <h4>Order List for {{customer}}</h4> </script> <script id="orderInstance" type="text/x-handlebars"> <dt>Order Date: {{dateFormat orderDate}}</dt> <dt>Ship Date: {{dateFormat shippedDate}}</dt> <dt>Sold By: {{soldBy}}</dt> <dt>Items: {{#each details}} <dd>{{productName}}</dd> {{/each}} </dt> <br> </script> <script id="orderList" type="text/x-handlebars"> {{#each list}} {{> orderInstance}} {{/each}} </script> </body> </html> |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
document.addEventListener('DOMContentLoaded', () => { const NWIND_URI = 'https://northwind.vercel.app/api/'; const headerTemplate = Handlebars.compile(document.querySelector('#header').innerHTML); const listTemplate = Handlebars.compile(document.querySelector('#orderList').innerHTML); const orderInstanceHTML = document.querySelector('#orderInstance').innerHTML; const orderTemplate = Handlebars.compile(orderInstanceHTML); Handlebars.registerPartial('orderInstance', orderInstanceHTML); Handlebars.registerHelper('dateFormat', str => str.slice(0, 10)); async function fetchCustomerName(custId) { const custResponse = await fetch(`${NWIND_URI}customers/${custId}`); const customer = await custResponse.json(); return customer.companyName; } async function fetchOrders(custId) { const ordResponse = await fetch(`${NWIND_URI}orders`); const orders = (await ordResponse.json()).filter(item => item.customerId === custId); const prodResponse = await fetch(`${NWIND_URI}products`); const products = await prodResponse.json(); const empResponse = await fetch(`${NWIND_URI}employess`); const employees = await empResponse.json(); for (order of orders) { let emp = employees.find(emp => emp.id === order.employeeId); order.soldBy = `${emp.lastName}, ${emp.firstName.slice(0, 1)}.`; for (item of order.details) { item.productName = products.find(product => item.productId === product.id).name; } } return orders; }; (async (custId) => { const customer = (await fetchCustomerName(custId)); const orders = (await fetchOrders(custId)); const header = document.querySelector('header'); header.innerHTML = headerTemplate({customer}); const dl = document.querySelector('dl'); dl.innerHTML = listTemplate({orders}) })('ALFKI'); }); |
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.