Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 87 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,92 @@ hbs.registerPartials(path.join(__dirname, '/views/partials'), {

**Note:** This method is async; meaning that the directory is walked in a non-blocking manner to app startup.

## Layouts ##

When a `layout` file with the same extension as the view (for example `views/layout.hbs`) exists in one of your `views` directories, hbs renders the view first and then renders the layout with the view's output available as `body`. Use the triple-stash so the HTML is not escaped:

```handlebars
<html>
<head><title>{{title}}</title></head>
<body>{{{body}}}</body>
</html>
```

The layout receives the same locals as the view, so `title` above can be set from `res.render`. If no default layout file exists the view is rendered on its own.

The `layout` option controls this behaviour. It can be set per render, or app-wide through Express' `view options`; a value given to `res.render` wins over the app-wide one.

```javascript
// use views/admin.hbs as the layout for this render
res.render('dashboard', { layout: 'admin' })

// render this view without any layout
res.render('home', { layout: false })

// turn the automatic layout off for every view
app.set('view options', { layout: false })

// use views/main.hbs as the layout for every view
app.set('view options', { layout: 'main' })
```

A layout name is resolved relative to each `views` directory, and the view extension is appended unless the name already has one. Asking for a layout that does not exist is a render error, while a missing default `layout` file is silently skipped.

## Template inheritance ##

Handlebars supports template inheritance natively through [partial blocks](https://handlebarsjs.com/guide/partials.html#partial-blocks) and [inline partials](https://handlebarsjs.com/guide/partials.html#inline-partials), so you can build reusable, nested layouts without any extra hbs feature. Register your base layouts as partials, expose override points with `{{#> block}}`, and let child views fill them with `{{#*inline "block"}}`.

Register a `base` layout as a partial (for example from your partials directory, see [Helpers and Partials](#helpers-and-partials)):

```handlebars
<html>
<head><title>{{title}}</title></head>
<body>
<main>{{#> content}}Default content{{/content}}</main>
</body>
</html>
```

A child view inherits it and overrides the `content` block:

```handlebars
{{#> base title="Home"}}
{{#*inline "content"}}<h1>Welcome {{name}}</h1>{{/inline}}
{{/base}}
```

A child that inherits from a partial layout already renders the whole page, so turn the automatic [layout](#layouts) off to avoid wrapping it twice, either per render:

```javascript
res.render('home', { layout: false, name: 'Alan' })
```

or for the whole app:

```javascript
app.set('view options', { layout: false })
```

Blocks a child does not override keep the parent default, and because partials can include other partials, layouts nest to any depth. For example, a `two-column` partial can inherit from `base`, fill its `content` block and expose `sidebar` and `main` blocks of its own:

```handlebars
{{#> base}}
{{#*inline "content"}}
<aside>{{#> sidebar}}Default sidebar{{/sidebar}}</aside>
<section>{{#> main}}{{/main}}</section>
{{/inline}}
{{/base}}
```

A view can then inherit from `two-column` and fill those blocks:

```handlebars
{{#> two-column title="Docs"}}
{{#*inline "sidebar"}}<nav>Docs menu</nav>{{/inline}}
{{#*inline "main"}}<h1>Welcome {{name}}</h1>{{/inline}}
{{/two-column}}
```

## Exposing locals as template data ##

hbs has the ability to expose the application and request locals within any context inside a view. To enable this functionality, simply call the `localsAsTemplateData` method and pass in your Express application instance.
Expand Down Expand Up @@ -135,7 +221,7 @@ app.engine('hbs', instance2.__express);
Each instance has the same methods/properties as the `hbs` module object. The module object is actually just an instance created for you automatically.

### extra scripts or styles ##
Sometimes it is useful to have custom scripts or stylesheets on your pages. Handlebars does not provide a way to import or extend a template, but through the use of helpers you can create a similar result.
Sometimes it is useful to have custom scripts or stylesheets on your pages. If your layout is a partial, use a partial block for this (see [Template inheritance](#template-inheritance)). If you rely on hbs's automatic layout instead, the layout is not a partial, so a pair of helpers can create a similar result.

We can take advantage of the fact that our body template is processed before the layout template. Knowing this, we can create two helpers `block` and `extend` which can be used to 'inject' custom stylesheets or scripts into the layout template. The `block` helper will act as a placeholder for values specified in earlier `extend` helpers.

Expand Down
107 changes: 107 additions & 0 deletions test/template_inheritance.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
const path = require('node:path')
const request = require('supertest')
const express = require('express')
const hbsFactory = require('../')

// https://github.com/pillarjs/hbs/issues/119
describe('template inheritance (#119)', () => {
let app

before(() => {
const hbs = hbsFactory.create()

// a reusable base layout registered as a partial, with an override block
hbs.registerPartial('base', [
'<html>',
'<head><title>{{title}}</title></head>',
'<body><main>{{#> content}}Default content{{/content}}</main></body>',
'</html>',
].join('\n'))

// a middle layout that inherits from `base`, fills its `content` block and
// exposes `sidebar` and `main` blocks of its own, so views nest two levels
hbs.registerPartial('two-column', [
'{{#> base}}',
'{{#*inline "content"}}',
'<aside>{{#> sidebar}}Default sidebar{{/sidebar}}</aside>',
'<section>{{#> main}}{{/main}}</section>',
'{{/inline}}',
'{{/base}}',
].join(''))

app = express()
app.engine('hbs', hbs.__express)
app.set('view engine', 'hbs')
app.set('views', path.join(__dirname, 'views'))

app.get('/home', (req, res) => {
res.render('inherit', { layout: false, name: 'Alan' })
})

app.get('/default', (req, res) => {
res.render('inherit_default', { layout: false })
})

app.get('/nested', (req, res) => {
res.render('inherit_nested', { layout: false, name: 'Alan' })
})

// the automatic layout is left on here on purpose, so the inherited page
// ends up wrapped a second time by views/layout.hbs
app.get('/wrapped', (req, res) => {
res.render('inherit', { name: 'Alan' })
})
})

it('a child view overrides a parent block', async () => {
await request(app)
.get('/home')
.expect(200, [
'<html>',
'<head><title>Home</title></head>',
'<body><main><h1>Welcome Alan</h1></main></body>',
'</html>\n',
].join('\n'))
})

it('an unoverridden block keeps the parent default', async () => {
await request(app)
.get('/default')
.expect(200, [
'<html>',
'<head><title>Plain</title></head>',
'<body><main>Default content</main></body>',
'</html>\n',
].join('\n'))
})

it('layouts nest more than one level deep', async () => {
await request(app)
.get('/nested')
.expect(200, [
'<html>',
'<head><title>Docs</title></head>',
'<body><main><aside><nav>Docs menu</nav></aside><section><h1>Welcome Alan</h1></section></main></body>',
'</html>\n',
].join('\n'))
})

it('without layout: false the page is wrapped by the automatic layout too', async () => {
await request(app)
.get('/wrapped')
.expect(200, [
'<html>',
'<head>',
' <title></title>',
' <link rel=\'stylesheet\' href=\'/stylesheets/style.css\'>',
'</head>',
'<body>',
'<html>',
'<head><title>Home</title></head>',
'<body><main><h1>Welcome Alan</h1></main></body>',
'</html>\n',
'</body>',
'</html>\n',
].join('\n'))
})
})
1 change: 1 addition & 0 deletions test/views/inherit.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#> base title="Home"}}{{#*inline "content"}}<h1>Welcome {{name}}</h1>{{/inline}}{{/base}}
1 change: 1 addition & 0 deletions test/views/inherit_default.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#> base title="Plain"}}{{/base}}
1 change: 1 addition & 0 deletions test/views/inherit_nested.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#> two-column title="Docs"}}{{#*inline "sidebar"}}<nav>Docs menu</nav>{{/inline}}{{#*inline "main"}}<h1>Welcome {{name}}</h1>{{/inline}}{{/two-column}}
Loading