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

Static content and templates

In the previous tutorial, we used two different techniques to create pages:

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

  1. Create a form.html file, containing a form like thus

    Choose a color

    red
    yellow

    You 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 the method and action attribute on the form, for the moment.

  2. Create a file named app.js, and initialize it with the following code:

    const express = require('express');
    const bodyP = require('body-parser');
    const cookieP = require('cookie-parser');
       
    const app = express();
    app
        .use(bodyP.urlencoded({ extended: false }))
        .use(cookieP());
       
    // Your handlers go here
       
       
    app.listen(process.env.PORT);
    
  3. Glitch will not run the code in app.js by default. For this, you must edit the package.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 the dependencies section of package.json and install them automatically with npm.

    Install the dependencies needed for this tutorial: body-parser, cookie-parser, consolidate and nunjucks.

    Note: If you’re working on your local machine, you can create a package.json file by running the npm init command in a shell. Then you can run your project with the npm start command.

    To install packages, directly edit the dependencies section of your package.json file, then run the npm install command in a shell. When you modify the dependencies section, you must run npm install again.

  4. Rename your file form.html to public/form.html. Add the following line to app.js

    app.use('/s', express.static('public'));
    

    This instructs Node.js to serve all files placed in the public folder from the URL /s/.... Run app.js and verify that your HTML form is served at the URL

    /s/form.html
    
  5. Let’s serve the same form at a different URL. In app.js, create a handler for the /signin URL, then use the res.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');
    
  6. Create a second html page (complete and valid) containing the text “Hello!”. Serve this page from the /hello URL, using sendFile(). Modify the form at /signin so that it is submitted to /hello (use the action 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.

  1. You have already installed the Nunjucks module before by editing the package.json file. Now you must load it in your application with

    const consolidate = require('consolidate');
    app.engine('html', consolidate.nunjucks);
    app.set('view engine', 'nunjucks');
    

    Note: All your templates must go in a folder named views. You can change the default folder, if you wish, with

    app.set('views', 'name_of_some_other_folder');
    
  2. Rename your “Hello” page to views/hello.html. Modify it so that it contains this code:

    <p>Hello {{ name }} !</p>
    
  3. In app.js, modify the handler for /hello by replacing sendFile with

    res.render('hello.html', { 'name' : 'Toto' });
    

    Visit the /hello URL and see the result.

  4. 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.

  1. 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.

  2. 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.

  1. 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.

  2. 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.

  1. Modify form.html so that the data is sent through the POST method (method attribute).

  2. Create a handler for POST requests at the /hello url. Show a message “Hello, …” as done previously. Test by submitting the form.

  3. 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.

  4. 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.

  5. 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.

  1. 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.

  2. 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.

  3. Add a link inside the /:name/counter/ view to /:name/counter/1.

  4. 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 or req.cookies are always strings. Use the parseInt 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.

  1. Give a prize to the millionth visitor.

  2. 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.

  1. 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.

  2. Give a prize to the millionth visitor.

  3. 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.

  1. Create a static page counting the number of visits through the localStorage API. Reload the page and verify that the counter is incremented.

  2. Give a prize to the millionth visitor.

  3. Win the prize (Hint: use the JavaScript console).

Fork me on GitHub