Hapi has great view support out of the box and ships with handlebars support by default. The framework offers multiple options to configure handlebars the way it fits perfectly into your project structure. Within earlier posts, we already guided you through the creation of a dynamic layout template and partial view support. This time, we use the helpersPath
option and create our own custom handlebars helpers for views rendered by hapi.
hapi Series Overview
- What You’ll Build
- Prepare Your Project: Stack & Structure
- Environment Variables and Storing Secrets
- Set Up MongoDB and Connect With Mongoose
- Sending Emails in Node.js
- Load the User’s Profile Picture From Gravatar Using Virtuals in Mongoose
- Implement a User Profile Editing Screen
- Generate a Username in Mongoose Middleware
- Displaying Seasons and Episodes for TV Shows with Mongoose Relationship Population
- Implementing Pagination for Movies
- Implement a Watchlist
- Create a Full Text Search with MongoDB
- Create a REST API with JSON Endpoints
- Update Mongoose Models for JSON Responses
- API Pagination for TV Shows
- Customize API Endpoints with Query Parameters
- Always Throw and Handle API Validation Errors
- Advanced API Validation With Custom Errors
- Create an API Documentation with Swagger
- Customize Your Swagger API Documentation URL
- Describe Endpoint Details in Your Swagger API Documentation
- 10 Tips on API Testing With Postman
- JWT Authentication in Swagger API Documentation
- API Versioning with Request Headers
- API Login With Username and Password to Generate a JWT
- JWT Authentication and Private API Endpoints
- Refresh Tokens With JWT Authentication
- Create a JWT Utility
- JWT Refresh Token for Multiple Devices
- Check Refresh Token in Authentication Strategy
- Rate Limit Your Refresh Token API Endpoint
- How to Revoke a JWT
- Invalidate JWTs With Blacklists
- JWT Logout (Part 1/2)
- JWT “Immediate” Logout (Part 2/2)
- A Better Place to Invalidate Tokens
- How to Switch the JWT Signing Algorithm
- Roll Your Own Refresh Token Authentication Scheme
- JWT Claims 101
- Use JWT With Asymmetric Signatures (RS256 & Co.)
- Encrypt the JWT Payload (The Simple Way)
- Increase JWT Security Beyond the Signature
- Unsigned JSON Web Tokens (Unsecured JWS)
- JWK and JWKS Overview
- Provide a JWKS API Endpoint
- Create a JWK from a Shared Secret
- JWT Verification via JWKS API Endpoint
- What is JOSE in JWT
- Encrypt a JWT (the JWE Way)
- Authenticate Encrypted JWTs (JWE)
- Encrypted and Signed JWT (Nested JWT)
- Bringing Back JWT Decoding and Authentication
- Bringing Back JWT Claims in the JWT Payload
- Basic Authentication With Username and Password
- Authentication and Remember Me Using Cookies
- How to Set a Default Authentication Strategy
- Define Multiple Authentication Strategies for a Route
- Restrict User Access With Scopes
- Show „Insufficient Scope“ View for Routes With Restricted Access
- Access Restriction With Dynamic and Advanced Scopes
- hapi - How to Fix „unknown authentication strategy“
- Authenticate with GitHub And Remember the Login
- Authenticate with GitLab And Remember the User
- How to Combine Bell With Another Authentication Strategy
- Custom OAuth Bell Strategy to Connect With any Server
- Redirect to Previous Page After Login
- How to Implement a Complete Sign Up Flow With Email and Password
- How to Implement a Complete Login Flow
- Implement a Password-Reset Flow
- Views in hapi 9 (and above)
- How to Render and Reply Views
- How to Reply and Render Pug Views (Using Pug 2.0)
- How to Create a Dynamic Handlebars Layout Template
- Create and Use Handlebars Partial Views
- Create and Use Custom Handlebars Helpers
- Specify a Different Handlebars Layout for a Specific View
- How to Create Jade-Like Layout Blocks in Handlebars
- Use Vue.js Mustache Tags in Handlebars Templates
- How to Use Multiple Handlebars Layouts
- How to Access and Handle Request Payload
- Access Request Headers
- How to Manage Cookies and HTTP States Across Requests
- Detect and Get the Client IP Address
- How to Upload Files
- Quick Access to Logged In User in Route Handlers
- How to Fix “handler method did not return a value, a promise, or throw an error”
- How to Fix “X must return an error, a takeover response, or a continue signal”
- Query Parameter Validation With Joi
- Path Parameter Validation With Joi
- Request Payload Validation With Joi
- Validate Query and Path Parameters, Payload and Headers All at Once on Your Routes
- Validate Request Headers With Joi
- Reply Custom View for Failed Validations
- Handle Failed Validations and Show Errors Details at Inputs
- How to Fix AssertionError, Cannot validate HEAD or GET requests
File Structure
If you read the related posts, you will recognize the file structure used for this tutorial since we already used it for the previous ones. We place all our template files (ending on .html
) into the views
directory. Additionally, to keep the helpers close to the views, we put them in a separated helpers
folder inside the views
directory.
Besides the views, we have a single file server.js
which describes the minimum functionality required to make this example work properly.
The following overview outlines the the file structure:
views/
helpers/
isSparta.js
isAdmin.js
partials/
layout/
default.html
index.html
server.js
Our helper files have their own directory called helpers
and are javascript files ending on .js
. The default
layout is located within the layout
folder. The index.html
is a normal view showing information to the user :)
Hapi Server View Configuration
As with dynamic layouts and partial views, we use the same server code for this example about custom helpers with hapi. The important part is the view configuration for the server. This time, we define the helpersPath
property to let hapi know where to find our custom handlebars helpers.
const Hapi = require('hapi')
// Create hapi server instance
const server = new Hapi.Server({
host: 'localhost',
port: 3000
})
async function liftOff() {
await server.register({
plugin: require('vision') // add template rendering support in hapi
})
server.views({
engines: {
html: require('handlebars')
},
path: 'views',
layoutPath: 'views/layout',
layout: 'default',
partialsPath: 'views/partials'
helpersPath: 'views/helpers',
})
// create your routes
server.route({
method: 'GET',
path: '/',
handler: (request, h) => {
// Render the view with the custom greeting
const data = {
title: 'This is Index!',
message: 'Hello, World. You crazy handlebars layout',
user: { role: 'user' }
}
return h.view('index', data)
}
})
try {
await server.start()
// Log to the console the host and port info
console.log('Server started at: ' + server.info.uri)
} catch (err) {
console.log(err)
process.exit(1)
}
}
liftOff()
The helpersPath
property expects the directory path to your helper files. Hapi does not support sub-folders within the helpers
directory. That’s why you need to put all your helpers into the defined directory.
Note: keep an eye on the path definition. The example above uses a relative path to the helper folder instead of an absolute path. Starting the node server from another location than the root folder of this example project will result in wrong paths. Node offers options like __dirname
and the path
module to create more robust paths.
Create Your Helper
Before we create a custom helper, we need to set you in the right context. The important thing to understand: the helpers we’re going to create extend the handlebars template engine, not hapi itself. Helpers can only be used in handlebars view templates, not as functions in your hapi server code.
Helpers are functions used within view templates. They can perform data manipulations and transformations using the template context. The name of each file within the helpers
directory is used as the helper name.
Helpers must export a single method with signature function(context)
and return a string value. Passing data to a helper extends the signature to function(options, context)
. We explain both method declarations in more detail in the following subsections.
Helper Definition: function(context)
Hapi expects a single function with signature function(context)
for each helper. The following code snippet illustrates the isAdmin
helper function which can be used to check whether a user has an admin role assigned.
// isAdmin Helper
// Usage: `{{#isAdmin}}`
// Checks whether the user has admin or owner role
const _ = require('lodash')
function isAdmin(context) {
if (_.isEmpty(context)) {
return context.inverse(this)
}
const user = context.data.root.user
if (_.includes([ user.role ], 'owner', 'admin')) {
return context.fn(this)
}
return context.inverse(this)
}
module.exports = isAdmin
Data passed to a view in hapi is available within the helper’s context object. Hapi saves the data in the context.data.root
object. Let’s dive into the code and have a look at each part.
Actually, the context
object shouldn’t be empty, null
or undefined
. We just make sure you won’t run into type errors because of undefined is not a function
. The user
object within this helper is available because we passed data to the view within the handler
method of the route definition. Further, we use lodash to verify whether the user has the admin
or owner
role assigned.
If the user is verified as admin or owner, we call return context.fn(this)
. The method encapsulates the view content while calling the helper. Use the isAdmin
helper in your view with block definition and add further HTML markup which will be rendered if the helper returns true. The usage section provides a concrete example.
If the user doesn’t have admin access, the helper returns with return context.inverse(this)
. Your view will be transformed and the part between your isAdmin
block won’t be rendered.
Helper Options: function(options, context)
Handlebars offers the ability to pass additional options to your helper function. Hapi supports this functionality as well and passes additional options
from your view to your helper. The following code example describes the usage of options within your helper function.
// isSparta helper — for demonstration purposes
var _ = require('lodash'),
isSparta;
isSparta = function (options, context) {
options = options || {};
if (_.isEqual(options, 'sparta')) {
return 'This is sparta!';
}
return 'No sparta today';
};
module.exports = isSparta;
The next section shows the usage of the isSparta
helper within a view. You pass options
from a view to the helper by separating the helper call and its options with a space: {{isSparta 'sparta'}}
. The isSparta
helper returns a string which will be rendered in replace of your helper call within your view.
Helper With Multiple Options: function(option1, …, optionN, context)
You can pass multiple options from your view to the helper function. Hapi maps each option to the helpers signature which results in a separate variable for each option value.
Changing the isSparta
helper from above to a helper with three options, changes the helper signature to: function(option1, option2, option3, context)
.
Helper Usage
Enough theory. Let’s look at code examples to get a better understanding which transformations can be made using handlebars helper functions.
The following snippet shows the usage of the isAdmin
helper. The block definition starting with {{#isAdmin}}
encapsulates additional HTML markup which is only rendered if the helper function returns with return context.fn(this)
.
<a href='/navigation/link1'>Navigation Link 1</a>
<a href='/navigation/link2'>Navigation Link 2</a>
{{#isAdmin}}
{{! show admin navigation}}
<h2>Admin Navigation</h2>
<a href='/super/secret/admin/link>Secret Link</a>
{{/isAdmin}}
Each block helper in handlebars ends with a slash followed by the helper name. In this case we need to end the encapsulating markup with {{/isAdmin}}
to let handlebars know what will be rendered in case the helper returns successfully.
The example below illustrates the usage of options within handlebars helper functions. This time, we don’t define a block helper. Instead, we pass an additional option value to the helper. The helper returns a string value which replaces the helper definition within the view.
<h1>{{title}}</h1>
<p>{{message}}</p>
{{! call test helper with option value}}
<p>{{isSparta 'sparta'}}</p>
<p>{{isSparta 'option'}}</p>
Using the isSparta
helper from above, the rendered HTML of the view depends on the passed option value.
<h1>{{title}}</h1>
<p>{{message}}</p>
<p>This is sparta!</p>
<p>No sparta today</p>
Both helper definitions within the view are replaced by the respective string returned from isSparta
helper.
Please use the comments below or give us a shot @futurestud_io and let us know about any issues you’re having with handlebars helper functions in hapi.