Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

{{#item.0}} checks and context #138

Closed
DeeHants opened this issue Sep 5, 2022 · 7 comments
Closed

{{#item.0}} checks and context #138

DeeHants opened this issue Sep 5, 2022 · 7 comments

Comments

@DeeHants
Copy link

DeeHants commented Sep 5, 2022

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:

{{#items.0}}
  <p>found items</p>
  {{#items}}
    {{#first}}
      <li><strong>{{name}}</strong></li>
    {{/first}}
    {{#link}}
      <li><a href="{{url}}">{{name}}</a></li>
    {{/link}}
  {{/items}}
{{/items.0}}
{{^items.0}}
  <p>no items</p>
{{/items.0}}

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:

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

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 a subitem which becomes the new context, and then looks for value which .0 (and .n) doesn't.

@bobthecow
Copy link
Member

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

@jgonggrijp
Copy link
Member

{{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:

spec/specs/sections.yml

Lines 9 to 26 in 5d3b58e

This tag's content names the data to replace the tag. Name resolution is as
follows:
1) Split the name on periods; the first part is the name to resolve, any
remaining parts should be retained.
2) Walk the context stack from top to bottom, finding the first context
that is a) a hash containing the name as a key OR b) an object responding
to a method with the given name.
3) If the context is a hash, the data is the value associated with the
name.
4) If the context is an object and the method with the given name has an
arity of 1, the method SHOULD be called with a String containing the
unprocessed contents of the sections; the data is the value returned.
5) Otherwise, the data is the value returned by calling the method with
the given name.
6) If any name parts were retained in step 1, each should be resolved
against a context stack containing only the result from the former
resolution. If any part fails resolution, the result should be considered
falsey, and should interpolate as the empty string.

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.

@bobthecow
Copy link
Member

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.

@DeeHants
Copy link
Author

DeeHants commented Sep 6, 2022

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.

@jgonggrijp
Copy link
Member

@DeeHants What did you find?

@agentgt
Copy link

agentgt commented Oct 20, 2022

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:

  1. Check if the list is empty with out iterating on it (use case is omitting surrounding <ul> elements)
  2. Know if we are on the first item in the list (use case is joining items with ,)
  3. Know if we are on the last item in the list

So many implementations have varying extensions for dealing with the above. Ideally you would have a lambda do it but as I mentioned here: #135 (comment)

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.

@jgonggrijp
Copy link
Member

@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:

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

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

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants