Templating in NodeJS using Express Pug View Engine
It is a common requirement to render dynamic content into our HTML page. Templating engines are a great way to support this feature. In this post, we will learn how to perform templating in NodeJS using Express Pug view engine.
In case you are new to Express, do check out this post on getting started with ExpressJS.
Nội Dung Chính
1 – What is a Templating Engine?
Templating engines help make our web applications dynamic in terms of data. At the heart of a templating engine, there is a HTML template with placeholders for dynamic data.
When a request is processed by our application, the templating engine kicks into action. Basically, the engine replaces the placeholders or snippets with actual HTML content. In other words, a templating engine performs on-the-fly generation of HTML that is sent to the client.
Some of the most popular templating engines are:
- EJS
- Pug (also known as Jade)
- Handlebars
Each of the engines has its own philosophy for defining the placeholders. In this post, we will look at Pug and how we can use it with NodeJS and Express.
2 – NodeJS Express Pug Installation
As a first step to using Pug in our Express app, we need to install the necessary packages.
To do so, we can execute the below command:
$ npm install express pug body-parser
Below is the package.json
for our example application.
{
"name": "nodejs-express-pug-sample",
"version": "1.0.0",
"description": "NodeJS Express Pug Sample",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon app.js",
"start-server": "node app.js"
},
"author": "Saurabh Dashora",
"license": "ISC",
"devDependencies": {
"nodemon": "^1.18.3"
},
"dependencies": {
"body-parser": "^1.18.3",
"express": "^4.16.3",
"pug": "^3.0.2"
}
}
As a templating engine, Pug uses minimal HTML. Instead, it leans towards a sort of custom template language.
Let us now look at building some templates using Pug.
3 – Creating NodeJS Express Pug Templates
For the purpose of our demo application, we will create a simple application that is going to show a list of products. Also, we will have a page to add products to our shop.
Let us first create the product list page template. We will keep our templates within our source code inside a special views
folder. The file name will be shop.pug
.
doctype html
html(lang="en")
head
meta(charset="UTF-8")
meta(http-equiv="X-UA-Compatible", content="IE=edge")
meta(name="viewport", content="width=device-width, initial-scale=1.0")
title #{pageTitle}
link(rel="stylesheet", href="/css/main.css")
link(rel="stylesheet", href="/css/product.css")
body
header.main-header
nav.main-header__nav
ul.main-header__item-list
li.main-header__item
a.active(href="/") Shop
li.main-header__item
a(href="/admin/add-product") Add Product
main
if prods.length > 0
.grid
each product in prods
article.card.product-item
header.card__header
h1.product_title #{product.title}
.card__image
img(src="https://cdn.pixabay.com/photo/2015/11/19/21/10/glasses-1052010__340.jpg" alt="Product Image")
.card__content
h2.product__price $9.99
p2.product_description The Best Book Ever Written
.card__actions
button.btn Add To Cart
else
h1 No Products
As you can see, the Pug template hardly looks like the familiar HTML syntax. Of course, we still use the common HTML tags to describe the page.
Indentation matters a lot in a Pug template. Therefore, we need to be mindful of indenting properly to communicate the hierarchy of our HTML structure.
Inside a pug template, we can also specify CSS classes as header.main-header
. Here, header
is the HTML tag and main-header
is the CSS class.
We can also declare expression in pug templates as if prods.length > 0
. Here, prods
contains the list of the products in our store. Also, we can define a placeholder for dynamic piece of data in our HTML using the #{product.title}
. At the time of request execution, the Pug templating engine will replace the placeholder with actual data.
Similarly, we can define a pug template for adding products.
html(lang="en")
head
meta(charset="UTF-8")
meta(http-equiv="X-UA-Compatible", content="IE=edge")
meta(name="viewport", content="width=device-width, initial-scale=1.0")
title Add Product
link(rel="stylesheet", href="/css/main.css")
link(rel="stylesheet", href="/css/forms.css")
link(rel="stylesheet", href="/css/product.css")
body
header.main-header
nav.main-header__nav
ul.main-header__item-list
li.main-header__item
a(href="/") Shop
li.main-header__item
a.active(href="/admin/add-product") Add Product
main
form.product-form(action="/admin/add-product", method="POST")
.form-control
label(for="title") Title
input(type="text", name="title", id="title")
button.btn(type="submit") Add Product
Here, we have a similar structure for the header and navigation bars. However, instead of a list of products, here we are rendering a form with a single input field for product title.
4 – Creating the NodeJS Express Routes
Now that our templates our ready, we need to write the appropriate logic to serve these templates.
To do so, we will first create the app.js
file for our application. Basically, this is the starting point of our app.
const path = require('path');
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.set('view engine', 'pug');
app.set('views', 'views');
const adminData = require('./routes/admin');
const shopRoutes = require('./routes/shop');
app.use(bodyParser.urlencoded({extended: false}));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/admin', adminData.routes);
app.use(shopRoutes);
app.use((req, res, next) => {
res.render('404', {pageTitle: "Page Not Found"})
});
app.listen(3000);
The most important thing here is the configuration of pug as our view engine. We do so by using the app.set()
function. Basically, the app.set()
function is used to set any value globally for our application. In this case, we use it to set the view engine
property to pug
.
Also, we set the views
property to views
(basically the name of the folder where our view files are located). Incidentally, the default views folder is also views
. Hence, we could have got away without setting it explicitly. However, if we decide to use a different name for the folder, we need to set it using app.set()
.
Next, we will create router for the shop related routes.
const path = require('path');
const express = require('express');
const adminData = require('./admin');
const router = express.Router();
router.get('/', (req, res, next) => {
const products = adminData.products;
res.render('shop', { prods: products, pageTitle: 'Shop' });
});
module.exports = router;
The main thing to note in this file is the call to res.render()
function. The function takes the name of the template (in this case, shop). The second attribute is an object that contains the prods
property and the pageTitle
property. These properties will be made available within the Pug template.
The second router is for the admin pages (or adding a product).
const path = require('path');
const express = require('express');
const router = express.Router();
const products = [];
router.get('/add-product', (req, res, next) => {
res.render('add-product', {pageTitle: 'Add Product'});
});
router.post('/add-product', (req, res, next) => {
products.push({ title: req.body.title });
res.redirect('/');
});
exports.routes = router;
exports.products = products;
Here, we have a couple of routes. One of them is a GET method handler for displaying the Add Product page. In this case, we render the add-product
template.
Second is the POST method handler for adding a new product. Whenever user submits the form, we push a new product to the products
array.
If interested, you can read more about routers in this post on Express Routers.
5 – NodeJS Express Pug Reusable Layout
In the above code, you may have noticed that we are repeating a bunch of template code. Basically, the navigation bar is present in both the templates even though it is practically the same. Also, we have some common CSS imports. It might benefit the maintainability of our code if we can remove this duplication of template code.
We can do so by using Pug reusable layout feature. To use this feature, we will create another folder layouts
within the views
folder. Inside the folder, we will create a special template file known as main-layout.pug
.
html(lang="en")
head
meta(charset="UTF-8")
meta(http-equiv="X-UA-Compatible", content="IE=edge")
meta(name="viewport", content="width=device-width, initial-scale=1.0")
title #{pageTitle}
link(rel="stylesheet", href="/css/main.css")
block styles
body
header.main-header
nav.main-header__nav
ul.main-header__item-list
li.main-header__item
a(href="/", class=(path === '/' ? 'active' : '')) Shop
li.main-header__item
a(href="/admin/add-product", class=(path === '/admin/add-product' ? 'active' : '')) Add Product
block content
This is largely the same as the other templates except for the presence of special block
sections. We have block styles
and block content
. The block
keyword is an indication that the actual content for will come from some other template.
Also, since we now have a common navigation bar, we need to dynamically determine the active navigation item. For this purpose, we have some special logic while applying the class
property using the incoming path
property.
We can now use the main-layout
reusable template in the shop
and add-product
templates.
extends layouts/main-layout.pug
block styles
link(rel="stylesheet", href="/css/product.css")
block content
main
if prods.length > 0
.grid
each product in prods
article.card.product-item
header.card__header
h1.product_title #{product.title}
.card__image
img(src="https://cdn.pixabay.com/photo/2015/11/19/21/10/glasses-1052010__340.jpg" alt="Product Image")
.card__content
h2.product__price $9.99
p2.product_description The Best Book Ever Written
.card__actions
button.btn Add To Cart
else
h1 No Products
As you can see, here we extend the main-layout.pug
using the extends
keyword. Also, we define the actual content for block sections. The block sections are designated by their respective names.
Below is the revised code for the add-product.pug
template file.
extends layouts/main-layout.pug
block styles
link(rel="stylesheet", href="/css/forms.css")
link(rel="stylesheet", href="/css/product.css")
block content
main
form.product-form(action="/admin/add-product", method="POST")
.form-control
label(for="title") Title
input(type="text", name="title", id="title")
button.btn(type="submit") Add Product
To support the updated pug templates, we also need to pass the additional path
property in our router files. This property will help determine which navigation item should get the active
CSS class.
router.get('/', (req, res, next) => {
const products = adminData.products;
res.render('shop', { prods: products, pageTitle: 'Shop', path: '/' });
});
router.get('/add-product', (req, res, next) => {
res.render('add-product', {pageTitle: 'Add Product', path: '/admin/add-product'});
});
Conclusion
With this, we have successfully learnt how to perform templating in NodeJS using Express Pug view engine. We also looked at creating reusable pug templates to reduce duplication of code within our templates.
The code for the above sample application is available on Github. All the CSS files and classes used in this application are present in the Github repo for reference.
Want to use a different templating engine? Check out this post on NodeJS templating with Express Handlebars.
If you have any comments or queries about this post, please feel free to mention them in the comments section below.