Views

This tutorial is compatible with hapi v17

Overview

hapi has extensive support for template rendering, including the ability to load and leverage multiple templating engines, partials, helpers (functions used in templates to manipulate data), and layouts. These capabilities are provided by the vision plugin.

Vision

Vision is a templates rendering plugin for hapi.js. Vision decorates the server, request, and h response toolkit interfaces with additional methods for managing view engines that can be used to render templated responses. Vision also provides a built-in handler implementation for creating templated responses.

Vision is compatible with most major templating engines out of the box, such as ejs, handlebars, pug, twig, etc. Engines that don't follow the normal API pattern can still be used by mapping their API to the vision API.

For more info, please see here.

Configuring the server

To get started with views, first you have to configure at least one templating engine on the server. This is done by using the server.views() method provided by vision:

  1. 'use strict';
  2.  
  3. const Path = require('path');
  4. const Hapi = require('@hapi/hapi');
  5. const Hoek = require('@hapi/hoek');
  6.  
  7. const start = async () => {
  8.  
  9. const server = Hapi.server();
  10.  
  11. await server.register(require('@hapi/vision'));
  12.  
  13. server.views({
  14. engines: {
  15. html: require('handlebars')
  16. },
  17. relativeTo: __dirname,
  18. path: 'templates'
  19. });
  20. };
  21.  
  22. start();

You're doing several things here. First, you load the vision module as a plugin. It adds template rendering support to hapi.

Secondly, you register the handlebars module as the engine responsible for rendering templates with an extension of .html.

Next, you tell the server that your templates are located in the templates directory. You indicate that this directory should be taken relative to the current directory by providing a relativeTo option. By default, hapi will look for templates relative to the current working directory.

server.views() Options

There are many options available to the views engine in hapi. Full documentation can be found in the vision API reference, but we'll go over some of them here as well.

Note that all options may be set either globally, which configures them for all registered engines, or local to one specific engine, for example:

  1. server.views({
  2. engines: {
  3. html: {
  4. module: require('handlebars'),
  5. compileMode: 'sync' // engine specific
  6. }
  7. },
  8. compileMode: 'async' // global setting
  9. });

Engines

In order to use views in hapi, you must register at least one templating engine with the server. Templating engines may be either synchronous, or asynchronous, and should be an object with an export named compile.

The signature of the compile method for synchronous engines is function (template, options) and that method should return a function with the signature function (context, options) which should either throw an error, or return the compiled html.

Asynchronous template engines should have a compile method with the signature function (template, options, callback) which calls callback in standard error first format and returns a new method with the signature function (context, options, callback). The returned method should also call callback in error first format, with the compiled html being the second parameter.

By default, hapi assumes that template engines are synchronous (i.e. compileMode defaults to sync), to use an asynchronous engine you must set compileMode to async.

Leveraging the options parameter in both the compile method, and the method it returns, is done via the compileOptions and runtimeOptions settings. Both of these options default to the empty object {}.

compileOptions is the object passed as the second parameter to compile, while runtimeOptions is passed to the method that compile returns.

If only one templating engine is registered, it automatically becomes the default allowing you to leave off the file extension when requesting a view. However, if more than one engine is registered, you must either append the file extension, or set the defaultExtension to the engine you use most frequently. For any views that do not use the default engine, you'll still need to specify a file extension.

Another useful options is isCached. If set to false, hapi will not cache the result of templates and will instead read the template from file on each use. When developing your application, this can be very handy as it prevents you from having to restart your app while working on templates. It is recommended that you leave isCached to its default value of true in production, however.

Paths

Since views can have files in several different locations, hapi allows you to configure several paths to help find things. Those options are:

  • path: the directory that contains your main templates
  • partialsPath: the directory that contains your partials
  • helpersPath: the directory that contains your template helpers
  • layoutPath: the directory that contains layout templates
  • relativeTo: used as a prefix for other path types, if specified other paths can be relative to this directory Additionally, there are two settings that alter how hapi will allow you to use paths. By default, absolute paths and traversing outside of the path directory is not allowed. This behavior can be changed by setting the allowAbsolutePaths and allowInsecureAccess settings to true.

For example, if you have a directory structure like:

  1. templates\
  2. index.html
  3. layout\
  4. layout.html
  5. helpers\
  6. getUser.js
  7. fortune.js

Your configuration might look like:

  1. server.views({
  2. engines: {
  3. html: require('handlebars')
  4. },
  5. relativeTo: __dirname,
  6. path: './templates',
  7. layoutPath: './templates/layout',
  8. helpersPath: './templates/helpers'
  9. });

Global context

In the rendering a view tutorial, you saw how to pass context directly into a view, but what if you have some default context that should always be available on all templates?

The simplest way to achieve this is by using the context option when calling server.views():

  1. const context = {
  2. title: 'My personal site'
  3. };
  4.  
  5. server.views({
  6. engines: {
  7. html: {
  8. module: require('handlebars'),
  9. compileMode: 'sync' // engine specific
  10. }
  11. },
  12. context
  13. });

The default global context will be merged with any local context passed taking the lowest precedence and applied to your view.

View helpers

JavaScript modules located in the defined helpersPath are available in templates. For this example, you will create a view helper fortune which will pick and print out one element out of an array of strings, when used in a template.

The following snippet is the complete helper function which you will store in a file called fortune.js in the helpers directory.

  1. module.exports = function () {
  2.  
  3. const fortunes = [
  4. 'Heisenberg may have slept here...',
  5. 'Wanna buy a duck?',
  6. 'Say no, then negotiate.',
  7. 'Time and tide wait for no man.',
  8. 'To teach is to learn.',
  9. 'Never ask the barber if you need a haircut.',
  10. 'You will forget that you ever knew me.',
  11. 'You will be run over by a beer truck.',
  12. 'Fortune favors the lucky.',
  13. 'Have a nice day!'
  14. ];
  15.  
  16. const x = Math.floor(Math.random() * fortunes.length);
  17. return fortunes[x];
  18. };

Now you can use the view helper in your templates. Here's a code snippet that show's the helper function in templates/index.html using handlebars as a rendering engine:

  1. <h1>Your fortune</h1>
  2. <p>{{fortune}}</p>

Now when you start the server and point your browser to the route which uses your template (which uses your view helper), you should see a paragraph with a randomly selected fortune right below the header.

For reference, here is a complete server script that uses the fortune view helper method in a template.

  1. 'use strict';
  2.  
  3. const Hapi = require('@hapi/hapi');
  4.  
  5. const start = async () => {
  6.  
  7. const server = Hapi.server({ port: 8080 });
  8.  
  9. await server.register(require('@hapi/vision'));
  10.  
  11. server.views({
  12. engines: {
  13. html: require('handlebars')
  14. },
  15. relativeTo: __dirname,
  16. path: 'templates',
  17. helpersPath: 'helpers'
  18. });
  19.  
  20. server.route({
  21. method: 'GET',
  22. path: '/',
  23. handler: function (request, h) {
  24.  
  25. return h.view('index');
  26. }
  27. });
  28. };
  29.  
  30. start();

Layouts

Vision includes built-in support for view layouts. It comes disabled by default, because it may conflict with other layout systems that specific view engines may provide. We recommend choosing only one layout system.

In order to use the built-in layout system, first setup the view engine:

  1. server.views({
  2. // ...
  3. layout: true,
  4. layoutPath: 'templates/layout'
  5. });

This enables the built-in layouts and defines the default layout page to templates/layout/layout.html (or whatever other extension you're using).

Setup a content area in your layout.html:

  1. <html>
  2. <body>
  3. {{{content}}}
  4. </body>
  5. </html>

And your view should be just the content:

  1. <div>Content</div>

When rendering the view, the {{{content}}} will be replaced by the view contents.

If you want a different default layout, you can set the option globally:

  1. server.views({
  2. // ...
  3. layout: 'another_default'
  4. });

You can also specify a different layout per view:

  1. return h.view('myview', null, { layout: 'another_layout' });

Rendering a view

There are two options for rendering a view, you can use either the h.view() method, where h is the response toolkit or the view handler.

h.view()

The first method of rendering a view we'll look at is h.view(). Here's what a route using this method would look like:

  1. server.route({
  2. method: 'GET',
  3. path: '/',
  4. handler: function (request, h) {
  5.  
  6. return h.view('index');
  7. }
  8. });

In order to pass context to h.view(), you pass an object as the second parameter, for example:

  1. return h.view('index', { title: 'My home page' });

View handler

The second method of rendering a view, is using hapi's built in view handler. That route would look like:

  1. server.route({
  2. method: 'GET',
  3. path: '/',
  4. handler: {
  5. view: 'index'
  6. }
  7. });

When using the view handler, context is passed in the key context, for example:

  1. handler: {
  2. view: {
  3. template: 'index',
  4. context: {
  5. title: 'My home page'
  6. }
  7. }
  8. }