# Views & Templates

19 min read (3618 words)

# Table of contents

# Introduction

Views are part of the MVC architecture (opens new window) and responsible for displaying data to the user. They can also be described as the presentation layer of the MVC architecture. In ZenTS views are created using templates. A template is the best way to render and organize HTML code inside your application. It usually contains HTML code and some logic on how to render the HTML properly. Templates in ZenTS applications are written with Nunjucks (opens new window), a powerful, fast and extensible template engine for Node.js. Nunjucks is build into ZenTS, so you don't need to install it by yourself. Of course, you're still free to use frontend-frameworks like Vue.js (opens new window), React (opens new window) or AngularJS (opens new window). In fact, they are a perfect pair for a ZenTS application (which is then used as the "backend" part of an application). Continue reading this guide and learn the basics of Nunjucks and how to render templates in your application. This guide will cover the most important things on how to use and extend the template engine, but it's always a good idea to take a look at the official documentation (opens new window) to learn more about supported tags (opens new window) and build-in filters (opens new window).

# Template language

Nunjucks allows you to write easy to understand and maintainable templates. Lets take a look at the following example, if you used similar template engines (like Twig (opens new window) or Jinja (opens new window)) before, then you'll be already familiar with the Nunjucks syntax:

<!DOCTYPE html>
<html>
    <head>
        <title>Welcome to ZenTS!</title>
    </head>
    <body>
        <h1>{{ title }}</h1>
        Hello {{ username }}!
    </body>
</html>
1
2
3
4
5
6
7
8
9
10

The above template code is pretty straightforward. In Nunjucks, variables are displayed using the {{ ... }} syntax. On the other hand, {% ... %} is used to run some logic, such as conditions (if-else) or loops (for).

You can't run TypeScript / JavaScript (backend) code inside your template code, but its possible to run math expressions and comparisons inside a template.

Nunjucks also supplies a lot of builtin filters, which are essential function that modifies an input value somehow. Filters are applied using the pipe (| operator):

{{ [1,2,3,4,5,6,7,8,9]|random }}
1

The above filter will return a random number (between 1-9) every time when its rendered. In ZenTS, you can also extend the template engine easily and write your own filters.

# Configuration

Nunjucks supports a wide range of configuration options, for example the syntax on how to display variables ({{ ... }}) can be changed to something else you prefer. Read the configuration guide to learn more about them.

# Template file extension

Templates are placed in the src/view folder of your application. A nunjucks template can, according to the official documentation, have any file extension. However, the community has adopted to use .njk as file extension. This is also the default file extension used in ZenTS applications. Furthermore ZenTS limits the file extension to one of the following: .njk, .nunjucks, .nunjs, .nj, .html, .htm, .template, .tmpl or .tpl.

If you want to use a different file extension then the default .njk, you've to configure this in your projects config file. Keep in mind, that only one extension is allowed and that they can't be mixed.

# Directory structure

When your application grows, it's often a good advice to put your template files in sub-folders, where each sub-folder marks a logical module of the application. For example, if you're writing a e-commerce application you maybe have templates to display, edit and create products and categories. Then the directory structure might look like this:

|-- src
    |-- view
        |-- navigation
        |   |-- create.njk
        |   |-- edit.njk
        |   |-- view.njk
        |-- product
            |-- create.njk
            |-- edit.njk
            |-- view.njk
1
2
3
4
5
6
7
8
9
10

Of course, you're free to create a directory structure that fits your needs. Templates can be even further nested in sub-directories or just keep in the src/view root directory.

TIP

Keep in mind, that the default template directory src/view can be changed in the configuration.

# Rendering templates in a controller

In order to render a template in a controller, you've to use the render() method attached to each controller:

import {  Controller, get } from 'zents'

export default extends Controller {
  @get('/')
  public async index() {
    return await this.render('index')
  }
}
1
2
3
4
5
6
7
8

The above code will render the template located at src/view/index.njk. The render() method returns a Promise, which will be resolved with a so-called TemplateResponse. This makes it a perfect fit to return it as a response value for a controller action. If you need to access the rendered HTML code, you can do so by using the html property of a TemplateResponse.

When we go back to our previous example...

<!DOCTYPE html>
<html>
    <head>
        <title>Welcome to ZenTS!</title>
    </head>
    <body>
        <h1>{{ title }}</h1>
        Hello {{ username }}!
    </body>
</html>
1
2
3
4
5
6
7
8
9
10

... we see that this template uses two variables ({{ title }} and {{ username }}). These are part of the template context and need to be passed in when rendering the template. This is done by supplying a second argument to the render() method:

import {  Controller, get } from 'zents'

export default extends Controller {
  @get('/')
  public async index() {
    return await this.render('index', {
        title: 'My ZenTS web-application',
        username: 'John Doe',
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11

This will result in the following HTML being rendered:

<!DOCTYPE html>
<html>
  <head>
    <title>Welcome to ZenTS!</title>
  </head>
  <body>
    <h1>My ZenTS web-application</h1>
    Hello John Doe!
  </body>
</html>
1
2
3
4
5
6
7
8
9
10

As said before, templates can be placed in various sub-folders. To render them, you've to call render() with the corresponding sub-folder structure:

this.render('path/to/template')
1

In all cases, the file extension (.njk) is always omitted.

# Assign variables

Using the set keyword you can assign and modify variables directly in your template:

{% set username = 'zen' %}
{{ username }}
1
2

It's also possible to overwrite variables this way, which have been previously passed in by the render() method.

Furthermore it's possible to capture the content of a so-called block into a single variable, allowing for more complex assigning variables:

{% set mood %}
    {% if hungry %}
        I am hungry
    {% elseif tired %}
        I am tired
    {% else %}
        I am good!
    {% endif %}
{% endset %}

{{ mood }}
1
2
3
4
5
6
7
8
9
10
11

WARNING

While it's allowed to set a variable this way, you should always make sure to pass in the complete data via render() method and use {% set ... %} sparsely.

# Conditional statements (if-elseif-else)

A Conditional statement tests if a condition evaluates to true and displays the content defined in the condition body:

{% if variable %}
  It is true
{% endif %}
1
2
3

In the above example, if variable is defined and evaluates to true, It is true will be displayed. Alternative conditions can be specified using elseif and/or else:

{% if hungry %}
  I am hungry
{% elseif tired %}
  I am tired
{% else %}
  I am good!
{% endif %}
1
2
3
4
5
6
7

Multiple conditions can be specified with the and and or keywords:

{% if happy and hungry %}
  I am happy *and* hungry; both are true.
{% endif %}

{% if happy or hungry %}
  I am either happy *or* hungry; one or the other is true.
{% endif %}
1
2
3
4
5
6
7

# For loops

For loops allow you to iterate over arrays, objects, Maps and Sets (and basically everything that implements the iterable protocol):

# Loop over object

The syntax is quiet similar when you want to loop over a object:

# Loop over Map

The next example shows how to loop over a Map:

# Special variables

Inside a loop, you've access to the following variables:

  • loop.index: the current iteration of the loop (1 indexed)
  • loop.index0: the current iteration of the loop (0 indexed)
  • loop.revindex: number of iterations until the end (1 indexed)
  • loop.revindex0: number of iterations until the end (0 based)
  • loop.first: boolean indicating the first iteration
  • loop.last: boolean indicating the last iteration
  • loop.length: total number of items

They are useful when you want to gain more fine control on how a template is rendered.

# Builtin filters

Nunjucks has a lot of useful filters builtin, which can be applied using the pipe (|) operator or a filter block:

It's also possible to chain multiple filters:

{{ ['FOO', 'BAR', 'BAZ']|first|capitalize }}
1

TIP

The following chapters list a few of the important builtin filters. The full list of builtin filters can be found in the official documentation (opens new window).

# capitalize

Converts the first character to uppercase and the rest to lowercase.

# dump

Useful for debugging complexer objects or arrays. Calls JSON.stringify() on the object / array and dumps the result into the rendered HTML code:

# escape

Converts the characters &, <, >, ', and " into their HTML representations.

# first

Returns the first element in an array or the first character of a string.

# float

Converts the value into a floating point number. If the conversation fails (e.g. a string like abc is used as value), 0.0 will be returned (this can be overwritten by providing a default value as first argument, e.g. value|float(1.0)).

# int

Converts the value into a integer. If the conversation fails (e.g. a string like abc is used as value), 0 will be returned (this can be overwritten by providing a default value as first argument, e.g. value|int(1)).

# join

join returns a new string by concatenating all of the elements in an array.

# last

Returns the last element in an array or the last character of a string.

# length

Returns the length of an array, a string or the numbers of keys in an object.

# lower

Converts a string to lowercase.

# nl2br

Replaces new lines with <br /> elements. This can be useful when displaying data that has been entered in a <textarea>.

# random

Returns a random value from an array.

# replace

Replace a entry with a different entry. The first argument is the search part, the second argument is the value which replaces search.

# safe

Marks the value as safe, which means that this value won't be escaped. This can be dangerous when using with user input values.

# trim

Strips leading and trailing whitespace from a string.

# upper

Converts a string to uppercase.

# and more...

Don't forget that nunjucks has many more builtin filters. Checkout the official documentation here (opens new window).

# Custom filters

Nunjucks supports custom filters and ZenTS makes it super easy to create them. Custom filters are registered in the src/template/filter directory (the base directory src/template/ can be changed in the projects configuration file, but the filter sub-folder is mandatory). Each .ts file created in this folder will be considered as a filter for the template engine and are auto-loaded when the application starts.

# Creating a custom filter

# Registering custom filter

As said before, custom filters are auto-loaded by ZenTS and must follow the same naming conventions then a controller. A filter is a standard ECMAScript class with a public run() method.

In order for the AutoLoader to register your custom filter probably, the class has to use either a default export or exporting a member with the same property then the filename.

In both cases the filter will be registered as myfilter and is available as {{ title|myfilter }} in the template code.

Sometimes using the filename (convert to lowercase) as the key for a custom filter isn't sufficient, for example when you want to use camelCase for your filters. In that case you can add a static member filtername to your exported class:

// src/template/filter/MyFilter.ts
export default class {
  static filtername = 'customFilter'
  // implementation...
}
1
2
3
4
5

The filter is then registered as customFilter and can be used with {{ title|customFilter }} in templates.

# A example filter

Lets create our first custom filter. It will format a number to a given currency format (default en-US) and optionally displays the correct currency sign:

// src/template/filter/CurrencyFormat.ts
export default class {
  static filtername = 'currency'
  public run(val: number | string, locale: string = 'en-US', currency: string = null) {
    const opts =
      typeof currency === 'string'
        ? {
            style: 'currency',
            currency,
          }
        : undefined

    return new Intl.NumberFormat(locale, opts).format(val)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

This filter returns a string which is formatted using the Intl.NumberFormat().format() (opens new window) method and it takes two optional arguments, locale and currency.

Using this filter in templates is simple:

Using a different locale:

Displaying currency sign:

# Async filter

It's possible to use async / await in a custom filter run() method. In order for the AutoLoader to understand that the filter is async, we need to attach a static async member to the filter class:



 

 




// src/template/filter/MyAsyncFilter.ts
export default class {
  static async = true

  public async run(val) {
    // implementation can use await now
  }
}
1
2
3
4
5
6
7
8

WARNING

You've to use the asyncEach tag when you want to use a custom async filter in loops. Using the for tag will fail to return the correct value from a filter, because its Promise won't be resolved until then. In general, it isn't a good idea to use async filters at all, because they produce a lot of overhead. Better construct the correct data in the controller before passing it into the template.

# Extending templates

Nunjucks has a powerful template inheritance system which allows the developer to organize and reuse templates efficient. This is done by defining so-called blocks that child templates can override or extend.

Lets start with a simple example. In a web application you might have a layout template, which is the skeleton of the website (e.g. with a header, navigation and a footer) and other templates extend the layout.

First we create our base layout template and save it to src/view/layout/default.njk:

<!doctype html>
<html>
<head>
  <title>{% block title %}My Web Application{% endblock %}</title>
</head>
<body>
  <nav>
    <ul>
      <li>Home</li>
      <li>Products</li>
      <li>About us</li>
    </ul>
  </nav>
  <div class="content">
    {% block content %}Default Content{% endblock %}
  </div>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

This template is pretty straightforward. It has a <title> with a default text, which is shown in the browser title bar, a simple navigation and a content-block.

A template that extends our default layout has now the ability to change or extend the content of our two blocks (title and content). The navigation (<nav>) on the other hand can't be changed in templates extending the default layout template, because it isn't defined as block.

So lets create a new template in src/view/startpage.njk that extends our base layout template:

{% extends 'layout/default' %}

{% block title %}{{ super() }} - Startpage{% endblock %}

{% block content %}
Welcome to my web application.
{% endblock %}
1
2
3
4
5
6
7

Rendering the src/view/startpage.njk will result in the following HTML output:

<!DOCTYPE html>
<html>
  <head>
    <title>My Web Application - Startpage</title>
  </head>
  <body>
    <nav>
      <ul>
        <li>Home</li>
        <li>Products</li>
        <li>About us</li>
      </ul>
    </nav>
    <div class="content">
      Welcome to my web application.
    </div>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

What is happening in our startpage.njk template code?

  • line 1: We extend src/view/layout/default.njk with our template code here. The meaning is, we take the HTML of the default template and inject it into our startpage.njk template. Please note that you have to supply the path to the template relative to the src/view directory and that the file extension (.njk) is omitted.
  • line 3: Here we extend our title-block with a appended text. This is done by calling {{ super() }} first in the block declaration. This will keep the original text defined in our default layout template and appends the text / code we defined in our startpage.njk template.
  • line 5-7: In this block, we define the text for the content block. This time we don't call {{ super() }}, thus the original text will be completely replaced by the block defined in the startpage.njk template.

Another great feature of Nunjucks is including a template. The include-tag pulls in other templates in place. That can be useful when you need to share smaller chunks of code across serval templates:

WARNING

If your included template has some async code (e.g. a async filter), you need to use asyncEach for looping and including a template inside the loop. Please take a look at the nunjucks documentation (opens new window) for more information.

# Next steps

It's time to celebrate, when you've followed this guide until here, you've learned the most important parts of a MVC (Model-View-Controller) framework and are one your path to ZenTS mastery.

Now it's time to learn some more about how to handle static files in a ZenTS application. Then you'll learn more about dependency injection in ZenTS and how to use services.