ejs icon indicating copy to clipboard operation
ejs copied to clipboard

block/template/extend support for ejs

Open huxia opened this issue 8 years ago • 17 comments

Currently ejs doesn't have any block/template/extend features.

existing solutions:

https://github.com/seqs/ejs-blocks

neat javascript grammar, however it only support raw strings as block content.

https://github.com/tj/ejs/pull/142 https://github.com/User4martin/ejs/blob/plugin-snippets/docs/plugin-snippet.md

they invents several preprocessor directives like <%block header%>, <%/block header%> <%+ template%>, <%* snippet name %>, <%* /snippet %>, thus not very easy to learn, this is against ejs's design goals.

this approach:

page implementation (home.ejs):

<!-- define block contents by functions, it should be able to access the same locals data & context -->
<% var head = () => { %>
  <%- include('./include.css') %>
  <title>Hello EJS Template</title>
<% } -%>
<% var body = () => { %>
  <div>
    you have an message: <%= message.toLowerCase() %>
  </div>
<% } -%>

<!-- a single "include" finally, and its contents are passed by locals -->
<%- include('./layout', {body, head}) %>

template/layout declaration (layout.ejs):

<!-- NOTE: template/layout can be nested -->
<html>
    <head>
        <% if (!head) { %>
            <title>default title</title>
        <% } else { %>
            <!-- NOTE: this is the only one thing changed for ejs users, ejs "include" function now accept function as its first argument -->
            <%- include(head) %>
        <% } %>
    </head>
    <body>
        <h1>This is a layout</h1>
        <% if (!body) { %>
            <small>default content</small>
        <% } else { %>
            <!-- same above -->
            <%- include(body) %>
        <% } %>
    </body>
</html>

advantages

  • pure javascript gramma, with ES6 arrow function, the code looks nice too
  • it breaks nothing
  • like the original "include", it can be nested
  • functions can have its parameters, so include can handle function-local variables as well as context variables, example: https://github.com/mde/ejs/issues/252#issuecomment-428576331

huxia avatar Apr 17 '17 10:04 huxia

PR, any suggestions are welcome: https://github.com/mde/ejs/pull/251

huxia avatar Apr 17 '17 10:04 huxia

Is anything happening with this?

rossrossp avatar Jul 03 '17 17:07 rossrossp

See https://github.com/mde/ejs/pull/251 for discussion; I'm not going to merge anything on this front without permission from the other maintainers.

RyanZim avatar Jul 03 '17 17:07 RyanZim

Compare to the original solution (to invent "block"/"blocks" directives), I now have a updated idea:

invent nothing new, but only one non-breaking change:

  • the built-in function "include" accepts function as its first parameter

there are several advantages:

  • pure javascript gramma, with ES6 arrow function, the code looks nice too
  • it breaks nothing
  • functions can have its parameters, so include can handle function-local variables as well as context variables, like below:

template.ejs

<%- include(content, {foo: 'Foo'}) %>

page.ejs

<% const content = ({foo}) => {%>
<div><%=foo%></div>
<% }; %>
<%- include('./template', { content })%>

I'm able to get a local modification running, however the code is not prod ready. if @RyanZim and @mde agree on this, I'll happy to work on this.

Comments & thoughts are welcome!

huxia avatar Oct 10 '18 13:10 huxia

This could definitely work. One thing to keep in mind is that we ultimately want to support async/await for include.

mde avatar Nov 13 '18 14:11 mde

Hello @mde @RyanZim . My proposal: https://github.com/huxia/ejs/pull/1/files (not finalized yet, issues listed below, but would be great if you guys could take a look and share your thoughts 🙏)

Good part:

  • Works just as the example above, see the test cases I added, as a template engine, the gramma makes senses to myself, also full recursive include is supported.
  • It brings no breaking change to ejs
  • It can pass all test cases right now

Issues:

  • A __global variable & an "output stack" is invented. It doesn't looks perfect enough to myself, what's your suggestions/ideas on this?

Detailed reason: the key for this implementation is to modify the "__append" target during runtime. The scenario above for example: by the time the user's function("content" in page.ejs) is defined & parsed, the "__append" is pointed to the included file(page.ejs). So when the function is executed in other place(template.ejs), the origin content-output order is wrong, needs manually reorder.

  • A "rootTemplate" property is invented to keep the relationships amount Template instances.
  • There are some problems to implement this feature along with "options.client" support, so the above code doesn't implemented it yet
  1. It will be harder to keep the relationships amount Template instances when options.client = true
  2. because the current implementation is to do the detection when ejs "include" is called, however, when options.client = true, developers will need to duplicate this detection logic in their include callback function. Maybe I need to provide a helper function? like below?
let str = "<% let a = () => {%>Function Implementation<% }; %> Hello " 
  + "<%= include('file', {person: 'John'}); %><%- include(a)%>",
   fn = ejs.compile(str, {client: true});
// the ejs.include is a helper function to "generate" a real include callback function, it does the "including-a-function detection logic" mentioned above.
fn(data, null, ejs.include(path => clientTemplates[path])); 

I don't have much experience on ejs client mode. So not sure on this, your suggestions needed.

huxia avatar Nov 18 '18 17:11 huxia

Hi @huxia, does this feature is still planned ?

ichiriac avatar Feb 16 '19 15:02 ichiriac

Hi,

Meanwhile I've made a workaround/hack in order to avoid extending - in my case I just needed the inheritance behavior (and it works with expressjs).

// ... expressjs bootstrap & routing ...
var layoutPath = path.join(__dirname, 'views', 'layouts');
var ejs = require('ejs');
var compile = ejs.compile;
ejs.compile = function(template, opts) {
  var fn = compile(template, opts);
  return function(locals) {
    var layout = null;
    locals.layout = function(name) {
      layout = name;
    };
    var output = fn.apply(this, arguments);
    if (layout) {
      var ext = path.extname(layout);
      if (!ext) {
        layout += '.ejs';
      }
      locals.contents = output;
      layout = path.resolve(layoutPath, layout);
      ejs.renderFile(layout, locals, opts, function(err, out) {
        if (err) {
          throw err;
        } else {
          output = out;
        }
      });
    }
    return output;
  };
};

And here the usage from an views/index.ejs :

<%_ layout("default"); _%>
<h1>Welcome</html>

And here my layout views/layouts/default.ejs :

<html>...
<body>
....
<%- contents; -%>
...
</body>
</html>

This little snippet not so intrusive and avoids extra dependencies but may break if renderFile executes the cb argument async (as it may should but it doesn't today)...

I think the simplest thing to do is to introduce on ejs an hook system on compile and then it would provide a way to implement new functions like inhertance or blocks out of the box...

I've made a quick & dirty prototype in order to see how the API could be, you can take a look at it here : https://github.com/ichiriac/ejs-decorator - tell me if you're interested in a PR

ichiriac avatar Feb 16 '19 16:02 ichiriac

A hook system, meaning make the Template class an EventEmitter?

mde avatar Mar 09 '19 03:03 mde

Hi @mde, not yet sure how to achieve this, at the time I've started the comment I did not fully grasp the syntax capabilities, now I'm not so sure that would be a clean way to achieve layouts decoration.

I'm still prototyping, and searching a solution...

BTW you may be interested in this : https://github.com/ichiriac/ejs-next - same syntax but with promises support on files or outputs. The parser is about 10 times more efficient than regex, but I need to work on execution. I want to avoid reference errors when strict=false mode - and just fallback on empty entries, so I'm using slow Proxy traps :smile:

ichiriac avatar Mar 09 '19 20:03 ichiriac

I think the best approach it @huxia's one, with a slightly difference.

Actually the problem comes from how to buffer inner output in order to redirect it into a variable or option, and pass it to the layout, or anywhere else.

EJS

<% var contents = () => {@ %>
  Hello <%= name %>
<% @} %>
or 
<% var contents = function() {@ %>
  Hello <%= name %>
<% @} %>

It's intuitive and keeps the idea of plain JS

JS

var contents = function(data) {
  var locals = locals.push(data);
  with(locals) {
    echo(`Hello `);
    echo(name);
  }
  return locals.resolveOutput();
};

May introduce changes on compiler, based on the following rule :

  • {@ %> : starts a decorative closure
  • <% @} : ends a decorative closure

Also for the start part you need to detect the function prefix in order to rewrite it.

USAGE

<%= contents %>
<%= contents({ name: 'John Doe' }) %>
<%- include('layout.ejs', { contents }) %>
<%-
    include('layout.ejs', { 
      contents: function() {@ %> 
         Something here ...
      <% @},
      header: function() {@ %> 
         Something here ...
      <% @}
    })
%>

That will be my approach, it avoids extra syntax with <%* snippet foo %> that does not stick with JS and introduce a new concept of inner template parts or blocks that missed for layouts.

Next it will be easy to implement helpers like blocks dirrectly from a custom function ...

ichiriac avatar Mar 09 '19 21:03 ichiriac

Hi @huxia, does this feature is still planned ?

sorry for the late reply, I would be glad to help with the code & pr, as long as @mde @RyanZim and other maintainer agrees on this approach.

@ichiriac agrees with you, I think there should be as little avoids extra syntax as possible

A hook system, meaning make the Template class an EventEmitter?

@mde my approach here is to introduce a global output stack, which could be toggled at runtime.

I guess by some feather modification, it could become something like a EventEmitter. there maybe some cool features could come from it, the only problem is, it looks like a big rewrite here -- which I'm not so sure, however I'll be willing to help if you guys can give a specific task 😄 .

huxia avatar Mar 18 '19 08:03 huxia

Hi there, I've finished a first prototype of that implementation, with layout, blocks & async support - you can checkout the code here : https://github.com/ichiriac/ejs-next

I wanted to avoid overhead on the hook/decorator so I've keeped my implementation kiss/stupid : https://github.com/ichiriac/ejs-next/blob/master/lib/ejs.js#L103 / https://github.com/ichiriac/ejs-next/blob/master/lib/ejs.js#L188

ichiriac avatar Mar 25 '19 21:03 ichiriac

One way to use extends/block in existing versions:

page.ejs

<% const body = __append => { -%>
  <h1>H1-text</h1>
  <div>content</div>
<% } -%>
<%-include('./base', { 
  title: 'PageTitle', 
  css: '<!--#css-html#-->', 
  body, 
  footer: '<!--#js-html#-->' 
})%>

base.ejs

<% const block = (name, def = '') => {
  const fn = locals[name];
  if(!fn) return def;
  if(typeof(fn)==='string') return fn;
  const arr = [];
  fn(txt=>arr.push(txt));
  return arr.join('');
}-%>
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
    <title>
      <%-block('title', 'No title')%>
      -
      Site Title
    </title>
    <%-block('head')%>
  </head>
  <body>
    <%-block('body', 'No body')%>
    <%-block('footer')%>
  </body>
</html>

huzunjie avatar May 30 '19 02:05 huzunjie

One way to use extends/block in existing versions:

page.ejs

<% const body = __append => { -%>
  <h1>H1-text</h1>
  <div>content</div>
<% } -%>
<%-include('./base', { 
  title: 'PageTitle', 
  css: '<!--#css-html#-->', 
  body, 
  footer: '<!--#js-html#-->' 
})%>

base.ejs

<% const block = (name, def = '') => {
  const fn = locals[name];
  if(!fn) return def;
  if(typeof(fn)==='string') return fn;
  const arr = [];
  fn(txt=>arr.push(txt));
  return arr.join('');
}-%>
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
    <title>
      <%-block('title', 'No title')%>
      -
      Site Title
    </title>
    <%-block('head')%>
  </head>
  <body>
    <%-block('body', 'No body')%>
    <%-block('footer')%>
  </body>
</html>

This is why I like ejs

rambo-panda avatar Apr 27 '20 04:04 rambo-panda

Without this being supported on the official library, is the any library that extends ejs and supports it? I would prefer not to use @huzunjie 's code because it uses internal variables and force me to define block function in all layouts.

@mde is there any comments on this proposal? https://github.com/mde/ejs/issues/252#issuecomment-439708783

forrestli74 avatar Nov 12 '20 21:11 forrestli74

Any update to this conversation?

syco avatar Oct 25 '23 23:10 syco