EJS: Embedded JavaScript Templating
One of the fundamental things that web frameworks do for us is to make it easier to combine dynamic data (which the app has looked up or computed) with a static HTML template to create a web page. The web page is therefore dynamically computed by the app.
There are several templating systems in common use with Express.
One very common and popular templating system for Express is Jade; indeed, it's the default. I have decided against it for CS304 because it's essentially a whole new language, substituting for HTML. The one I've chosen is EJS — Embedded JavaScript Templating. With EJS, the template looks like HTML, but you can insert special tag-like things that allow you to use JavaScript to create HTML elements. The result is a little ugly, but relatively easy to learn, because it just combines two languages we know: HTML and JS.
This reading introduces EJS for this semester.
EJS Tutorials¶
Because EJS is a well-known templating engine, there are some good tutorials on the web. Feel free to consult one of these for a different presentation.
But you can get the essentials of EJS from this reading.
First Example¶
Let's start with a relatively simple example, based on our Hello World example. Instead of replying with a fixed string, "Hello World!", we'll render a template for a complete web page, and the dynamic data will be the current time and the number of times the page has been visited (just a global counter).
Here's the relevant part of the server.js
file:
var visitCounter = 0;
// This is our simple Hello World example, this time, rendering a template
app.get('/', (req, res) => {
let now = new Date();
visitCounter ++;
return res.render('index.ejs',
{visits: visitCounter,
time: now.toLocaleTimeString()});
});
The last statement renders the template, where "rendering" means to combine a template (the first argument) with the dynamic data (the second argument) and then send the result to the browser.
The template file looks like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Node Starter</title>
<!-- load local stylesheet (css) -->
<link rel="stylesheet" href="/styles.css" />
<!-- loading jquery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
</head>
<body>
<h1>Welcome!</h1>
<p>Everything is fine.</p>
<p>This visit is at <%= time %>.</p>
<p>There have been <%= visits %> visits since the app started.</p>
</body>
</html>
Almost all of that is HTML that you are familiar with. But the two differences are these lines:
<p>This visit is at <%= time %>.</p>
<p>There have been <%= visits %> visits since the app started.</p>
The tag-like syntax <%= expression %>
is how we substitute some
dynamic data into the template. The first inserts the time
value
from the data; the second inserts the visits
.
Look back at the render
function call:
return res.render('index.ejs',
{visits: visitCounter,
time: now.toLocaleTimeString()});
The second argument is a dictionary. The keys, visits
and time
correspond to expressions in the template file. The values are what
replaces the keys.
Setup¶
A few things have to be set up first. We have to configure our Express app to use EJS, so the following line gets added to the prelude part of the server:
app.set("view engine", "ejs");
A "view engine" is a templating engine, because these dynamically rendered pages are called views. (Recall the MVC: Model-View-Controller way of thinking about a web application.)
Secondly, the index.ejs
file is stored in a folder called views
in
the same folder as the server file. All our templates will be stored
in the views
folder. Our app structure looks like this:
app/
connection.js -> ../connection.js
cs304.js -> ../cs304.js
node_modules -> ../node_modules
server.js
views/
index.ejs
That's all the setup that is necessary for EJS.
Tags¶
Just being able to substitute one expression is a nice start, but templating engines give us a lot more power. Here is a partial list of the tags in EJS:
<%=
Outputs the value into the template (HTML escaped)<%-
Outputs the unescaped value into the template<%
'Scriptlet' tag, for control-flow, no output.<%#
Comment tag, no execution, no output<%%
Outputs a literal<%
%>
Plain ending tag
There are other features which we'll get into if needed.
The first item on that list is the one we've seen in our example: it substitutes a value into the template.
The second does the same thing, but without "escaping". Substituting "unescaped" data involves a small security risk involving XSS attacks, which we will discuss later in the course. So for now, stick to the first tag.
The third is an important tag, because it allows us to have control flow: conditionals and loops. We'll discuss that in the next few sections.
All the other tags are straightforward: we can insert comments into our templates that then get removed when the template is rendered (unlike, say, HTML comments, which would get sent to the browser). So, if we're feeling uncharitable:
<%# this section is for users who are idiots %>
<p>You can read more about <a href="FAQ.html"> <%= topic %> </a>.</p>
The two most common things we want to do with templates, other than substituting a single expression, are conditionals and loops. Let's cover those next.
Conditionals¶
Suppose that our app has a way to log someone in and the user id is stored in a variable. In the template for the home page, we want to put a login form, but only if the person is not logged in. If they are logged in, we want to put a customized logout button.
Here's the server code:
app.get('/home', (req, res) => {
let userId = getUserId();
return res.render('home.ejs', {userId: userId});
});
It's pretty simple; it just gets the user id and passes it to the
render
function.
The HTML will look like this, with the lines of a JS if
statement
spliced into the HTML using the <% %>
syntax:
<% if( userId === null ) { %>
<% } else { %>
<% } %>
We can then put HTML, including other EJS stuff, in the two branches, so the finished template looks like this:
<% if( userId === null ) { %>
<%# login form, because user not logged in %>
<form action="/login" method="post">
<label>username <input type="text" name="username"></label>
<label>password <input type="password" name="password"></label>
<input type="submit" value="login">
</form>
<% } else { %>
<%# logout button %>
<form action="/logout" method="post">
<button>logout <%= userId %></button>
</form>
<% } %>
Notice the comments, which will be omitted from the rendered page.
Now let's look at loops.
Loops¶
So far, I've asked you to avoid loops in JavaScript because I wanted
you to practice with callback function arguments using forEach
. And
we can use forEach
in EJS as well.
The syntax is not too bad, though. Recall that, to use forEach()
over an array, we just do:
array_variable.forEach( function (elt) {
// body of "loop"
});
So, we need to splice the beginning and end of the forEach
into our
template:
<% array.forEach( function (elt) { %>
<!-- loop body here -->
<% }) %>
Here's an example, where we might have a list of names and we want to create a bullet list of those names:
<h1>List of <%= listDescription %></h1>
<ul>
<% list.forEach( (elt) => {%>
<li><%= elt %></li>
<% }) %>
</ul>
In the code above, I used an arrow function, but either kind of function is fine.
The server code might be as simple as:
const ourHeroes = ['Harry', 'Ron', 'Hermione', 'Fred', 'George'];
app.get('/heroes', (req, res) => {
return res.render('list.ejs',
{listDescription: 'Some Heroes of the Harry Potter Saga',
list: ourHeroes});
});
Compound Data¶
So far, we've just been passing in strings and other "atomic" data. What about passing in dictionaries or lists of dictionaries? This isn't any harder, because we have the power of JavaScript with EJS. So, supposing we have an array of dictionaries as our data:
const ourHeroes2 = [{nm: 1, name: 'Harry'},
{nm: 2, name: 'Ron'},
{nm: 3, name: 'Hermione'},
{nm: 6, name: 'Fred'},
{nm: 7, name: 'George'}];
app.get('/heroes2', (req, res) => {
return res.render('list.ejs', {listDescription: 'Our Heroes',
list: ourHeroes});
});
The EJS is barely different. We just use elt.name
to access the name
property:
<h1>List of <%= listDescription %></h1>
<ul>
<% list.forEach( (elt) => {%>
<li><%= elt.name %></li>
<% }) %>
</ul>
Partials¶
EJS has one more cool trick. Very often web pages of a multi-page site have common features: a navbar, a logo, a header or footer, login forms, and so forth. We could copy/paste these from one template to another, but copy/paste is difficult to maintain: if you make a change to the navbar, you then have to go and update every place that you copied it to.
Copy/paste violates the DRY principle: Don't Repeat Yourself. While copy/paste has its place, it's useful to be able to avoid it when we want to, as with repeated chunks of HTML.
EJS solves this with what it calls partials. You can put the chunk
of code in another file, typically in a subfolder of your views
called partials
. For example, we could have a file
partials/navbar.ejs
containing this plain HTML:
<nav>
<ul>
<li><a href="/">home</a></li>
<li><a href="/about">about</a></li>
<li><a href="/contact">contact</a></li>
<li><a href="/order">order</a></li>
</ul>
</nav>
Then in our template files, we can include the partial like this:
<%- include("partials/navbar.ejs") %>
This allows us to assemble our dynamic web pages out of larger chunks of HTML, which is a big advantage.
Static Files¶
Our dynamic web pages may also refer to static files, such as a CSS file. In fact, our templates above all contained the following line of HTML:
<link rel="stylesheet" href="/styles.css">
We could similarly have static files of JavaScript code (to run in the browser), image files (for logos, backgrounds, and the like) and so forth.
Since the CSS is a static file, there's no need to write a handler function or to dynamically compute anything. Instead, Express can just send the file as needed. To do that, we have the following lines in our prelude:
const serveStatic = require('serve-static');
app.use(serveStatic('public'));
We mentioned this last time, in our discussion of Express.
We then put the static files in the public
folder. This folder is in
our main folder, as a sibling of views
. So our app structure now
looks like:
app/
connection.js -> ../connection.js
cs304.js -> ../cs304.js
node_modules -> ../node_modules
public/
styles.css
server.js
views/
index.ejs
partials/
navbar.ejs
That's about it for the complex structure of our apps. We'll add more templates, partials and static files, but this is as many folders as we'll need, at least for a while.
All Example Files¶
Here are links to all the example files, instead of just snippets. All
of the working files are in the ~cs304node/apps/ejs/
folder; we'll
play with that in class.
- server.js
- views folder
- views/index.ejs shows expression substitution
- views/main.ejs shows conditionals
- views/list.ejs shows loops
- views/list2.ejs shows loops over a list of dictionaries
- views/main.ejs shows use of partials
- views/partials/navbar.ejs shows a definition of a partial
- public folder
Summary¶
- EJS allows us to mix HTML and JavaScript in a template.
- The
res.render(template, data)
method combines the data with the template to produce a page of pure HTML - The template can insert dynamic data using the
<%= expression %>
tag. - The template can use control structures (conditionals and loops) using
<% syntax %>
- The templates are stored in a
views
folder. - EJS also allows partials, which can pull chunks of HTML from files
- Partials are typically stored in a
views/partials
folder - Partials are used by
<%- include('path/to/partial') %>
Exhanced Object Literals¶
Version 6 of JavaScript (ES6) has a enhanced object literal syntax that I have often found handy.
There is a small trick, which can be convenient at times. We often
have some variables, say x
and y
and we want to create an object
literal (a dictionary) with keys matching those values. Like this:
let x = 3;
let y = 4;
let obj = {x: x, y: y}; // {x: 3, y: 4}
In that not un-common situation, we can abbreviate and just give the keys:
let x = 3;
let y = 4;
let obj = {x, y}; // {x: 3, y: 4}
This avoids a small amount of tedious typing, but also (in my opinion) makes the code a little clearer.
So, back in the section on conditionals the argument
to render
could just be {userId}
instead of {userId: userId}