ejs
ejs copied to clipboard
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
PR, any suggestions are welcome: https://github.com/mde/ejs/pull/251
Is anything happening with this?
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.
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!
This could definitely work. One thing to keep in mind is that we ultimately want to support async/await for include.
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
__globalvariable & 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
- It will be harder to keep the relationships amount Template instances when
options.client = true- 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.
Hi @huxia, does this feature is still planned ?
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
A hook system, meaning make the Template class an EventEmitter?
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:
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 ...
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 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
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>
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
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
Any update to this conversation?