shannonmoeller / handlebars-layouts

Handlebars helpers which implement layout blocks similar to Jinja, Nunjucks (Swig), Pug (Jade), and Twig.
http://npm.im/handlebars-layouts
MIT License
361 stars 29 forks source link

Support for programmatically composed layouts #5

Closed idpaterson closed 10 years ago

idpaterson commented 10 years ago

I wanted to use the Handlebars Layouts helpers, but they did not work with the layout system that I was using, express3-handlebars. Rather than allowing views to specify a layout by name, the layout is specified either in the global express3-handlebars configuration or on a per-render basis. The module compiles the view into an HTML string, then renders the layout, providing the body such that the layout can show it with {{{body}}}. Clearly this is not as powerful as Handlebars Layouts.

This very simple change allows the append, prepend, and replace blocks to be used outside of an extend block by initializing context._blocks = {} if it does not yet exist. _blocks is still maintained on the context allowing the layout, compiled with the same context or at least with a copy of the _blocks attribute, to define modifiable blocks.

Tests

I added three test cases to hopefully cover the gamut of potential use cases for this.

  1. should properly render programmatically-composed layouts tests that the approach described above of rendering the view to a string and then passing that to the body. The test uses {{#block "body"}} rather than {{{body}}} since it is a more useful pattern.
  2. should properly render self-modifying layouts might be useful in limited circumstances, such as providing top-of-file configuration blocks for a very large and gangly template.
  3. should properly render layouts included as partials exists only to test that including the template as a partial works. There is no reason I can think of to not use an extend block in this case so I noted that in the template comments.

    Caveats

This requires either the same context to be used when rendering the layout, or the _blocks attribute to be copied. express3-handlebars does this already by extending the context used for the view to add a property to the rendered view {body: body}, preserving _blocks. I do not know whether other layout engines that may also benefit from this change do the same so I added a note to that effect in the readme.

coveralls commented 10 years ago

Coverage Status

Coverage remained the same when pulling 3fd10b6b74a2845eda951afd8ea39786b6d11815 on idpaterson:pull-requests/programmatically-composed-templates into 376de9bf912699ed7dff450aa67393a19c60feeb on shannonmoeller:master.

shannonmoeller commented 10 years ago

Thanks for the PR and for including tests! Can you provide a little more concrete usage example? For instance, I'm curious what benefit this provides over specifying the layout to extend with a variable.

Handlebars.registerPartial('bar', '...');
el.innerHTML = template({ foo: 'bar' });
{{#extend foo}} {{! extends "bar" }}
    ...
{{/extend}}
idpaterson commented 10 years ago

Sure, I suppose the best way to illustrate the difference is by showing the default behavior of express3-handlebars.

First, the environment is established with certain paths for templates and for partials. Partials are found and registered as needed, layouts are found as needed but not registered as partials.

var exphbs = require('express3-handlebars');

// ... set up express app

hbs = exphbs.create({
    defaultLayout: 'my_base_layout',
    partialsDir: [
        'shared/templates/',
        'views/partials/'
    ],
    layoutsDir: 'views/layouts/'
});
app.engine('handlebars', hbs.engine);
app.set('view engine', 'handlebars');

With that base in place, response handlers simply call res.render to render templates. The home.handlebars view can use any of the partials that are available, but it does not have direct access to the layout. Instead, it is rendered to a string, mixed in to the {title: 'Home', _data: {...}} context as body, then passed to the layout which includes it with {{{body}}}.

app.get('/', function (req, res) {
    res.render('home', {
        title: 'Home'
    });
});

It is possible to override the layout template at this point as well if necessary.

app.get('/', function (req, res) {
    res.render('home', {
        title: 'Home',
        layout: 'mobile_layout'
    });
});

In order to implement {{#extend layout_name}} without this pull request it would be necessary to make a few changes to disable the default layout functionality:

hbs = exphbs.create({
    defaultLayout: false,      // render views directly, without a layout
    partialsDir: [
        'shared/templates/',
        'views/partials/',
        'views/layouts/'       // register layouts as partials
    ],
    layoutsDir: null           // avoid loading layouts
});
app.engine('handlebars', hbs.engine);
app.set('view engine', 'handlebars');

// Add layout_name to all rendering contexts
app.locals.layout_name = 'my_base_layout';

app.get('/', function (req, res) {
    res.render('home', {
        title: 'Home'
    });
});

That's not at all difficult, but it requires all of our view templates to use Handlebars Layouts syntax. Most templates would simply render the body of the page and not mess with any other blocks in the layout. The minority case would be a template that needs to add additional stylesheets, modify a menu, or similar cases where Handlebars Layouts would shine. Such a template might look like this:

<h1>Alpha</h1>
<p class="funky-lead">Lorem ipsum and such</p>

{{#append "styles"}}
<link rel="stylesheet" type="text/css" href="/styles/funky.css" />
{{/append}}

While most views would use the simple syntax.

<h1>Beta</h1>
<p>Lorem ipsum and such</p>

I hope that helps, let me know if you would prefer some clarification, at least on the last point, in the Readme.

shannonmoeller commented 10 years ago

I'll be looking into Express issues this weekend. Had to rewrite some things to support deep inheritance. I'll look into porting your changes over.

idpaterson commented 10 years ago

Thanks, I hoped to help with the other Express issue that came up this week but I wasn't able to replicate the problem. The solution in this PR is still working very well for us in production, but I have no idea how it will jive with all the other improvements on your radar.

We're using handlebars-layouts heavily, but it's still nice to not have to include the #extend in each template and to just be able to use the layout functionality provided by express-handlebars without any workarounds.

shannonmoeller commented 10 years ago

I've added an express test case to the feature/deep-inheritance branch using the hbs module (see #8). As I mentioned, the deep-inheritance support is a rewrite. One which I'm hoping resolves your issue. Two changes to note:

  1. Breaking change: The {{#append [block]}}, {{#prepend [block]}}, and {{#replace [block]}} helpers have been replaced by a single {{#content [block] mode="[append|prepend]"}} helper.
  2. New Feature: There is now an {{#embed [partial]}} which is a layout-compatible replacement for {{> [partial]}}.
idpaterson commented 10 years ago

Thanks! I'll check it out when I get a chance.

shannonmoeller commented 10 years ago

Because of the rewrite, this pull request is no longer valid. I'm closing it, but please let me know if this is still an issue. Thanks again!

shannonmoeller commented 10 years ago

@idpaterson Just curious if you've been able to test the new version yet. Want to make sure you've got what you need!