handlebars.js icon indicating copy to clipboard operation
handlebars.js copied to clipboard

functions in nested objects are not bound to that object, resulting in inconsistent behavior

Open vassudanagunta opened this issue 3 years ago • 6 comments

Given the following input:

const context = {
    nested: {
        awesome: function () {
            return this.more;
        },
        more: 'Deeply awesome'
    },
    more: 'More awesome'
}

the following template:

{{nested.awesome}}

yields 'More awesome', whereas the equivalent Javascript:

context.nested.awesome()

yields 'Deeply awesome'

More importantly,

{{#with nested}}
    {{awesome}}
{{/with}}

also yields 'Deeply awesome'.

Here is the js-fiddle demonstrating this

PR submitted with a failing test case: #1861.

vassudanagunta avatar May 10 '22 21:05 vassudanagunta

This doesn't seem to be a bug but wrong expectations: JavaScript and Handlebars are not the same and therefore don't work the same. A scope in Handlebars means something different than in JavaScript: With context.nested.awesome() the function in JavaScript looks up the scope it's in whereas in Handlebars the scope is set to where you call something from. Calling {{nested.awesome}} still means that you are in @root and call something from there, no matter how deep it is nested. So even {{nested.inside.some.other.object.awesome}} would still call awesome() from within the @root scope. Therefore the this in your function awesome() is taken from the @root (as it's called from there) and return this.more; means @root/more, so More awesome. The way to change the scope would be using {{#with ...}}:

{{#with nested}}
    {{awesome}}
{{/with}}

This outputs Deeply awesome. Now you call {{awesome}} with the scope of @root/nested. From within this scope you could call awsome() also like {{@root/nested/awesome}} or {{../nested/awesome}} but you're still passing it the scope @root/nested.

Handlebars doesn't make a difference between a registered helper function or a function from within the context. The context of the function is always bound to where you call it from.

PitPik avatar Jun 19 '22 10:06 PitPik

JavaScript and Handlebars are not the same

Sure, but in this case we are talking specifically about a Javascript feature and the Javascript Handlebars library. We are talking specifically about how Javascript implemented contexts with Javascript functions as properties in that context behave. It stands to reason that such functions behave in a way that is consistent with and natural to Javascript.

Much more importantly, it also stands to reason, from the template author's side, without even knowing that the context is implemented in Javascript or even that the awesome property is a function, that:

{{#with nested}}
    {{awesome}}
{{/with}}

and

{{nested.awesome}}

yield the exact same thing, just as if awesome were a plain old property.

I would expect this consistency from any other Handlebars library, be it Python, Rust, Golang or any other language implementations.

Perhaps I should update the title of this issue.

vassudanagunta avatar Jul 12 '22 16:07 vassudanagunta

@jaylinski, @ilharp, or @nknapp, if you confirm that this is a bug that should be fixed, I could take a shot at a PR. Does the label indicate that?

vassudanagunta avatar Jul 12 '22 17:07 vassudanagunta

@PitPik has some valid points in his response, I'm not sure if this is really a bug, but wrong expectations.

This behavior should be defined as part of a specification. (But so far all attempts to write a specification failed: https://github.com/handlebars-lang/spec, https://github.com/handlebars-lang/specification)

jaylinski avatar Jul 12 '22 18:07 jaylinski

@jaylinski Kinda sounds like you're saying "it is what it is". Is it because the various implementations are inconsistent and also too entrenched to impose any consistency?

Which points of @PitPik are valid? Are none of mine? Honest question.

vassudanagunta avatar Jul 12 '22 18:07 vassudanagunta

In this case Handlebars mimics the behavior of Mustache. There is no {{#with}} helper in mustache, but {{#nested}}{{awesome}}{{/nested}} would be the same thing.

This fiddle demonstrates it: https://jsfiddle.net/ob5ezgcn/6/

{{nested.awsome}} => More awesome
{{#nested}}{{awesome}}{{/nested}} => Deeply awesome

I am not sure if it is specified in the Mustache spec explicitly.

Personally, I would advise against the use of functions in the context. This feature is a relict due to the compatibility with Mustache. Use a helper instead. Helper-behavior is much more constent.

In my eyes, the primary use case of Handlebars is rendering JSON-like objects.

nknapp avatar Jul 13 '22 21:07 nknapp