Skip to content

[Not-quite-pull-request contribution] Template inheritance #208

Closed
thejohnfreeman opened this Issue Mar 21, 2012 · 36 comments
@thejohnfreeman

I wrote a couple helpers that let me use template inheritance, a la Django. Since they're just helpers, they don't need to go into Handlebars proper, but I wanted to share them. Perhaps if they are deemed useful enough, someone can put in the effort to integrate them.

Usage

The first helper is named block. It lets you specify default content.

The second helper is named partial. (I don't know why I chose these names.) It lets you specify overriding content.

In your base template, you declare sections of content that may be replaced by deriving templates. For these sections, you can either use {{#block}}s or normal partial inclusion ({{>}}). The second option serves as a shorthand for when you have no default content.

<!-- base.hbs -->
<title>{{#block "title"}}Untitled{{/block}}</title>
{{> content}}

Then in your deriving templates, you specify any overrides with {{#partial}}s before declaring the inherited template with normal partial inclusion.

<!-- derived.hbs -->
{{#partial "title"}}Title{{/partial}}
{{#partial "content"}}Hello, World!{{/partial}}
{{> base}}

Example output:

<title>Title</title>
Hello, World!
@thejohnfreeman

I remember why I chose the names now:

  • partial lets you specify inline partials that can be used for any reason, not just inheritance.
  • block mimics the convention from other template systems.
@natevw
natevw commented Apr 24, 2012

This looks interesting and a useful approach (modulo Handlebar's "global" partials registration, but that's not really this plugin's fault).

Via a somewhat-unrelated Google search, I stumbled across your blog post about it. That's where I actually found the code for this. It might be helpful to post a clean patch or pull request.

@thejohnfreeman

Thanks, I'll take a stab at it this weekend.

@natevw
natevw commented Apr 25, 2012

Wanted to share the trick required for "chained" inheritance like one can do in Django, e.g. an arrangement like:

base --> subtype --> page

This is possible, but you have to remember that because of the way the helpers work, the "parentmost" partial actually wins over the child partials when filling in a block.

# base.html
<h1>My Website — {{> title}}</h1>

# subtype.html
{{#partial "title"}}Some Section — {{> page_title}}{{/partial}}
{{>base}}

# page.html
{{#partial "page_title"}}Specific Page{{/partial}}
{{>subtype}}

Then when you render page.html you'll get:

<h1>My Website — Some Section — Specific Page</h1>
@thejohnfreeman

Good point. Adding a check to the partial helper that exits before overwriting an existing partial would implement the same behavior as in Django. Would that be better?

@natevw
natevw commented Apr 25, 2012

I don't think that would work. The partials are stored globally and so not overwriting partials will cause trouble with later template calls (e.g. separate HTTP requests) in the very common case (since this is the point of template inheritance) that different page renders need to overwrite the same named partial.

@bobbydavid

I'm also interested in this. Is there an update?

@thejohnfreeman

Yeah, I lied and didn't work on it last weekend. We had Bjarnefest here at Texas A&M, and I got signed up for a paper due in a few days. I'll get to this after the paper. I'm sorry. :(

@ashenden

Would love to see template inheritance make it into Handlebars. This is the only thing keeping me from fully embracing Handlebars.

@natevw
natevw commented May 10, 2012

I've been using an even further simplified version of this in a project. It's nice, but my initial worry about Handlebars' global partials has bit me a few times already. It's tempting to try override something in a child template (e.g. provide a different header block) but that ends up "sticking" and affecting unrelated page renders.

I have some ideas for a workaround in my particular case, but IMO a pre-requisite to this would be fixing Handlebars to not rely on module-level globals — that may be peachy on the client side, but on the server breaks things like this and more.

@ashenden

Still haven't figured out how to get this to work. I have a directory named "templates" which has a number of sub-directories. One of these sub-directories is called "layouts".

GOAL: have a parent template containing boilerplate markup for an empty page, with blocks that could be overridden or replaced by templates that extend it.

EXAMPLE OF PARENT TEMPLATE ("/app/templates/layouts/main.html"):

<!doctype html>
<!--[if lt IE 7 ]>
    <html lang="en" class="no-js ie6">
<![endif]-->
<!--[if IE 7 ]>
    <html lang="en" class="no-js ie7">
<![endif]-->
<!--[if IE 8 ]>
    <html lang="en" class="no-js ie8">
<![endif]-->
<!--[if IE 9 ]>
    <html lang="en" class="no-js ie9">
<![endif]-->
<!--[if (gt IE 9)|!(IE)]><!-->
    <html lang="en" class="no-js">
<!--<![endif]-->

<html>
    <head>

        <!-- =================================================== -->
        <!-- =============== BASIC PAGE SETUP ================== -->
        <!-- =================================================== -->

        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">

        <title>{{#block "title"}}{{ page_title }}{{/block}}{{#block "subtitle"}}{{ page_subtitle }}{{/block}}</title>

        <meta property="og:title" content="" itemprop="name" />
        <meta property="og:description" content="" itemprop="description" />
        <meta property="og:url" content="" />
        <meta property="og:type" content="" />
        <meta property="og:image" content="" itemprop="image" />

        <!--[if lt IE 9]>
            <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
        <![endif]-->

        <!-- =================================================== -->
        <!-- ========== MOBILE SPECIFIC META TAGS ============== -->
        <!-- =================================================== -->

        <meta name="viewport" content="width=device-width, initial-scale=1.0" />

        <!-- =================================================== -->
        <!-- ==================== FAVICONS ===================== -->
        <!-- =================================================== -->

        {{#block "favicons"}}
        <link rel="shortcut icon" type="image/x-icon" href="/resources/images/icons/favicon.ico" />
        {{/block}}

        {{#block "head"}}
        {{/block}}

    </head>

    <body>

        {{#block "body"}}
        {{/block}}

        {{#block "foot"}}
        {{/block}}

    </body>
</html>

EXAMPLE OF EXTENDING TEMPLATE ("/app/templates/layouts/subpage.html"):

{{#partial "body"}}<div>Hello Subpage!</div>{{/partial}}
{{> main}}

IDEAS? I'm hitting issues with John's solution. I've dumped the following .js at the end of my Handlebars.js file, but keep getting the error "Uncaught Error: The partial main could not be found"

Handlebars.loadPartial = function loadPartial(name) {

    var partial = handlebars.partials[name];

    if (typeof partial === "string") {

        partial = Handlebars.compile(partial);
        Handlebars.partials[name] = partial;

    }

    return partial;

};

Handlebars.registerHelper("ifNotEmpty", function ifNotEmpty(options) {

    var content = options.inverse(this);

    if (content.trim() === "") {

        content = "";

    } else {

        Handlebars.registerPartial("$content", content);
        content = options.fn(this);

    }

    return content;

});

Handlebars.registerHelper("join", function join(list, sep, options) {

    return list.map(options.fn).join(sep);

});

Handlebars.registerHelper("ul", function ul(list, options) {

    if (!list || list.length === 0) return "";

    return "<ul>\n" + list.map(function (item) {

        return "<li>" + options.fn(item) + "</li>";

    }).join("\n") + "\n</ul>";

});

Handlebars.registerHelper("partial", function partial(name, options) {

    Handlebars.registerPartial(name, options.fn);

});

Handlebars.registerHelper("block", function block(name, options) {

    // Look for partial by name.
    var partial = Handlebars.loadPartial(name) || options.fn;
    return partial(this, { data : options.hash });

});
@thejohnfreeman

You have to register every partial with Handlebars; it will not search your directories for you.

@ashenden

Thanks John. If you don't mind, do you have an example of how that might look (i.e. how to register a partial)? Maybe working from the loose example above?

@ashenden

Also, is the .js snippet I pasted above correct? I pulled this from one of your example files. Should this just be pasted at the bottom of my Handlebars.js file? What is best practice here?

@thejohnfreeman
Handlebars.registerPartial("main", // partial name
  "the source string that you get from reading the file"); // partial source

Yes, you can include whatever I put in the file anywhere after you include Handlebars.

@ashenden

Hmmm. Ok. Might have an issue working this into my development paradigm. Trying to figure it out. I'm working with a derivative of Tim Branyen's "boilerplate-handlebars-layoutmanager" repo (https://github.com/tbranyen/boilerplate-handlebars-layoutmanager).

You can check out the work in progress here if you're curious: https://github.com/ashenden/Backbone-Boilerplate. Working in a branch called "inheritance". I'm trying to weave in your work, but I'm just having difficulty figuring out the best practice for doing so. Maybe you'll see something I haven't.

@thejohnfreeman

@natevw Is this the kind of "chained inheritance" in Django that you mentioned? They use a special variable block.super. I don't think special variables fit in Handlebars's philosophy. Is there an alternative?

Or perhaps you just want a grandchild's block to override a child's block? When I do get around to adding inheritance to Handelbars (should be tomorrow night), I will try to scope partials created by the helper to each top-level template instantiation. That should address the contention problem you pointed out earlier.

@natevw
natevw commented May 13, 2012

No, talking more the latter — the simpler case of a grandchild overriding a parent's partial.

However, it might be possible (if not terribly practical from an implementation standpoint) to also implement the "super" template by simply letting the partial's own name within its definition refer to "super":

{{#partial "title"}}{{> title}} — {{> subtitle}}{{/partial}}
@thejohnfreeman

How does this look?

Notes:

  • The partial block helper has become the override block helper as its behavior has changed beyond just inline registration of a partial.

  • Blocks in children will override their parents, but can include their parent like a normal partial:

    {{#override "title"}}{{> title}} - Subtitle{{/override}}
    

    This should mean that you can include the parent block several times with different contexts, like any other partial, but I haven't tested that, and I don't know how useful it is.

  • There is no interference between different renderings. The extend helper is necessary for this behavior.

  • The block block helper can be used several times with the same name but different contents. I'm not sure how useful this is, either.

@ashenden

John. This solution looks great. I agree with you regarding the odd use-case of including the parent block several times with different context. But, you never know.

As I said above, I'm working off a derivative of Tim Branyen's boilerplate-handlebars-layoutmanager repo and struggling to find a graceful way of integrating your solution above. The primary issue is figuring where to best "registerPartial", as his layoutManager library strips out a lot of this complexity to give you a more robust and extendable framework. Maybe you'd have some ideas?

@thejohnfreeman

I'm not familiar with his library. Is there a quick rundown somewhere?

@ashenden

Yeah. He actually did a nice Screencast on his blog. I'm sure it's changed since the post, but will give you a solid idea of what we're attempting to achieve.

I've basically copied over his code-base, modified a few folder names and am attempting to bake in a few other goodies as well to give myself, and anyone else, a great starting point for building a backbone/handlebar app.

@thejohnfreeman

Okay. I generally try to be helpful to people, but I have to be frank. I hope you don't take the following criticism personally; it is just business to me.

A 21-minute screencast is not a short rundown. Do you think it takes you more effort to understand Handlebars and my small contribution here, or for me to understand Backbone and Tim Branyen (of whom I've never heard) and his Layout Manager and your derivative? I tried above to give the simplest explanation of how to use the inheritance extension I've laid out. It is much shorter than a 21-minute screencast, and it is written for people who I assume are already using Handlebars. I do not want this issue thread to be hijacked with personal tech support. If you want to discuss this further, please email or choose another medium. Thank you.

@ashenden

Point taken.

@oncletom

Any news on this feature? Looks interesting for maintainability.

@sontek
sontek commented Sep 4, 2012

@thejohnfreeman You have any updated code for this? We are looking to do this in ours :)

@thejohnfreeman

@sontek hmm I haven't used this in several months. What's the current status? Does it not work any more? My last fiddle seems to work still, but I haven't packaged it yet. Are you looking to use this for pyramid_mustache?

@sontek
sontek commented Sep 5, 2012

@thejohnfreeman I'll take a look at the jsfiddle. Yeah, I'm looking to handle some inheritance that I need to do inside pyramid_mustache to make the templates a little cleaner

@ReklatsMasters

Here's a simple way how to always keep in partials default value of block. Example here

# base.tpl
You are at {{#block "test"}} Martian {{/block}}.<br />

# child.tpl
{{#partial "test"}} Earthman {{/partial}}
{{> base}}

# child2.tpl
{{> base}}

Output:

You are at Martian .
You are at Earthman .
You are at Martian .
@adeleinr
adeleinr commented Apr 4, 2013

Any updates on this? Will it ever make it to master? It seems like a most

@joshhansen

Status? Definitely interested in this feature!

@davidvetrano

I've created a modified version of @ReklatsMasters 's version of @thejohnfreeman 's code which supports "multi-level" inheritance.

Ideally, child templates should also be able to be inherited as well as introduce new blocks. The "block" and "partial" paradigm does work in this case with a small tweak. The realization is that only the highest "inheritance level" partial of a given name (we're assuming that each block has a globally-unique name) is the one which gets applied.

So after running through a child template, we override partials and then run the parent. By introducing an invariant that "inheritance" happens at the bottom of the file, we can assume that for partials of the same name which are applied more than once, only the first application should occur. Thus, for a given rendering of a template, all we need to do is keep track of which partials have been applied and ensure that it only happens once.

The only snag is how to detect when the template is completely finished so the set of applied partials can be cleared for next time. I used a hacky approach where I count how many 'inheritsFrom' blocks begin and end to detect the ending of the last such block which corresponds to the end of the top-most child template. Perhaps someone can come up with a better approach for keeping track of partial application?

Here's a link to the jsfiddle: http://jsfiddle.net/DdGex/1/

@ccleve
ccleve commented Jun 13, 2013

+1. This whole idea is slick and should be integrated into the main Handlebars code base. It has already been integrated into Handlebars.java: https://github.com/jknack/handlebars.java @thejohnfreeman -- any thoughts? Is the latest and greatest code for doing this still the code in your blog post?

@thejohnfreeman

The latest I think is in the last fiddle I linked in this thread. I haven't worked in JavaScript in several months, so I don't know if I'll be getting to this again. Further, I think the best solution involves changing the library to properly clear blocks after each evaluation. When I first posted this, I was kind of hoping someone would pick it up and get it into the library the right way. If I get a compulsion to do it myself, I'll report back here.

@kolya-ay kolya-ay referenced this issue in assemble/assemble Apr 8, 2014
Closed

Make assemble more modular #486

@kpdecker kpdecker added this to the Backlog milestone Jul 5, 2014
@MarkMurphy

and over two year's later?

@kpdecker
Collaborator
kpdecker commented May 5, 2015

Closing in favor of #1018

@kpdecker kpdecker closed this May 5, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.