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. Thereturn
of the function is an object that may look like this:{ cookies: [ ... ], pageTitle: "Cookieshop" }
And instead of writing the object directly in therender()
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.
- Users should be able to navigate to each individual cookie page using the
:slug
in the URL. (e.g.,/cookies/chocolate-chip
) - The individual cookie page should render a
name
andprice
to the user. - If you havenāt already, add a new
description
field to the list ofcookies
and show the description text on the cookie page.
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.