-
Notifications
You must be signed in to change notification settings - Fork 74
Description
2023-10-31 update: the newest version of my proposal is here.
2022-10-21 update: the following excerpt from #138 (comment) summarizes how I currently envision power lambdas.
The way I currently think of it, lambdas receive a second argument which somehow (i.e., in an implementation-defined way) makes the following things possible:
- retrieve any context frame;
- retrieve a list of all keys visible in the current (full) context;
- resolve any key against the current (full) context;
- identify the context frame in which a key is resolved;
- render a template of choice against the current context.
Whereby lambdas must not modify the pre-existing contents of the context, and implementations are welcome to actively prevent this if the programming language can enforce it. However, lambdas can (already) push a new frame on the stack, which still has the net effect of changing what's available in the context.
2022-10-16 update: added playground links and savestates.
This is a feature I've meant to implement for a while, and which I was planning to propose to the spec after implementing it. However, it seems relevant to the discussion about dynamic partials that is currently taking place with @anomal00us, @gasche and @bobthecow in #134 and #54, so I decided to describe the idea now already.
The context name resolution algorithm described in the current interpolation, section and inverted section specs states that if any name in a dotted path happens to refer to a function (method), that function is called in order to obtain the next frame from which to resolve the next name in the path (or the final result). In case of a section or inverted section, the function is also passed the contents of the section as a string argument, if it is capable of accepting an argument. Illustration with JavaScript lambda code:
{{#key.lambda.otherKey}}Hello {{name}}{{/key.lambda.otherKey}}
{
key: {
lambda(section) {
// section will be the string 'Hello {{name}}'
return {
otherKey: { name: 'John' }
};
}
}
}
Hello John
Try the above example in the playground by pasting the following code:
{"data":{"text":"{\n key: {\n lambda(section) {\n // section will be the string 'Hello {{name}}'\n return {\n otherKey: { name: 'John' }\n };\n }\n }\n}"},"templates":[{"name":"","text":"{{#key.lambda.otherKey}}Hello {{name}}{{/key.lambda.otherKey}}"}]}
The lambdas spec adds to this that if the last name in the path is a function and it returns a string, then that string is rendered as a template before finally being substituted for the interpolation or section tag.
{{#key.lambda}}Hello {{name}}{{/key.lambda}}
{
key: {
lambda(section) {
// section will be the string 'Hello {{name}}'
return 'I changed my mind about the template, {{name}}';
}
},
name: 'John'
}
I changed my mind about the template, John
Try the above example in the playground by pasting the following code:
{"data":{"text":"{\n key: {\n lambda(section) {\n // section will be the string 'Hello {{name}}'\n return 'I changed my mind about the template, {{name}}';\n }\n },\n name: 'John'\n}\n"},"templates":[{"name":"","text":"{{#key.lambda}}Hello {{name}}{{/key.lambda}}"}]}
I would like to suggest another optional extension spec on top of this, which I'll dub "power lambdas" for now. It amounts to passing an additional argument to functions that can accept it, containing a (read-only) view of the full context stack. I'll illustrate a couple of advanced features that could be implemented using power lambdas below. For now I'll assume that the context stack is passed as the most conventional sequence type of the programming language, with the top of the context stack as the last element. It could also be left to the implementation whether the stack frames are ordered top or bottom first.
Access to properties of lower context stack frames that have been hidden by higher frames:
{{#nested1}}
{{#nested2}}
{{name}}
{{parentScope.name}}
{{rootScope.name}}
{{/nested2}}
{{/nested1}}
{
parentScope(section, stack = section) {
return stack[stack.length - 2];
},
rootScope(section, stack = section) {
return stack[0];
},
name: 'John',
nested1: {
name: 'Lizzy',
nested2: {
name: 'Deborah'
}
}
}
Deborah
Lizzy
John
Hash iteration by key-value pairs like in Handlebars's #each
helper:
{{#nested}}{{#byKey}}
The {{key}} is called {{value}}.
{{/byKey}}{{/nested}}
{
byKey(section, stack) {
var context = stack[stack.length - 1];
var pairs = [];
for (var key in context) {
pairs.push({key, value: context[key]});
}
return pairs;
},
nested: {
'police officer': 'Jenny',
'doctor': 'Hildegard',
'hairdresser': 'Tim'
}
}
The police officer is called Jenny.
The doctor is called Hildegard.
The hairdresser is called Tim.
Dynamic partials:
{{!template.mustache}}
{{animal}} goes:
{{#dereference.>}}animal{{/dereference.>}}
{{!cow.mustache}}
Moo!
{{!dog.mustache}}
Woof!
{
dereference(section, stack) {
// This example presumes that there is a way to render a template with a prepared
// template stack instead of just a plain view that will become the root of a new
// context stack. Offering such an interface will be attractive for implementations
// that support power lambdas.
var name = renderMustache('{{' + section + '}}', stack);
return {
'>': function() { return '{{>' + name + '}}'; }
// I'm oversimplifying here, you could do this for all sigils without code
// duplication.
};
},
animal: 'cow'
}
cow goes:
Moo!