Templating
« Return to Request Handling Chapter
Introduction to Templating
Templating is a powerful technique for generating dynamic HTML content on the server side. Instead of manually concatenating HTML strings or using complex template literals, templating engines provide a clean, organized way to create reusable HTML templates with dynamic data.
In this chapter, we'll explore:
- Basic templating - Simple string replacement and template functions
- Handlebars templating - A popular, feature-rich templating engine
- Best practices - How to organize and structure your templates
Basic Templating Without Handlebars
Before diving into templating engines, let's understand the fundamentals using basic JavaScript techniques.
Method 1: Simple String Replacement
The most basic form of templating is string replacement:
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
const PORT = 3000;
// Middleware
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(express.static('public'));
// Simple template function
function renderTemplate(template, data) {
let html = template;
// Replace placeholders with data
for (const key in data) {
const placeholder = ' + key + ';
const value = data[key] || '';
html = html.replace(new RegExp(placeholder, 'g'), value);
}
return html;
}
// User profile route
app.get('/user/:id', (req, res) => {
const userId = parseInt(req.params.id);
// Mock user data
const users = [
{ id: 1, name: 'John Doe', email: 'john@example.com', age: 25, city: 'New York' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', age: 30, city: 'Los Angeles' },
{ id: 3, name: 'Bob Johnson', email: 'bob@example.com', age: 35, city: 'Chicago' }
];
const user = users.find(u => u.id === userId);
if (!user) {
return res.status(404).send('<h1>User not found</h1>');
}
// Read template file
// path.join() creates a proper file path by combining directory parts
// __dirname is the current directory where the script is running
// This creates: /your-project/templates/user-profile.html
const templatePath = path.join(__dirname, 'templates', 'user-profile.html');
const template = fs.readFileSync(templatePath, 'utf8');
// Render template with user data
const html = renderTemplate(template, user);
res.send(html);
});
app.listen(PORT, () => {
console.log(`Server running on ${PORT}`);
});
Template file (templates/user-profile.html):
<!DOCTYPE html>
<html>
<head>
<title>{{name}} - Profile</title>
</head>
<body>
<h1>{{name}}</h1>
<p>User Profile</p>
<div>
<p><strong>Email:</strong> {{email}}</p>
<p><strong>Age:</strong> {{age}}</p>
<p><strong>City:</strong> {{city}}</p>
<p><strong>User ID:</strong> #{{id}}</p>
</div>
<p><a href="/">← Back to Home</a></p>
</body>
</html>
As you can see, the templating uses regular expressions to find and replace values. A helper function is used in the example to partially automate the process based on the json object sent to it.
Introduction to Handlebars
Like how Express simplifies many processes related to webserver hosting, Handlebars simplifies many processes related to templating.
Installing Handlebars
First, install the hbs package:
npm install hbs
Setting Up Handlebars with Express
const express = require('express');
const hbs = require('hbs');
const path = require('path');
const app = express();
const PORT = 3000;
// Set up Handlebars
app.set('view engine', 'hbs');
app.set('views', path.join(__dirname, 'views'));
// Register partials directory
hbs.registerPartials(path.join(__dirname, 'views', 'partials'));
// Middleware
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(express.static('public'));
// Routes
app.get('/', (req, res) => {
res.render('home', {
title: 'Welcome to Our Site',
message: 'This is a Handlebars template!'
});
});
app.listen(PORT, () => {
console.log('Server running on http://toastcode.net/tschotter_node');
});
Directory Structure
Create this directory structure for your Handlebars templates:
your-project/
├── views/
│ ├── partials/
│ │ ├── header.hbs
│ │ └── footer.hbs
│ ├── home.hbs
│ └── user.hbs
├── public/
└── server.js
Home Page Template (views/home.hbs)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}}</title>
</head>
<body>
{{> header}}
<main>
<h1>{{title}}</h1>
<p>{{message}}</p>
</main>
{{> footer}}
</body>
</html>
Header Partial (views/partials/header.hbs)
<header>
<nav>
<a href="/tschotter_node/">Home</a>
</nav>
</header>
Footer Partial (views/partials/footer.hbs)
<footer>
<p>© 2024 My Website</p>
</footer>
User Information Page (views/user.hbs)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}}</title>
</head>
<body>
{{> header}}
<main>
<h1>User Information</h1>
<div>
<p><strong>Name:</strong> {{user.name}}</p>
<p><strong>Email:</strong> {{user.email}}</p>
<p><strong>Age:</strong> {{user.age}}</p>
<p><strong>City:</strong> {{user.city}}</p>
</div>
</main>
{{> footer}}
</body>
</html>
Server Routes with Handlebars
// User information route
app.get('/user/:id', (req, res) => {
const userId = parseInt(req.params.id);
const users = [
{
id: 1,
name: 'John Doe',
email: 'john@example.com',
age: 25,
city: 'New York'
},
{
id: 2,
name: 'Jane Smith',
email: 'jane@example.com',
age: 30,
city: 'Los Angeles'
}
];
const user = users.find(u => u.id === userId);
if (!user) {
return res.status(404).send('<h1>User not found</h1>');
}
res.render('user', {
title: 'User Information',
user: user
});
});
Using Variables in Handlebars
Handlebars provides several ways to work with variables in your templates. Understanding these different approaches will help you create more dynamic and flexible templates.
1. Basic Variable Output
The simplest way to use variables is with double curly braces:
2. Passing Variables to Partials
When including partials, you can pass specific variables to them:
Then in your header.hbs partial:
3. Using Context Variables
Variables from your route's context are automatically available in partials:
// In your route
app.get('/dashboard', (req, res) => {
res.render('dashboard', {
pageTitle: 'User Dashboard',
user: {
name: 'John Doe',
role: 'admin',
lastLogin: '2024-01-15'
},
stats: {
totalPosts: 25,
followers: 150
}
});
});
4. Using @root for Global Context
The @root context is a special Handlebars variable that gives you access to the original data passed to your template, even when you're inside loops or partials where the context might have changed.
Why do you need @root?
When you're inside a `` loop or a partial, Handlebars changes the context (the available variables). The @root variable always points back to the original data you passed to `res.render()`.
Example without @root (problematic):
// In your route
app.get('/blog', (req, res) => {
res.render('blog', {
siteName: 'My Blog',
currentPage: 'blog',
posts: [
{ title: 'Post 1', author: 'John' },
{ title: 'Post 2', author: 'Jane' }
]
});
});
Example with @root (correct):
In partials, @root is especially useful:
Think of @root as:
@root= the original data you passed tores.render()this= the current context (changes in loops and partials)@root= your "escape hatch" back to the original data
5. Conditional Variables
Use variables in conditional statements:
6. Looping Through Variables
Iterate over arrays and objects:
7. Nested Object Access
Access nested properties using dot notation:
8. Default Values
Provide fallback values when variables might be undefined:
Or use the or helper (if you create one):
// Register a helper for default values
hbs.registerHelper('or', function(value, defaultValue) {
return value || defaultValue;
});
Handlebars Helpers
Handlebars allows you to create custom helpers for more complex logic:
// Register custom helpers
hbs.registerHelper('formatDate', function(date) {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
});
hbs.registerHelper('capitalize', function(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
});
hbs.registerHelper('ifEquals', function(arg1, arg2, options) {
return (arg1 == arg2) ? options.fn(this) : options.inverse(this);
});
hbs.registerHelper('times', function(n, block) {
let accum = '';
for (let i = 0; i < n; ++i) {
accum += block.fn(i);
}
return accum;
});
Using Helpers in Templates
Here's how these helpers are used in your Handlebars templates:
Using the formatDate helper:
<!-- In your template -->
<p>Published on: {{formatDate post.date}}</p>
<!-- Output: Published on: January 15, 2024 -->
Using the capitalize helper:
<!-- In your template -->
<h1>Welcome, {{capitalize user.name}}!</h1>
<!-- If user.name is "john doe", output: Welcome, John doe! -->
Using the ifEquals helper:
<!-- In your template -->
{{#ifEquals user.role "admin"}}
<p>You have admin privileges</p>
{{else}}
<p>You are a regular user</p>
{{/ifEquals}}
Using the times helper:
<!-- In your template -->
{{#times 5}}
<p>Star {{this}}</p>
{{/times}}
<!-- Output: 5 paragraphs with "Star 0", "Star 1", etc. -->
Benefits of Using Handlebars
- Clean syntax - Easy to read and maintain templates
- Logic-less - Encourages separation of concerns
- Partials - Reusable template components
- Layouts - Consistent page structure
- Helpers - Custom functions for complex logic
- Conditionals and loops - Built-in support for dynamic content
- Escape by default - Automatic HTML escaping for security
Best Practices
- Organize templates - Use clear directory structure
- Use partials - Break down complex templates into reusable components
- Keep logic minimal - Move complex logic to helpers or server-side code
- Validate data - Always validate data before passing to templates
- Use layouts - Maintain consistent page structure
- Escape output - Let Handlebars handle HTML escaping automatically
Next Steps
Now that you understand templating with both basic techniques and Handlebars, you have the foundation to create dynamic HTML pages that respond to user input. Next, you'll learn how to create forms and handle form submissions.