Templates, data, state
In this tutorial we discover the Nunjucks template language, and apply
some techniques to keep the state. Create a new Glitch project, by
creating a new Node App, or by remixing one of your old projects; you
can delete all files in your new project, with the exception of
package.json
.
The references for this tutorial are
- The JavaScript manual on MDN,
- The Node.js reference,
- The Express guide,
- The Express API reference,
- The Nunjucks docs.
Static content and templates
In the previous tutorial, we used two different techniques to create pages:
-
For dynamic URLs, like
/query_string
, etc., we defined handlers in a JavaScript file. We generated HTML outputs by concatenating strings. -
For the static form, we created a simple HTML file.
These are two radically different ways of coding, and it would be disappointing if we couldn’t mix them. In this section, we are going to learn how Express can read and serve static files, then how the Nunjucks template engine lets us transition smoothly from static to dynamic pages.
Serving files
-
Create a
form.html
file, containing a form like thusChoose a color
red
yellowYou won’t be able to access your
form.html
right now in the preview, it is normal. See the next instructions to make this file accessible.Strive for a complete and valid HTML page: use a
<!Doctype>
,<head>
,<body>
, etc. Do not set themethod
andaction
attribute on the form, for the moment. -
Create a file named
app.js
, and initialize it with the following code:var express = require('express'); var bodyP = require('body-parser'); var cookieP = require('cookie-parser'); var app = express(); app .use(bodyP.urlencoded({ extended: false })) .use(cookieP()); // Your handlers go here app.listen(process.env.PORT);
-
Glitch will not run the code in
app.js
by default. For this, you must edit thepackage.json
file: look for the block"scripts": { "start": "node server.js" },
and change it to
"scripts": { "start": "node app.js" },
You must also tell Glitch what dependencies you want loaded in your project. For this, while still editing
package.json
, click on “Add Package” on top of the editor, and start typing the names of the packages you want to install: Glitch will add them to thedependencies
section ofpackage.json
and install them automatically withnpm
.Install the dependencies needed for this tutorial:
body-parser
,cookie-parser
andnunjucks
.Note: If you’re working on your local machine, you can create a
package.json
file by running thenpm init
command in a shell. Then you can run your project with thenpm start
command.To install packages, directly edit the
dependencies
section of yourpackage.json
file, then run thenpm install
command in a shell. When you modify thedependencies
section, you must runnpm install
again. -
Rename your file
form.html
topublic/form.html
. Add the following line toapp.js
app.use('/s', express.static('public'));
This instructs Node.js to serve all files placed in the
public
folder from the URL/s/...
. Runapp.js
and verify that your HTML form is served at the URL/s/form.html
-
Let’s serve the same form at a different URL. In
app.js
, create a handler for the/signin
URL, then use theres.sendFile()
method to serve the form.Note: the
sendFile()
method takes only one argument, the absolute path to the file. Since the absolute path to your project may change depending on what server you are running it on, and in particular you do not know how Glitch’s filesystem is set up, it is good practice to use the special__dirname
variable, like thus:res.sendFile(__dirname + '/public/file_name.html');
-
Create a second html page (complete and valid) containing the text “Hello!”. Serve this page from the
/hello
URL, usingsendFile()
. Modify the form at/signin
so that it is submitted to/hello
(use theaction
attribute).
Nunjucks Templates
HTML templates contain usual HTML code, with special syntax to specify the dynamic parts of the document.
Here’s a very simple one:
<p>Hello {{ name }} !</p>
The special marking {{ name }}
will be replaced by the value of the
name
variable passed to the template.
Every template language defines its own syntax, more or less
rich. Node has support for tons of template languages, to get an idea,
have a look at the list on the npm
server.
We are going to use a language, called Nunjucks, inspired from the
famous Django/Jinja templates of the Python web ecosystem. The full
reference on its syntax is available
here. In this
tutorial we will stick to variable evaluation (the example above),
if
and for
blocks.
-
You have already installed the Nunjucks module before by editing the
package.json
file. Now you must load it in your application withvar nunjucks = require('nunjucks');
Then configure it for express with
nunjucks.configure('views', { express: app, noCache: true });
Note: All your templates must go in a folder named
views
. You can change the default folder, if you wish, withapp.set('views', 'name_of_some_other_folder');
-
Rename your “Hello” page to
views/hello.html
. Modify it so that it contains this code:<p>Hello {{ name }} !</p>
-
In
app.js
, modify the handler for/hello
by replacingsendFile
withres.render('hello.html', { 'name' : 'Toto' });
Visit the
/hello
URL and see the result. -
Modify again the handler for
/hello
so that, instead of showing “Hello Toto”, it shows the name sent by the form at/signin
.Note: Remember you can access the contents of the query string via
req.query
. You can test directly your view by editing the query string in the address bar.
Here’s an example of a template using a for
loop:
<ul>
{% for en, fr in numbers %}
<li>{{ fr }} means {{ en }}</li>
{% endfor %}
</ul>
This template, evaluated with
res.render('for_loop.html', { 'numbers' : {
'One' : 'Un',
'Two' : 'Deux',
'Three' : 'Trois'
} });
produces the output
<ul>
<li>Un means One</li>
<li>Deux means Two</li>
<li>Trois means Three</li>
</ul>
For more for
loop examples, see
https://mozilla.github.io/nunjucks/templating.html#for.
-
Insert in your handler code the following lists:
['cherry', 'strawberry', 'blood'] ['sun', 'lemon', 'banana']
In the
/hello
template, using an{% if %}
and a{% for %}
block, output one list or another according to the color chosen in the form. -
By adding a
<style>
block in the head of your template, color the text in yellow or in red, according to the color chosen in the form.Hint: It is cleaner and safer to use a purely static CSS code in
<style>
(no templating), and to change the color dynamically by setting a CSS class on the part you want to color.
Persistence
We now move on to the various techniques for persisting the state. We are going to persist the name inserted in the form through many HTTP requests.
Persistence by the query string
We start with query string persistence.
-
Create a new view at the URL
/bye
showing the text “Bye bye, …“, where the dots are replaced by a name passed through the query string, similarly to what you have done in the/hello
view. -
With the help of the template engine, add a link to the
/hello
view pointing to/bye
, and persisting the name passed by the form through the query string.
Test the whole application, starting from /signin
and ending in
/bye
, and verify that the name is kept through each HTTP request.
Persistence by hidden fields
We now move on to persistence through the POST HTTP method. This technique can eventually be mixed with the previous one.
-
Modify
form.html
so that the data is sent through the POST method (method
attribute). -
Create a handler for POST requests at the
/hello
url. Show a message “Hello, …” as done previously. Test by submitting the form. -
Add to
/hello
a form containing a text field pre-filled with the user name, and a button for submitting. Let this form point to the/bye
url via the POST method. -
Create a handler for POST requests at the
/bye
url. Show a message “Bye bye, …” as done previously. Test the whole application from/signin
to/bye
. -
Modify the form in the
/hello
view so that the text field is hidden (type=hidden
). Test again.
Persistence by the URL
Last lightweight persistence technique: we keep the state in the url. This is made possible by the Express router. To make things more interesting, we will implement a counter.
-
Create a handler
app.get('/:name/counter/', ...);
Using a template, show the message “…, this is your first visit”, where dots are replaced by the value of the
:name
component of the URL. -
Create a handler for all URLs of the form
/:name/counter/:cnt
. The:cnt
component will be an integer for counting the number of visits. Show the text “…, you have visited this page … times”, where dots will be replaced by the values of the:name
and:cnt:
components.Note: you can restrict
:cnt
to take only integer values using a regular expression. -
Add a link inside the
/:name/counter/
view to/:name/counter/1
. -
Add a link to the
/:name/counter/:cnt
view to the same view, with the counter incremented by one.Warning: values passed through
req.query
,req.params
,req.body
orreq.cookies
are always strings. Use theparseInt
function to convert them to numbers.
If you implemented correctly the exercise, you now have a working counter incrementing by one unit at each click. You can apply the same technique to the query string or to hidden form fields. Try it.
-
Give a prize to the millionth visitor.
-
Without clicking a million times, win the prize.
Cookies and Storage API (optional)
We get to the last two techniques for client-side state keeping. Cookies are an extension of the HTTP protocol, they are controlled by the server and stored by the client. The storage API is a part of the (HTML5) DOM specification, entirely controlled by the client through JavaScript.
Cookies
Cookies are key-value pairs stored by the client. A Express handler
can ask the client to store a cookie by the using following command
before any res.send
or res.render
call.
res.cookie('user', 'toto');
Once a cookie is stored, it is sent by the client along with any
following request for the same domain name (whatever the URL
path). They are read in the object req.cookies
by the
cookie-parser
middleware.
-
Create a page
/cookie-monster
, counting the number of visits to it. The number of visits must be stored in a cookie on the client. If the cookie is not set (e.g., on the first visit), the handler will initialize it to 1. If it is already set, it will increment it and send it again to the client.No need to add a link from the view to itself: simply reload the page to see the number of visits go up.
-
Give a prize to the millionth visitor.
-
Win the prize (Hint: have a look in the dev tools).
Storage API
The storage API is handled entirely through JavaScript. This means that you can in principle use it on a purely static web page, without any Node server.
It is a simple JavaScript object, named localStorage
, that is kept
through page reloads and closing the browser.
-
Create a static page counting the number of visits through the
localStorage
API. Reload the page and verify that the counter is incremented. -
Give a prize to the millionth visitor.
-
Win the prize (Hint: use the JavaScript console).