diff --git a/README.md b/README.md index 0ec2e9e..7c2e43b 100644 --- a/README.md +++ b/README.md @@ -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 + + {{title}} + {{{body}}} + +``` + +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 + + {{title}} + +
{{#> content}}Default content{{/content}}
+ + +``` + +A child view inherits it and overrides the `content` block: + +```handlebars +{{#> base title="Home"}} + {{#*inline "content"}}

Welcome {{name}}

{{/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"}} + +
{{#> main}}{{/main}}
+ {{/inline}} +{{/base}} +``` + +A view can then inherit from `two-column` and fill those blocks: + +```handlebars +{{#> two-column title="Docs"}} + {{#*inline "sidebar"}}{{/inline}} + {{#*inline "main"}}

Welcome {{name}}

{{/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. @@ -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. diff --git a/test/template_inheritance.test.js b/test/template_inheritance.test.js new file mode 100644 index 0000000..ae5e771 --- /dev/null +++ b/test/template_inheritance.test.js @@ -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', [ + '', + '{{title}}', + '
{{#> content}}Default content{{/content}}
', + '', + ].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"}}', + '', + '
{{#> main}}{{/main}}
', + '{{/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, [ + '', + 'Home', + '

Welcome Alan

', + '\n', + ].join('\n')) + }) + + it('an unoverridden block keeps the parent default', async () => { + await request(app) + .get('/default') + .expect(200, [ + '', + 'Plain', + '
Default content
', + '\n', + ].join('\n')) + }) + + it('layouts nest more than one level deep', async () => { + await request(app) + .get('/nested') + .expect(200, [ + '', + 'Docs', + '

Welcome Alan

', + '\n', + ].join('\n')) + }) + + it('without layout: false the page is wrapped by the automatic layout too', async () => { + await request(app) + .get('/wrapped') + .expect(200, [ + '', + '', + ' ', + ' ', + '', + '', + '', + 'Home', + '

Welcome Alan

', + '\n', + '', + '\n', + ].join('\n')) + }) +}) diff --git a/test/views/inherit.hbs b/test/views/inherit.hbs new file mode 100644 index 0000000..2ee6d79 --- /dev/null +++ b/test/views/inherit.hbs @@ -0,0 +1 @@ +{{#> base title="Home"}}{{#*inline "content"}}

Welcome {{name}}

{{/inline}}{{/base}} diff --git a/test/views/inherit_default.hbs b/test/views/inherit_default.hbs new file mode 100644 index 0000000..104931d --- /dev/null +++ b/test/views/inherit_default.hbs @@ -0,0 +1 @@ +{{#> base title="Plain"}}{{/base}} diff --git a/test/views/inherit_nested.hbs b/test/views/inherit_nested.hbs new file mode 100644 index 0000000..fb80558 --- /dev/null +++ b/test/views/inherit_nested.hbs @@ -0,0 +1 @@ +{{#> two-column title="Docs"}}{{#*inline "sidebar"}}{{/inline}}{{#*inline "main"}}

Welcome {{name}}

{{/inline}}{{/two-column}}