Adds support of layout and blocks to EJS #147

Open
wants to merge 11 commits into
from

Conversation

Projects
None yet
5 participants
@mostly-magic

I've added supoort for layouts to EJS. The key features are:

  • Imports layout files specified with <% layout ... %>
  • Parses blocks identified with <% block ... %>
  • Exposes blocks to layout files with local block variables
  • Supports multi-layered layouts; child blocks override parent blocks

I saw that @ictliujie also has a pull request for this feature (#142), but since I was also working on my own implementation I thought it should submit it for discussion. The key differences are:

  • Minimal syntax changes: The only addition are the layout and block commands. No need to add <%+ and <%/ directives
  • Implicit end block: Blocks are implicitly ended when the next block is defined, or EOF is reached. This removes the need for the <%/block%> statement
  • Local blocks variable: Layouts can access blocks using a local blocks variable. This provides the greatest flexiblity for implementation.
Adds support of layout and blocks to EJS:
- Imports layout files specified with <% layout ... %>
- Parses blocks identified with <% block ... %>
- Exposes blocks to layout files with local `block` variables
- Supports multi-layered layouts; child blocks override parent blocks
Adds unit tests for layouts
Adds layouts to README
@tj

View changes

test/fixtures/layout-include-block.ejs
+<% layout layout-page -%>
+<% block title -%>
+Page Title
+<% block page -%>

This comment has been minimized.

Show comment Hide comment
@tj

tj Nov 28, 2013

Owner

personally i find the implicit ending a little confusing, I'd vote for <% block title %> to act like an empty placeholder, and then <% endblock %> (or whatever it's called) to specify blocks that do have default content, otherwise it reads a little unclear to me at least

@tj

tj Nov 28, 2013

Owner

personally i find the implicit ending a little confusing, I'd vote for <% block title %> to act like an empty placeholder, and then <% endblock %> (or whatever it's called) to specify blocks that do have default content, otherwise it reads a little unclear to me at least

This comment has been minimized.

Show comment Hide comment
@tj

tj Nov 28, 2013

Owner

<% extend is a bit more traditional than <% layout as well but other than that everything else looks good!

@tj

tj Nov 28, 2013

Owner

<% extend is a bit more traditional than <% layout as well but other than that everything else looks good!

This comment has been minimized.

Show comment Hide comment
@mostly-magic

mostly-magic Nov 28, 2013

Fair enough. I can change up the name.

@mostly-magic

mostly-magic Nov 28, 2013

Fair enough. I can change up the name.

This comment has been minimized.

Show comment Hide comment
@mostly-magic

mostly-magic Nov 28, 2013

Just to clarify, are you thinking something like:

<% extend layout-file %>
...
<% block content %>
...
<% endblock %>

And any content not in between a block/endblock section will get added to the default block?

@mostly-magic

mostly-magic Nov 28, 2013

Just to clarify, are you thinking something like:

<% extend layout-file %>
...
<% block content %>
...
<% endblock %>

And any content not in between a block/endblock section will get added to the default block?

This comment has been minimized.

Show comment Hide comment
@tj

tj Nov 28, 2013

Owner

yup i think that sounds about right, more like jinja stuff that most people are used to

@tj

tj Nov 28, 2013

Owner

yup i think that sounds about right, more like jinja stuff that most people are used to

This comment has been minimized.

Show comment Hide comment
@mostly-magic

mostly-magic Dec 6, 2013

Sorry, I just realized I misinterpreted your first comment. Let me backup and explain.

What I'm addressing is, when a template is extending a layout file, what to do with any content that falls outside of block/endblock pairs:

<% extend layout-file %>
What to do with me?
<% block title %>...<% endblock %>
Me too?
<% block content %>...<% endblock %>

There are three way to deal with this content: 1) throw an exception, 2) throw it away, 3) collect the content together into a default block. With option 3, the above template would essentially be the equivalent of:

<% extend layout-file %>
<% block title %>...<% endblock %>
<% block content %>...<% endblock %>
<% block default %>
What to do with me?
Me too?
<% endblock %>

I misunderstood you earlier comment and thought you were describing option 3, so jumped ahead without explaining myself. Sorry about that. Hope this clears it up a bit.

Edit: I should mention that my current implementation uses option 3. It's easy enough to switch to option 2. Option 1 is a little trickier, as it should consume whitespace without exception, but any of the options should be any easy change.

@mostly-magic

mostly-magic Dec 6, 2013

Sorry, I just realized I misinterpreted your first comment. Let me backup and explain.

What I'm addressing is, when a template is extending a layout file, what to do with any content that falls outside of block/endblock pairs:

<% extend layout-file %>
What to do with me?
<% block title %>...<% endblock %>
Me too?
<% block content %>...<% endblock %>

There are three way to deal with this content: 1) throw an exception, 2) throw it away, 3) collect the content together into a default block. With option 3, the above template would essentially be the equivalent of:

<% extend layout-file %>
<% block title %>...<% endblock %>
<% block content %>...<% endblock %>
<% block default %>
What to do with me?
Me too?
<% endblock %>

I misunderstood you earlier comment and thought you were describing option 3, so jumped ahead without explaining myself. Sorry about that. Hope this clears it up a bit.

Edit: I should mention that my current implementation uses option 3. It's easy enough to switch to option 2. Option 1 is a little trickier, as it should consume whitespace without exception, but any of the options should be any easy change.

This comment has been minimized.

Show comment Hide comment
@mostly-magic

mostly-magic Dec 6, 2013

Now to address what you were actually talking about earlier. I have a couple concerns about using a single-line placeholder.

  • Performance: In order to determine if the content that follows a single-line block is default content, you will have to scan ahead to the end of the file looking for an endblock statement. This won't be achievable using the current one forward-pass parser that's written. The best way I can think to implement this would be to add a pre-parse step that scans the file and tags any single-line blocks with an <% endblock %>. Then in the second pass, the parser can assume that any content following a <% block %> statement is default content.
  • Potential ambiguity: Can default content include a block? In other words, would you allow this:
<% block container %>
<div class="container">
    <% block content %>
</div>
<% endblock %>

If so, then the parser is going to have an ambiguity problem. Is the above code a single-line container block, followed by a content block with default content, or a container block with default content containing a single-line content block? If you don't want to allow this behavior it's not an issue, but I can see it being useful in certain scenarios.

  • Local block variable: My reason for opting to use a local variable instead of an embed statement was to allow the ability conditionally include decorative HTML in the layout file. For example:
<% if (blocks.title) { %>
<div class="page-title">
    <h1><%- blocks.title %></h1>
</div>
<% } %>

This doesn't mean that you can't also have a command statement to include a block, but <%- blocks.title %> seems just as concise as <% block title %>, and precludes the other issues.

@mostly-magic

mostly-magic Dec 6, 2013

Now to address what you were actually talking about earlier. I have a couple concerns about using a single-line placeholder.

  • Performance: In order to determine if the content that follows a single-line block is default content, you will have to scan ahead to the end of the file looking for an endblock statement. This won't be achievable using the current one forward-pass parser that's written. The best way I can think to implement this would be to add a pre-parse step that scans the file and tags any single-line blocks with an <% endblock %>. Then in the second pass, the parser can assume that any content following a <% block %> statement is default content.
  • Potential ambiguity: Can default content include a block? In other words, would you allow this:
<% block container %>
<div class="container">
    <% block content %>
</div>
<% endblock %>

If so, then the parser is going to have an ambiguity problem. Is the above code a single-line container block, followed by a content block with default content, or a container block with default content containing a single-line content block? If you don't want to allow this behavior it's not an issue, but I can see it being useful in certain scenarios.

  • Local block variable: My reason for opting to use a local variable instead of an embed statement was to allow the ability conditionally include decorative HTML in the layout file. For example:
<% if (blocks.title) { %>
<div class="page-title">
    <h1><%- blocks.title %></h1>
</div>
<% } %>

This doesn't mean that you can't also have a command statement to include a block, but <%- blocks.title %> seems just as concise as <% block title %>, and precludes the other issues.

This comment has been minimized.

Show comment Hide comment
@tj

tj Dec 6, 2013

Owner

yeah we'd have to use a different syntax for placeholders with no default content. I'm not sure I'd do anything with the "default" content, if I was looking at a template and saw something like this:

<% block head %>
  some stuff
<% endblock %>

some other content here

<% block content %>
  some stuff
<% endblock %>

I wouldn't really expect much out of the content in-between, at least it wouldn't be clear where it's going anyway, especially when it spans multiple blocks that are otherwise unrelated

@tj

tj Dec 6, 2013

Owner

yeah we'd have to use a different syntax for placeholders with no default content. I'm not sure I'd do anything with the "default" content, if I was looking at a template and saw something like this:

<% block head %>
  some stuff
<% endblock %>

some other content here

<% block content %>
  some stuff
<% endblock %>

I wouldn't really expect much out of the content in-between, at least it wouldn't be clear where it's going anyway, especially when it spans multiple blocks that are otherwise unrelated

This comment has been minimized.

Show comment Hide comment
@ictliujie

ictliujie Dec 9, 2013

In my opinion, the content out of blocks should be ignored. Giving the layout and blocks, the child just specify the block it would like to replace, other content just inherit the layout.
If many child tpls have sth that out of block, you could define a block in the layout, then you could explicitly inherit and replace the block in child tpl.
I think maybe this way would be more clear and familiar to developers :) see also #142

@ictliujie

ictliujie Dec 9, 2013

In my opinion, the content out of blocks should be ignored. Giving the layout and blocks, the child just specify the block it would like to replace, other content just inherit the layout.
If many child tpls have sth that out of block, you could define a block in the layout, then you could explicitly inherit and replace the block in child tpl.
I think maybe this way would be more clear and familiar to developers :) see also #142

This comment has been minimized.

Show comment Hide comment
@mostly-magic

mostly-magic Dec 13, 2013

Cool, thanks for your feedback. I'll discard the external content.

I'm feeling uninspired as to a name to give a single line block besides sblock, so I'll go with that for now - can always rename. Potentially we could add a pblock as well to include overridden parent content to address prepend/append feature described in #142.

@mostly-magic

mostly-magic Dec 13, 2013

Cool, thanks for your feedback. I'll discard the external content.

I'm feeling uninspired as to a name to give a single line block besides sblock, so I'll go with that for now - can always rename. Potentially we could add a pblock as well to include overridden parent content to address prepend/append feature described in #142.

@tj

View changes

lib/ejs.js
+ layout = exports.parse(layout, { filename: path, _with: false, open: open, close: close, compileDebug: compileDebug, _blocks: false });
+ buf += "');";
+ buf += "\n if (!blocks['default']) blocks['default'] = (function() {\n var buf=[];";
+ buf += "\n buf.push('";

This comment has been minimized.

Show comment Hide comment
@tj

tj Nov 28, 2013

Owner

not a huge huge deal since this is pretty small but it would be nice to maybe do buf += extend(path, ...) or similar to keep this block and the block below more declarative

@tj

tj Nov 28, 2013

Owner

not a huge huge deal since this is pretty small but it would be nice to maybe do buf += extend(path, ...) or similar to keep this block and the block below more declarative

This comment has been minimized.

Show comment Hide comment
@tj

tj Nov 28, 2013

Owner

same with my old include code as well, this function is getting pretty massive

@tj

tj Nov 28, 2013

Owner

same with my old include code as well, this function is getting pretty massive

This comment has been minimized.

Show comment Hide comment
@mostly-magic

mostly-magic Nov 28, 2013

I had similar thoughts when I was working on it. I'll add this in.

@mostly-magic

mostly-magic Nov 28, 2013

I had similar thoughts when I was working on it. I'll add this in.

mostly-magic added some commits Nov 29, 2013

Adds `Channel` to contain buffer.
This commit is the first step to providing a default block to contain
layouts. The `Channel` class contains the output buffer. The active
channel can them be switched out to provide multiple buffers.

Note: Used an object buffer instead of an array based on performance
tests. Tests can be viewed at http://jsperf.com/array-vs-object-buffer.
Adds explicit `endblock` command
Adds an explicit `endblock` statement to close layout blocks. If a
block is not closed before another block is opened or the end of file is
reached, an exception will be thrown. Any content not contained inside
of a block/endblock section will be added to the `default` block.
Moves commands into separate function
Moves template commands into a separate function,
cleaning up main parse loop.
@mostly-magic

This comment has been minimized.

Show comment Hide comment
@mostly-magic

mostly-magic Feb 11, 2014

I've finally gotten back to this. I've added the ability to define default content for a block. In order to support this I've made some pretty extensive modifications to the code.

As a shorthand to embed a block with no default content, you can use

<% sblock blockName %>

I'm not particularly happy with the label sblock, so I'm open to any alternatives. Let me know your thoughts.

I've finally gotten back to this. I've added the ability to define default content for a block. In order to support this I've made some pretty extensive modifications to the code.

As a shorthand to embed a block with no default content, you can use

<% sblock blockName %>

I'm not particularly happy with the label sblock, so I'm open to any alternatives. Let me know your thoughts.

@orianda

This comment has been minimized.

Show comment Hide comment
@orianda

orianda Feb 9, 2015

What about supporting blocks like twig does. This is quite convenient.

orianda commented Feb 9, 2015

What about supporting blocks like twig does. This is quite convenient.

@mde

This comment has been minimized.

Show comment Hide comment
@mde

mde Feb 9, 2015

Collaborator

This version of EJS is no longer maintained. Current work on the EJS module for NPM is happening at https://github.com/mde/ejs. There's some discussion of layouts/blocks happening here: mde/ejs#62

Collaborator

mde commented Feb 9, 2015

This version of EJS is no longer maintained. Current work on the EJS module for NPM is happening at https://github.com/mde/ejs. There's some discussion of layouts/blocks happening here: mde/ejs#62

@orianda

This comment has been minimized.

Show comment Hide comment
@orianda

orianda Feb 10, 2015

thx

orianda commented Feb 10, 2015

thx

@Hugo-seth Hugo-seth referenced this pull request in Hugo-seth/blog Dec 7, 2016

Open

ejs 的 layout 实现方法 #6

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment