Closed DeeHants closed 8 months ago
{{items.0}}
isn't a language feature of mustache, it's an implementation detail of some mustache implementations. as with many idiomatic language features, mustache itself has no stance on how this sort of thing should work.
in JS, a better check for "array isn't empty" is probably {{items.length}}
, as that will push a number on the context stack rather than the first element in the array.
{{items.0}}
isn't a language feature of mustache, it's an implementation detail of some mustache implementations. as with many idiomatic language features, mustache itself has no stance on how this sort of thing should work.
Slight nuance: not every programming language might indicate the first element of an array as a 0
property, but dotted names are a required part of the spec, and they have been for a long time:
Anyway, it is indeed the case that items.0
gets pushed on the context stack. This is as specified, if the programming language recognizes 0
as the first element of an array. You can work around this by using items.length
, as suggested by @bobthecow, or by adding "first": false
to the other items, because those end up higher on the context stack.
A third alternative is to reorganize your template a bit. The following version will even properly nest the list items within a <ul>
and keep the <p>
outside of it. I used items.0
here, but you could also use items.length
.
{{#items}}
{{#first}}
<p>found items</p>
<ul>
<li><strong>{{name}}</strong></li>
{{/first}}
{{#link}}
<li><a href="{{url}}">{{name}}</a></li>
{{/link}}
{{/items}}
{{#items.0}}
</ul>
{{/items.0}}
{{^items.0}}
<p>no items</p>
{{/items.0}}
Side note: mustache.js, which powers the well-known demo page, unfortunately does not adhere to the spec very well. You can tell so in this case because it strips all indentation. If I take your original template and input data through an implementation that does adhere to the spec, I get the following output:
<p>found items</p>
<li><strong>red</strong></li>
<li><strong>green</strong></li>
<li><a href="#Green">green</a></li>
<li><strong>blue</strong></li>
<li><a href="#Blue">blue</a></li>
Coincidentally, I am working on a new implementation for JavaScript that does adhere to the spec, and also on a playground site where you can try templates in a spec-compliant way. If you're interested, have a look at Wontache and perhaps consider donating to my Patreon.
Slight nuance: not every programming language might indicate the first element of an array as a 0 property, but dotted names are a required part of the spec, and they have been for a long time
Right. I wasn't trying to say that dotted names were a language-specific feature, rather grabbing the 0th element of an array at all was language-specific.
Note that items.length
depends on the implementation as well. It's idiomatic javascript (and ruby, and a bunch of other things) but it's not a feature of Mustache itself. So, for example, in PHP where arrays are a primitive and not an object, there's no built-in length
method or property.
Thanks everyone. We can preprocess the data too to add a length
field.
The wording of that section spec also suggests to me the perl implementation is not handling dotted entries correctly. I'll double check once at work.
@DeeHants What did you find?
It would be really nice if there was a spec'ed way to deal with this like 99% use case of dealing with lists.
There are three exceptionally common use cases where the only option is to re-decorate the list:
<ul>
elements),
)So many implementations have varying extensions for dealing with the above. Ideally you would have a lambda do it but as I mentioned here: https://github.com/mustache/spec/issues/135#issuecomment-1285576618
It is not easily possibly even with enhanced lambdas as ambiguity comes when referencing a list. For example if I'm passed the context stack can I get access to the list (e.g. contextStack[length - 2]
)?
It is just sad that this is such an incredibly common use case without a clear option that doesn't vary greatly from implementation to implementation other than mindlessly redecorating the model (which in some cases is not easily possibly particularly with immutable objects etc).
Maybe power lambdas can get direct access to lists. Then you just have to port a lambda to other implementations.
@agentgt Yes, I think power lambdas could and should solve this issue. 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:
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.
Given such a hypothetical second argument (named magic
below), here is how I might write power lambdas in JavaScript that make these common use cases possible. empty
and enumerate
basically redecorate the context on the fly, so you no longer need to do it in ad hoc preparatory code.
Data with power lambdas
{
// Pushes a new frame on the stack that shadows all keys currently visible.
// Each key on the new frame is a lambda that lazily checks whether the
// corresponding key on the lower frames is an empty list.
empty: function(section, magic) {
var allKeys = magic.somehowGetAllKeys();
var decoratedFrame = {};
function createChecker(key) {
// Crucial: the following function closes over the `magic` that was passed
// to the `empty` lambda, so it resolves against lower frames only.
return function() {
return magic.somehowResolve(key).length === 0;
};
}
for (var l = allKeys.length, i = 0; i < l; ++i) {
var key = allKeys[i];
decoratedFrame[key] = createChecker(key);
}
return decoratedFrame;
},
// Pushes a shadowing frame, similar to `empty`. However, each key pushes
// a new list on the stack that shadows the original list. Each item in the
// new list has the same contents as the corresponding element of the
// underlying list, but an `index` property is added with its numerical
// position in the list.
enumerate: function(section, magic) {
var allKeys = magic.somehowGetAllKeys();
var decoratedFrame = {};
function createEnumerated(key) {
return function() {
var list = magic.somehowResolve(key);
if (!(list instanceof Array)) return list;
var decoratedList = [];
for (var l = list.length, i = 0; i < l; ++i) {
decoratedList.push({...list[i], index: i});
}
return decoratedList;
};
}
for (var l = allKeys.length, i = 0; i < l; ++i) {
var key = allKeys[i];
decoratedFrame[key] = createEnumerated(key);
}
return decoratedFrame;
},
// Check whether we are currently rendering the first element of a list.
// Only works inside an `{{#enumerate}}{{/enumerate}}`.
first: function(section, magic) {
var index = magic.somehowResolve('index');
return index === 0;
},
// Check whether we are currently rendering the last element of a list.
// Only works inside an `{{#enumerate}}{{/enumerate}}`.
last: function(section, magic) {
var index = magic.somehowResolve('index');
var frameIndex = magic.somehowGetFrameIndexOf('index');
// Next line assumes that lower frames have lower indices.
var list = magic.somehowGetFrame(frameIndex - 1);
return index === list.length - 1;
}
}
Template with example usage
{{! rendering something only if a list is not empty, but only once, regardless
of list length }}
{{^empty.myList}}
<ul>
{{/empty.myList}}
{{#myList}}
<li>{{item}}
{{/myList}}
{{^empty.myList}}
</ul>
{{/empty.myList}}
{{! rendering something between elements, but not before or after }}
I like {{#enumerate.myList
}}{{^first}}{{^last}}, {{/last}}{{#last}} and {{/last}}{{/first}}{{item}}{{/
enumerate.myList}}.
Not sure if this counts as a bug, and seems to be by design as per the wording of the docs, and (at least) the perl and JS implementations, but a gotcha...
We make extensive use of
{{#items.0}}
type checks to see if an array has any items before rendering the container and the items themselves ({{#items}}{{value1}}{{value2}}{{/items}}
). This gives example code like:The issue here is that the 0th item is now part of the context stack, meaning that if items 1+ do not have a specific named value, but the 0th does, that value will be rendered instead. If you put the above mustache into the demo, you get this broken output:
It now thinks every item is "first" as it pulls that value from the
{{#items.0}}
context.We can work around it in some cases, by changing
{{subitem.value}}
(which also exists at.0
) to{{#subitem}}{{value}}{{/subitem}}
. This works, as the.n
entries will have asubitem
which becomes the new context, and then looks forvalue
which.0
(and.n
) doesn't.