Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

block/template/extend support for ejs #252

Open
huxia opened this issue Apr 17, 2017 · 19 comments
Open

block/template/extend support for ejs #252

huxia opened this issue Apr 17, 2017 · 19 comments

Comments

@huxia
Copy link

huxia commented Apr 17, 2017

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.

tj/ejs#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: block/template/extend support for ejs #252 (comment)
@huxia
Copy link
Author

huxia commented Apr 17, 2017

PR, any suggestions are welcome: #251

@huxia huxia changed the title block/template support for ejs block/template/extend support for ejs Apr 17, 2017
@rossrossp
Copy link

Is anything happening with this?

@RyanZim
Copy link
Collaborator

RyanZim commented Jul 3, 2017

See #251 for discussion; I'm not going to merge anything on this front without permission from the other maintainers.

@huxia
Copy link
Author

huxia commented Oct 10, 2018

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!

@mde
Copy link
Owner

mde commented Nov 13, 2018

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

@huxia
Copy link
Author

huxia commented Nov 18, 2018

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.

@ichiriac
Copy link

Hi @huxia, does this feature is still planned ?

@ichiriac
Copy link

ichiriac commented Feb 16, 2019

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

@mde
Copy link
Owner

mde commented Mar 9, 2019

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

@ichiriac
Copy link

ichiriac commented Mar 9, 2019

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 😄

@ichiriac
Copy link

ichiriac commented Mar 9, 2019

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 ...

@huxia
Copy link
Author

huxia commented Mar 18, 2019

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 😄 .

@ichiriac
Copy link

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

@huzunjie
Copy link

huzunjie commented May 30, 2019

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>

@rambo-panda
Copy link

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

@forrestli74
Copy link

forrestli74 commented Nov 12, 2020

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?
#252 (comment)

@syco
Copy link

syco commented Oct 25, 2023

Any update to this conversation?

@noonien
Copy link

noonien commented Dec 10, 2024

I'm also interested in this, currently using huzunjie's method, but would like to avoid using the __append private variable or overriding outputFunctionName.

@mde
Copy link
Owner

mde commented Dec 14, 2024

Still considering this, but right now focused on a v4 update to use ESM in the EJS source code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants