-
-
Save typpo/abc09519d8c7939ebc30762edd80e654 to your computer and use it in GitHub Desktop.
Handlebars template inheritance - with a bug fix (see handlebars.loadPartial)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Template composition with inclusion | |
Every template language I have seen provides some mechanism for one template to include another, thus supporting the reuse of repeated elements like headers and footers. The included templates are called partials in Mustache parlance: | |
```html | |
<!-- home.hbs --> | |
<html> | |
<body> | |
{{> header}} | |
<p> HOME </p> | |
{{> footer}} | |
</body> | |
</html> | |
``` | |
```html | |
<!-- about.hbs --> | |
<html> | |
<body> | |
{{> header}} | |
<p> ABOUT </p> | |
{{> footer}} | |
</body> | |
</html> | |
``` | |
```html | |
<!-- header.hbs --> | |
<p> HEADER </p> | |
``` | |
```html | |
<!-- footer.hbs --> | |
<p> FOOTER </p> | |
This is not the DRYest implementation, however. The <html> and <body> tags are copied on every page. Adding scripts and stylesheets will only aggravate the situation. Larger sections can be abstracted: | |
<!-- home.hbs --> | |
{{> top}} | |
<p> HOME </p> | |
{{> bottom}} | |
``` | |
```html | |
<!-- about.hbs --> | |
{{> top}} | |
<p> ABOUT </p> | |
{{> bottom}} | |
``` | |
```html | |
<!-- top.hbs --> | |
<html> | |
<body> | |
{{> header}} | |
``` | |
```html | |
<!-- bottom.hbs --> | |
{{> footer}} | |
</body> | |
</html> | |
``` | |
This pattern is recommended when template inheritance is unavailable. It is fragile and rigid, though. Some tags, like <html> and <body> here, are split among the included templates - terrible for maintenance and readability. To customize a portion of an included template for each page, like the <title>, the templates must be divided even further: | |
```html | |
<!-- home.hbs --> | |
{{> top-before-title}} | |
Home | |
{{> top-after-title}} | |
<p> HOME </p> | |
{{> bottom}} | |
``` | |
```html | |
<!-- about.hbs --> | |
{{> top-before-title}} | |
About | |
{{> top-after-title}} | |
<p> ABOUT </p> | |
{{> bottom}} | |
``` | |
```html | |
<!-- top-before-title.hbs --> | |
<html> | |
<head> | |
<title> | |
``` | |
```html | |
<!-- top-after-title.hbs --> | |
</title> | |
</head> | |
<body> | |
{{> header}} | |
``` | |
This can quickly grow into a mess (and already has by some standards). | |
# Template composition with inheritance (and inclusion) | |
Template inheritance comes, I believe, from Django. It nicely addresses the above issues with template composition by essentially providing a mechanism for implementing the Dependency Inversion Principle. | |
With template inheritance, a base template has specially annotated sections of content that can be overwritten by deriving templates. Deriving templates then declare their base template and replacement content: | |
```html | |
<!-- base.hbs --> | |
<html> | |
<head> | |
<title>{{#title}} Default Title {{/title}}</title> | |
</head> | |
<body> | |
{{> header}} | |
{{#content}} | |
This will be default content that appears in a | |
deriving template if it does not declare a | |
replacement for the "content" section. | |
{{/content}} | |
{{> footer}} | |
</body> | |
</html> | |
``` | |
```html | |
<!-- home.hbs --> | |
{{derives base}} | |
{{#title}} Home {{/title}} | |
{{#content}} HOME {{/content}} | |
``` | |
```html | |
<!-- about.hbs --> | |
{{derives base}} | |
{{#title}} About {{/title}} | |
{{#content}} ABOUT {{/content}} | |
``` | |
Much better. Fewer templates are needed, and no context needs to be split. | |
# Template inheritance in Handlebars | |
Unfortunately, the Mustache specification does not prescribe support for template inheritance. Handlebars, which offers a number of improvements over vanilla Mustache, does not include it either. Dust does, but I prefer the syntax and helper interface of Handlebars. The good news is that Handlebars (perhaps unintentionally) exposes its registry of partials which can be used along with a couple of simple block helpers to implement template inheritance. | |
Three constructs are needed: | |
1. For base templates: A block of default content | |
2. For deriving templates: A block of replacement content | |
3. A declaration of the base template | |
The `block` block helper will replace its section with the partial of the same name if it exists: | |
```js | |
handlebars.loadPartial = function (name) { | |
var partial = handlebars.partials[name]; | |
if (typeof partial === "string") { | |
partial = handlebars.compile(partial); | |
handlebars.partials[name] = partial; | |
} else { | |
handlebars.partials[name] = undefined; | |
} | |
return partial; | |
}; | |
handlebars.registerHelper("block", | |
function (name, options) { | |
/* Look for partial by name. */ | |
var partial | |
= handlebars.loadPartial(name) || options.fn; | |
return partial(this, { data : options.hash }); | |
}); | |
``` | |
It will be used to specify default content in base templates: | |
```html | |
{{#block "name"}} | |
Default content | |
{{/block}} | |
``` | |
Do not confuse the name `block` of this block helper with the general concept of [block helpers](http://handlebarsjs.com/block_helpers.html). The name is simply an (admittedly potentially confusing) coincidence that was chosen to be consistent with existing systems supporting template inheritance, like Django. | |
The partial block helper generates no output and instead registers a section of content as a named partial in the Handlebars runtime: | |
```js | |
handlebars.registerHelper("partial", | |
function (name, options) { | |
handlebars.registerPartial(name, options.fn); | |
}); | |
``` | |
It can be used to declare any inline partial, but in the context of template inheritance, it annotates replacement content in deriving templates: | |
```html | |
{{#partial "name"}} | |
Replacement content | |
{{/partial}} | |
``` | |
For the final piece, declaring a base template, we will resort to normal template inclusion (partials). In contrast to existing template inheritance convention, this declaration will occur at the end of a deriving template rather than the beginning. The reason why becomes apparent when we consider the dataflow: | |
1. The partials for the base and deriving templates are registered. | |
2. The user requests a rendering of the deriving template. | |
3. Handlebars instantiates the partial for the deriving template: | |
- At the beginning of the deriving template, a number of partial blocks will register partials for sections of replacement content. | |
- At the end of the deriving template, the partial for the base template will be included. | |
4. Handlebars instantiates the partial for the base template: | |
5. Each `#block` block (forgive me) will be replaced by the partial of the given name if one was registered in the deriving template. Otherwise, its given content will be used as the default. | |
#Conclusion | |
The running example can be rewritten using these helpers: | |
```html | |
<!-- base.hbs --> | |
<html> | |
<head> | |
<title> | |
{{#block "title"}} Default Title {{/block}} | |
</title> | |
</head> | |
<body> | |
{{> header}} | |
{{#block "content"}} | |
This will be default content that appears in a | |
deriving template if it does not declare a | |
replacement for the "content" section. | |
{{/block}} | |
{{> footer}} | |
</body> | |
</html> | |
``` | |
```html | |
<!-- home.hbs --> | |
{{#partial "title"}} Home {{/partial}} | |
{{#partial "content"}} HOME {{/partial}} | |
{{> base}} | |
``` | |
```html | |
<!-- about.hbs --> | |
{{#partial "title"}} About {{/partial}} | |
{{#partial "content"}} ABOUT {{/partial}} | |
{{> base}} | |
``` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment