Control flow in EJS Templates

Last content update September 20th, 2022

In this lesson, we are going to look at some more advanced ways of how to use the templating language EJS. Particularly, weā€™re going to look at using control flow concepts such as functions, conditions, and loops.

šŸ’” Control flow is a programming term referring to any kind of programming logic that changes when, if, or how often code is executed - therefore ā€œcontrolling the flow.ā€ This refers generally refers to conditions (aka if-statements) and loops but can also include functions.

We are also going to look into using more complex data types such as arrays and objects.

Most of the things we will be looking at in this lesson are just regular JavaScript. So technically speaking, it shouldnā€™t include anything entirely new to you. But what might be new is understanding how to transfer that knowledge to use it in EJS.

Rendering items from an array

Most web applications render lists of items. This might be a list of shop items, a list of blog articles, a list of tasks, a list of chat messages, etc. Therefore, knowing how to display a list in your templates is a crucial skill.

The nice thing about the templating language EJS is that itā€™s ā€œjust JavaScriptā€ wrapped inside some custom EJS tags such as <% %>, <%= %>, or <%- %>. That means we can use regular loops when working with arrays (aka lists).

Letā€™s put a very simple array in our backend code (probably your app.js file). It should be somewhere close to the top and before the various route functions. For example, you can put it right below the import statement at the top.

const cookies = [
  "Chocolate Chip",
  "Banana"
]

šŸ¤” If you have been following the advanced tasks from the previous lessons, you may already have a more advanced array containing objects. Later, in this lesson, weā€™ll use an array exactly like that. But weā€™ll start with a little simpler version. If you still want to be able to follow along, you could pick a different name for the variable above - e.g., cookieStrings.

Now, Iā€™d like to render the list of cookies on the /cookies page. To do that, I can pass the cookies variable to the corresponding template. So first, I have to update my route function:

app.get('/cookies', (request, response) => {
  response.render('cookies/index', { cookies: cookies })
})

With this, I have access to the cookies variable in the template.

The route function renders a template found in the following path: views/cookies/index.ejs. Somewhere in <body> of your HTML add the <%= cookies %>. Remember, the <%= %> will put the output of your JavaScript code in the HTML. In this case, itā€™ll be the value of the cookies variable.

In my example, I put it in the <main> element of my page:

<main>
  <p>Our current offering:</p>
  <ul>
    <%= cookies %>
  </ul>
</main>

If you now access the cookies page localhost:3000/cookies you should see the array rendered as a simple list on the page.

That sort of works. But we would like each item in the list to have its own line and look like a regular HTML list. What we want to end up with is something like this:

<main>
  <p>Our current offering:</p>
  <ul>
    <li>Chocolate Chip</li>
    <li>Banana</li>
  </ul>
</main>

So what we need to do is alter each element in the array and add a <li> in front of the string and a </li> behind the string.

For that, we can use a regular JavaScript forEach() loop. In a JavaScript file, we could use a forEach() look, for example, like this:

cookies.forEach(cookie => {
  '<li>' + cookie + '</li>'
})

The loop above iterates over every single cookie in the cookies array. Each item in the array represents a string. So the cookie variable on the first iteration will represent ā€œChocolate Chipā€, and on the second iteration, it will represent ā€œBananaā€.

šŸ’” If this sounds foreign or new to you, I suggest to review how loops work in JavaScript and specifically learn about the forEach loop.

You can put multiple lines of JavaScript code in EJS templates using multiple EJS tags. Look at the code below carefully and write it by hand in your own code. (Donā€™t copy and paste!)

<main>
  <p>Our current offering:</p>
  <ul>
    <% cookies.forEach(cookie => { %>
      <li><%= cookie %></li>
    <% }) %>
  </ul>
</main>

The result should look like this:

If this is hard to understand to you, spend some time comparing the three recent code snippets.

The first and the last line of the loop use regular <% %> tags because they just contain JavaScript code that should not show up in the HTML code.

The contents of the forEach loop will be inserted into the HTML as often as the loop code between the { and } runs. In each iteration, you have access to the variable cookie representing a single item in the array.

And we can use a <%= %> tag to print out the contents of the variable cookie.

If this still seems a little strange to you, keep practicing with different kinds of arrays. Change the HTML code within the for loop and see what happens. Try to get a feel for whatā€™s happening, and donā€™t shy away from breaking stuff.

Rendering objects

Now, we have a list of cookie names. But Iā€™d like to be able to link to each of them. So, in addition to their name, I need to know a slug for each one of them.

šŸ’” Remember, a slug is a unique identifyer that can be used as part of a URL. So it should be all lowercase without any special character or spaces. Usually slugs look like this: i-am-a-slug, chocolate-chip.

Right now, each list item looks like this:

<li>Chocolate Chip</li>

But I would like it to look like this:

<li>
  <a href="/cookies/chocolate-chip">Chocolate Chip</a>
</li>

That means I need two pieces of data: A name (ā€œChocolate Chipā€) and a slug (ā€œchocolate-chipā€). But my array of cookies currently only contains a list of strings where each string is just the name of the cookie. So letā€™s update the array in the app.js to use objects instead. Objects allow us to provide many data points to a single item.

const cookies = [
  { name: 'Chocolate Chip', slug: 'chocolate-chip' },
  { name: 'Banana', slug: 'banana' }
]

If you now refresh the page, though, youā€™ll see this in the web browser.

The individual cookie in each iteration of the forEach() loop now represents an object and not a string. Therefore, <%= cookie %> will output [object Object]. To read the name property of the object, just change it to cookie.name:

<main>
  <p>Our current offering:</p>
  <ul>
    <% cookies.forEach(cookie => { %>
      <li><%= cookie.name %></li>
    <% }) %>
  </ul>
</main>

And with the same method, we can now add the HTML anchor tag and use the <%= cookie.clug %> to define the href attribute:

<main>
  <p>Our current offering:</p>
  <ul>
    <% cookies.forEach(cookie => { %>
      <li>
        <a href="/cookies/<%= cookie.slug %>"><%= cookie.name %></a>
      </li>
    <% }) %>
  </ul>
</main>

If you now access localhost:3000/cookies youā€™ll see a list of links where each link leads to one of the following pages: localhost:3000/cookies/chocolate-chip, localhost:3000/cookies/banana.

Conditional rendering

Using the same method as with the loop, we can use EJS tags to also add JavaScript if-conditions to the template.

Letā€™s say we want to signal to users whether a particular type of cookie is sold out. As a first step, we need to add that piece of information to the list of cookies signaling whether or not this particular type of cookie is sold out. So Letā€™s add a boolean property for that to our list:

const cookies = [
  { name: 'Chocolate Chip', slug: 'chocolate-chip', isInStock: true },
  { name: 'Banana', slug: 'banana', isInStock: false }
]

We set one of the cookies to not be in stock anymore.

Now, we can use this new property (isInStock) and a regular JavaScript condition to display a warning if the item is sold out. Back in the views/cookies/index.ejs change the code inside the existing loop like this:

<% cookies.forEach(cookie => { %>
  <li>
    <a href="/cookies/<%= cookie.slug %>"><%= cookie.name %></a>
    <% if(!cookie.isInStock) { %> [SOLD OUT] <% } %>
  </li>
<% }) %>

You can see that the entire condition fits in a single line. The opening tag ends with a {, followed by some plain text saying [SOLD OUT]. Then we signal the end of the condition with a tag that only includes the } closing braces.

The condition itself uses the bang operator !, and says: if the cookie is not (!) in stock, then show the message [SOLD OUT].

You can also spread the condition onto multiple lines and turn it into a if-else block like this:

<% cookies.forEach(cookie => { %>
  <li>
    <% if(cookie.isInStock) { %> 
      <a href="/cookies/<%= cookie.slug %>"><%= cookie.name %></a>
    <% } else { %>
      <%= cookie.name %> [SOLD OUT] 
    <% } %>
  </li>
<% }) %>

This condition will only display a link to the cookie page if it is in stock. Otherwise, itā€™ll just list the name of the cookie in plain text.

You can use the same logic to write more complicated code, such as if-else if-else` blocks or combine multiple nested loops and conditions. You have endless options.

Helper functions

The last concept we will look at is passing functions to templates.

You already know that you can pass simple and complex data to the templates by adding them to an object as the second parameter of the response.render() function. Using the same method, you can also pass entire functions to the template. This might be helpful if you want to perform smaller kinds of logic - like converting a number to a currency or formatting a date. You can also hide more complex condition logic in functions.

Letā€™s add a price to each of our cookies. When working with currencies in applications, a common practice is to work with integers and store all prices in the smallest possible currency unit. In the case of Euros or US dollars, those would be called ā€œcentsā€. Developers do that to maintain the highest possible level of accuracy and avoid issues with floating point numbers (an issue that we wonā€™t be able to go into detail about here.)

So letā€™s add prices in cents to the list of cookies:

const cookies = [
  { name: 'Chocolate Chip', slug: 'chocolate-chip', priceInCents: 350, isInStock: true },
  { name: 'Banana', slug: 'banana', priceInCents: 300, isInStock: false }
]

We could display those prices directly to the user. But a much more user-friendly approach would be to convert the 350 to something like $3.50.

There are different ways to do that - and arguably, the best way to do that is to pass the conversion on the backend and pass only the $3.50 to the template. But weā€™re here to practice using functions in templates. So weā€™re going to do it that way for now.

Itā€™s always a good idea not to put too much business logic in the same file. So weā€™ll put our function for converting the cents to a readable price in its own file.

In the root of your project, create a new folder called helpers (a common place for general purpose helper functions. Folder names with similar concepts are util utility functions, services, or lib.)

In that folder, create a new file called cookie-views.js.

Next, add a function, that converts our priceInCents to a decimal number and adds a currency symbol to it.

export const readablePrice = (priceInCents) => {
  return '$' + (priceInCents / 100 )
}

šŸ’” An interesting alternative function is also .toLocalString(). You can find more information about it on MDN. Particularly, if you want to display pricing in different currencies, this could be a quite useful function.

We export the function to make it available in other JavaScript files of our project. This allows us to go back to the app.js file and use import to import the function at the top of the file:

import { readablePrice } from './helpers/cookie-views.js'

To pass the function to the /cookies route, we add it as an additional parameter to the object that already contains the cookies variable:

app.get('/cookies', (request, response) => {
  response.render('cookies/index', { 
    cookies: cookies,
    readablePrice: readablePrice
  })
})

Note that we donā€™t execute the function here (using parenthesis and writing readablePrice())! We just pass the function variable to the template and want to execute it only in the template.

Back in the /views/cookies/index.ejs file, we can now call the function and pass as a parameter the priceInCents of each cookie:

<li>
  <% if(cookie.isInStock) { %> 
    <a href="/cookies/<%= cookie.slug %>"><%= cookie.name %>: <%= readablePrice(cookie.priceInCents) %></a>
  <% } else { %>
    <%= cookie.name %> [SOLD OUT] 
  <% } %>
</li>

With the code above, your page should now look something like this:

You can see that the function can be passed to the template just like any other variable.

šŸ’” As mentioned before, using helper functions like this is a little bit of a controverse topic. A lot of developers will argue that the views (aka templates) should contain as little logic as possible. Instead, the logic should be handled in a separate place of the code. A very common advanced approach to handle this is the concept of View Models or Presenter functions. Using that method, youā€™d have one larger class or function perform all the necessary logic on the backend and then pass the ready-to-use object to the template. For example, you could keep a function called cookiesView() in a separate file in your project. The return of the function is an object that may look like this: { cookies: [ ... ], pageTitle: "Cookieshop" } And instead of writing the object directly in the render() function, you pass this view model function instead (which will return the entire object). render('/cookies/index', cookiesView())

Recap

This was a very full lesson. However, most of what we used was just plain JavaScript wrapped in EJS tags. You learned to render items from arrays using forEach(), as well as applying if-else conditions. Lastly, we looked into passing functions to the template to add some business logic.

šŸ›  How to practice

If you havenā€™t done it yet through the advanced tasks of the previous lessons, now is a good time to update the /cookies/:slug route of your application and use everything that you have learned in this lesson.

To practice what you have learned in an additional, separate project, include an array of object in it and try to list different items using EJS and forEach. Additionally, add some if-else conditions to have different HTML show up based on various conditions being true.