ejs: block/template/extend support for ejs

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

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 12
  • Comments: 17 (2 by maintainers)

Most upvoted comments

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>

Any update to this conversation?

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

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

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

Hi @huxia, does this feature is still planned ?

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