Template inheritance for Handlebars
Handlebars is a member of the family of "logic-less" template systems born from Mustache. I will not describe the merits of logic-less templates in this post. (If you are unfamiliar with them, then please visit either of the two links at the beginning of this paragraph.) Rather, I want to describe a simple technique for extending Handlebars with template inheritance.
The goal
Consider a simple website with two pages:
Both pages share the same header and footer elements, while providing their own content.
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:
<!-- home.hbs -->
<html>
<body>
<p> HOME </p>
</body>
</html>
<!-- about.hbs -->
<html>
<body>
<p> ABOUT </p>
</body>
</html>
<!-- header.hbs -->
<p> HEADER </p>
<!-- 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 -->
<p> HOME </p>
<!-- about.hbs -->
<p> ABOUT </p>
<!-- top.hbs -->
<html>
<body>
<!-- bottom.hbs -->
</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:
<!-- home.hbs -->
Home
<p> HOME </p>
<!-- about.hbs -->
About
<p> ABOUT </p>
<!-- top-before-title.hbs -->
<html>
<head>
<title>
<!-- top-after-title.hbs -->
</title>
</head>
<body>
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:
<!-- base.hbs -->
<html>
<head>
<title> Default Title </title>
</head>
<body>
This will be default content that appears in a
deriving template if it does not declare a
replacement for the "content" section.
</body>
</html>
<!-- home.hbs -->
Home
HOME
<!-- about.hbs -->
About
ABOUT
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:
-
For base templates:
- A block of default content
-
For deriving templates:
- A block of replacement content
- A declaration of the base template
The block
block helper will replace its section with the partial
of the same name if it exists:
handlebars.loadPartial = function (name) {
var partial = handlebars.partials[name];
if (typeof partial === "string") {
partial = handlebars.compile(partial);
handlebars.partials[name] = partial;
}
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:
Default content
Do not confuse the name (block
) of this block helper with the general
concept of block helpers. 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:
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:
Replacement content
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:
- The partials for the base and deriving templates are registered.
- The user requests a rendering of the deriving template.
- 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.
- Handlebars instantiates the partial for the base template:
- 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.
- Each
- At the beginning of the deriving template, a number of
Conclusion
The running example can be rewritten using these helpers:
<!-- base.hbs -->
<html>
<head>
<title>
Default Title
</title>
</head>
<body>
This will be default content that appears in a
deriving template if it does not declare a
replacement for the "content" section.
</body>
</html>
<!-- home.hbs -->
Home
HOME
<!-- about.hbs -->
About
ABOUT
You can find the helpers on GitHub. If you are a Handlebars user or contributor, please encourage their integration.