Open huxia opened 7 years ago
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:
there are several advantages:
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 🙏)
__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
- 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.
<% var contents = () => {@ %>
Hello <%= name %>
<% @} %>
or
<% var contents = function() {@ %>
Hello <%= name %>
<% @} %>
It's intuitive and keeps the idea of plain 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 closureAlso for the start part you need to detect the function prefix in order to rewrite it.
<%= 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>
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?
Currently ejs doesn't have any block/template/extend features.
existing solutions:
this approach:
page implementation (home.ejs):
template/layout declaration (layout.ejs):
advantages