Skip to content
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

Handlebars syntax for the (get) helper #379

Closed
mmun opened this issue Sep 21, 2018 · 33 comments
Closed

Handlebars syntax for the (get) helper #379

mmun opened this issue Sep 21, 2018 · 33 comments

Comments

@mmun
Copy link
Member

mmun commented Sep 21, 2018

What do you think about introducing a "dynamic access" syntax for Handlebars to streamline or even replace the need for the get helper?

{{planets[name]}}

would be equivalent to {{get planets name}}, and

{{planets[name].closestPlanet[field]}}

would be equivalent to

{{get (get (get planets name) 'closestPlanet') field}}

The second example clearly demonstrates that using the dynamic access syntax makes it easier to understand what is being accessed.

@mmun
Copy link
Member Author

mmun commented Sep 21, 2018

This situation gets a bit more complicated if we include some allowances for subexpressions:

{{!-- How would these be represented with the dynamic access syntax? --}}

{{get (planets-near-star star) name}}
{{get planets (favourite-planet-name user)}}

I'd suggest

{{(planets-near-star star)[name]}}
{{planets[(favourite-planet-name user)]}}

@nightire
Copy link

Is this form allowed/reasonable?

{{planets[name[closestPlanet[field]]]}}

@mmun
Copy link
Member Author

mmun commented Sep 21, 2018

@nightire Yeah. I'd expect the grammar to be something as simple and generic as

GetExpression = Expression "[" Expression "]"

@buschtoens
Copy link
Contributor

I think we can PoC or actually properly implement this as a template transform addon to avoid changing handlebars itself.

@mmun
Copy link
Member Author

mmun commented Sep 21, 2018

@buschtoens Unfortunately, {{planets[name]}} will fail to parse. For a PoC, you might be able to hijack the largely unknown and unused escaping syntax, {{planets.[name]}}, but it will be a be a little messy.

@Panman82
Copy link

How far of a difference between http://handlebarsjs.com/ and Ember's flavor are we at this point? In that, is this something that should be requested to be added to the official Handlebars.js project, or is Ember beyond that point? Also, adoption of editor syntax highlighters would probably be better if added to the official Handlebars.js project.

@rwjblue
Copy link
Member

rwjblue commented Sep 24, 2018

@Panman8201 - AFAICT @mmun is suggesting adding this to the main handlebars parser (which we would then update to in glimmer-vm)...

@rwjblue
Copy link
Member

rwjblue commented Sep 24, 2018

@mmun - I'm sold! 😸

@Panman82
Copy link

@rwjblue Guess I was questioning it because this place is for Ember RFC's. Does Handlebars.js have their own RFC process this should go through?

@btecu
Copy link

btecu commented Sep 24, 2018

This introduces a very different syntax than what we currently have, as we don't have anything with [] in the template.

Since it's solving more of an edge case, I don't think it should be added as it would add to the list of things needed learn to use ember.

@buschtoens
Copy link
Contributor

@btecu

Since it's solving more of an edge case, I don't think it should be added as it would add to the list of things needed learn to use ember.

If we did not add this syntax, users would still have to learn about the {{get something key}} helper.

I would argue that most Ember new-comers never worked with Handlebars before. And I can tell from my own and co-worker's experience, that most seem to intuitively expect the {{something[key]}} syntax to work.

So actually, I think this decreases the learning curve. 😊

@btecu
Copy link

btecu commented Sep 25, 2018

@buschtoens yes, but the users are learning about helpers anyway, so that example of yours ({{get something key}}) will make complete sense as opposed to something that has [] in it.

@buschtoens
Copy link
Contributor

buschtoens commented Sep 25, 2018

@btecu Would {{someObj[someKey]}} not make sense to you? 🤔
This is exactly the same syntax as in JavaScript.

So far, {{get}} won't be deprecated. This is still pretty far ahead and might never be done at all. So you can still use {{get}}, if you prefer it. 😊

@btecu
Copy link

btecu commented Sep 25, 2018

@buschtoens I get it, since I already read this RFC.

This is exactly the same syntax as in JavaScript.

Exactly my point, this is not React to have JS in the template. 😃

I'm not worried about {{get}} since I'm not sure I ever had to use it, it was more about introducing different syntax.

@robclancy
Copy link

I will give a kidney for this feature. no bamboozle

@mehulkar
Copy link
Contributor

Adding this seems like a step away from the "lisp-ish" syntax that Handlebars/Ember templates tout as a feature. What would prevent making a case for something like {{if (foo === bar)}} in the future?

@richard-viney
Copy link
Contributor

Question: is there any consideration here in regard to ever wanting to use [] syntax to look up arrays in templates?

@buschtoens
Copy link
Contributor

@richard-viney Do you mean creating an array on the fly?

{{if (contains needle ["foo", "bar"]) "found it!"}}

instead of

{{if (contains needle (array "foo" "bar")) "found it!"}}

@richard-viney
Copy link
Contributor

@buschtoens I was meaning simple index lookup, i.e. exactly the same as in JS: array[index].

@buschtoens
Copy link
Contributor

@richard-viney Ah, I understand. 😊
I guess that this is already implied, because you can use the (get) helper for getting an element from an array.

@acorncom
Copy link
Contributor

I'm personally a bit ambivalent on this, as despite headaches that we run into now and then (particularly in deeper loops), part of what I appreciate about Handlebars syntax at the moment is that it constrains access to things and helps keep things relatively simple. At the moment, despite some pretty sophisticated and complex helpers, as a community we still seem to value simpler templates with more complex logic in components. This would seem to be a shift away from that ...

@shankarsridhar
Copy link

Aren't expressions in handlebars supposed to be kept as simple as possible ?
Even though I like this idea at a high level, I feel like this would encourage ember devs to place relatively more complex and higher amount of logic in the .hbs rather than the .js files.

Based on the original example, anything like that can easily moved to an helper or a computed property. But again this could be just a personal preference, unless there are some performance reasons advising against it.

@wycats
Copy link
Member

wycats commented Sep 29, 2018

I think that people are right to be nervous about this. I am in favor of this particular syntax change, but only after carefully evaluating the Handlebars and the Glimmer grammar, and working through the reasons that this syntax is less disruptive than it looks.

This analysis also gives us a way to evaluate future syntax extensions.

Adjacent Expressions

One of the most constraining syntax choices of Handlebars was the decision to leave out commas between expressions. This means that there are many places in the grammar where two expressions are literally adjacent to each other.

{{t "hello" "world"}}

In this example, we are calling the t helper with a list of two expressions: "hello" and "world". So far, so good.

But not all expressions are simple literals. Let's take a look at paths:

{{t my.name is.wycats}}

In this example, we are calling the t helper with a list of two expressions: my.name and is.wycats. This is still ok because it's illegal to start any Handlebars expression with a .. If an expression could start with a period, we wouldn't be able to tell the difference between the two expressions my.name .is.wycats and the expression my.name.is.wycats.

The general rule here is that if it is possible to continue a complete expression with any token (my is a valid expression, and we're continuing it with .), that token cannot also be valid at the beginning of an expression.

For the rest of this comment, the term continuation token is defined to mean a token that is permitted to follow a valid expression to produce a longer valid expression.

The term start token is defined to mean a token that is valid at the beginning of an expression.

The rule we just described can be summarized as: in Handlebars, a token cannot be both a continuation token and a start token.

Note: Unary operators are start tokens, while infix operators are continuation tokens. Right now, Glimmer has one infix operator (.) and zero unary operators. Therefore, this conflict cannot happen by definition.

Other Languages

Handlebars has an extremely restrictive grammar, but let's take a look at JavaScript.

In JavaScript + is a valid unary operator (+a) and also a valid binary operator (x + y).

Let's say we tried to allow both of these syntaxes in Handlebars:

{{t x + y}}

Superficially, this looks fine. It's "clearly" a call to the helper t with a single expression argument: x + y.

But what about this:

{{t x + y + z}}

This is ambiguous: it might mean a call to t with two expressions as arguments (x + y and +z) or it might mean a call to t with one expression as an argument (x + y + z).

The general analysis from above applies: because + is a continuation token and also a start token, adding these two expression forms to Handlebars would produce a fatal ambiguity.

Note that JavaScript carefully avoids this problem by carefully avoiding adjacent expressions. Whenever two expressions are placed side by side in a list, there is always some kind of separator that is not a start token (usually ,).

Ambiguity in Handlebars

This analysis is a big part of what keeps the Handlebars grammar simple. Because we allow adjacent expressions, we are prevented from casually adding infix or unary operators. This is a property that people like a lot about the Handlebars template language, and it's nice that this analysis shows us that it would be hard to go too far down the slippery slope for a simple technical reason.

About the [] Indexing Operator

We now have the tools necessary to evaluate possible problems caused by the [] indexing operator.

First of all, it's clear that adding this operator would introduce a new continuation token, since indexing is always applied directly to a previous expression.

Since it introduces a continuation token, that means that it will prevent us from ever using [ as a start token. In particular, it would prevent us from ever adding [ ... ] as an array literal or hash literal syntax:

{{t x.y[z]}}

If we introduced [...] array literal syntax and the indexing operator, there is an ambiguity. Does this mean a call to t with two expressions as arguments (x.y and [z]) or a call to t with one expression as an argument (x.y[z]).

If we believe that indexing is more important than array literals and hash literals (which would probably be added as [ x=expr y=expr ]), then this change is totally fine, and I think, not too much of a slippery slope (because allowing adjacent expressions already prevents us from doing anything too wild).

@wycats
Copy link
Member

wycats commented Sep 29, 2018

P.S. This analysis also illustrates why we can't add () call syntax.

Today, ( is a start token (it starts a subexpression). If we allowed expr(...), it would also be a continuation token. Bzzzt.

Here's the ambiguity:

{{t hello(world)}}

Does hello(world) mean two expressions: hello and (world) or one expression: hello(world)?

@mehulkar
Copy link
Contributor

mehulkar commented Oct 2, 2018

@wycats that was a really great read on how you think about this, thanks for posting.

That said, I'm not sure your explanation addresses the general concern of adding syntax to Handlebars. (For me, it actually added an additional concern of would we ever want [ as a start token?)

To re-iterate, it's great to know that adding a [] accessor would not add ambiguity for the compiler. But to humans, it still adds ambuiguity by moving away from the "lisp" syntax1 and closer to a JS syntax. (I don't generally think of Handlebar expressions as a separate language from JS and I'd wager that I'm not unique in this.)

To further illustrate this ambiguity in context of your post, there are other, unused characters that can still be introduced as continuation or start tokens. For example, if {{if foo[bar]}} works, it's more likely for someone to expect {{if foo === bar}} to work, and make a case for adding that to the language.

This puts us in the situation where we (1) have to make hard decisions about continuation vs start tokens and (2) move closer to JS syntax without being able to support all of it, which ends up being a bad experience and also moves an arbitrary line for existing users and new users. In my opinion, it's not a huge win to add this behavior at least until Ember.get and this.get are commonly used. If get was not a core Ember concept, the case for removing {{get}} would feel much stronger to me.

Footnotes

  1. I say "lisp" syntax, but I don't know Lisp, and I am only kinda sorta familiar with what is meant when people say that Handlebars is a lisp syntax, so I might be using it wrong.

@robclancy
Copy link

(2) move closer to JS syntax without being able to support all of it, which ends up being a bad experience

The lisp syntax is already a bad experience. Doesn't matter how many years I use it for it sucks. Still better than alternatives mixing logic into HTML syntax. Handlebars would be perfect if it didn't have such hardcore restrictions from the start because of some philosophy about logic in controllers or something. Easily my number 1 issue with using ember. Hell you mix components and helpers as the exact same syntax (except 1 can be block)... hows that for your ambiguity.

@mehulkar
Copy link
Contributor

mehulkar commented Oct 2, 2018

Handlebars would be perfect if it didn't have such hardcore restrictions

Some of the restrictions are technical, some of them are philosophical. I'd say the philosophical restrictions are largely handled by 3rd party addons.

Hell you mix components and helpers as the exact same syntax

That's changing pretty soon with angle bracket components

The lisp syntax is already a bad experience.

¯_(ツ)_/¯ I don't mind it so much, but that's neither here nor there.

@luxzeitlos
Copy link

@wycats sorry if I'm wrong, but wouldn't this work by just tokenizing whitespaces as seperators?

I feel like {{t foo . bar}} is and should not be valid syntax. A continuation token AFAIK may never be preceded by a whitespace. A start token however always is either preceded by a whitespace at the begin of a expression.

So {{t x.y[z]}} should tokenize different then {{t x.y [z]}} right?

So the first (and the later if whitespaces are not tokenized) would would tokenize to something like this:

[
  { token: 'HBS_INLINE_START' },
  { token: 'LITERAL', value: 't' },
  { token: 'LITERAL', value: 'x' },
  { token: '.' },
  { token: 'LITERAL', value: 'y' },
  { token: '[' },
  { token: 'LITERAL', value: 'z' },
  { token: ']' },
]

However the later (if whitespaces are tokenized) should result in something like this:

[
  { token: 'HBS_INLINE_START' },
  { token: 'LITERAL', value: 't' },
  { token: 'LITERAL', value: 'x' },
  { token: '.' },
  { token: 'LITERAL', value: 'y' },
  { token: 'WHITESPACE' }
  { token: '[' },
  { token: 'LITERAL', value: 'z' },
  { token: ']' },
]

So shouldn't a grammar like this work?

<Expression>           ::= <Expression_Part>
                         | <Expression_Part> <WHITESPACE> <Expression>
<Expression_Part>      ::= <Lookup>
                         | <Array_Literal>
                         | <(> <Expression> <)>
<Lookup>               ::= <LITERAL>
                         | <LITERAL> <Lookup_Following>
<Lookup_Following>     ::= <.> <Lookup_Following>
                         | <[> <Expression_Part> <]>
                         | <[> <Expression_Part> <]> <Lookup_Following>
<Array_Literal>        ::= <[> <Array_Literal_Content> <]>
<Array_Literal_Content>::= <Expression_Part>
                         | <Expression_Part> <WHITESPACE> <Array_Literal_Content>

@locks
Copy link
Contributor

locks commented Oct 4, 2018

@luxferresum

@wycats sorry if I'm wrong, but wouldn't this work by just tokenizing whitespaces as seperators?

Isn't this a massive breaking change?

@mehulkar

But to humans, it still adds ambuiguity by moving away from the "lisp" syntax1 and closer to a JS syntax. (I don't generally think of Handlebar expressions as a separate language from JS and I'd wager that I'm not unique in this.)

The Lispiness of Handlebars can be referring to a couple different concepts. In terms of syntax, I think that adding === as {{if foo === bar}}, would move the needle away from the Lispiness of Handlebars more because you would be introducing an infix operator. To maintain coherence it would have to be {{if (=== foo bar)}}, a prefix operator that is then evaluated (()) and the resulting value passed to the if operator.
I think using [] for array literals (disclaimer: I'm on that camp) is also in line with the Lispiness. Clojure uses it, for example.

Handlebars is definitely a separate language from JavaScript with its own syntax and semantics. The overlap I think is in how thruthiness and falsiness are handled. If I remember correctly, JavaScript semantics are maintained? I think the bracket accessor syntax is in a couple of syntax, right? So it would overlap with a bunch of host languages.

This puts us in the situation where we (1) have to make hard decisions about continuation vs start tokens

This doesn't sound like a new situation ;)

@luxzeitlos
Copy link

@locks

Isn't this a massive breaking change?

I'm not sure what you mean - what existing syntax/API would break?
This seems more kind of a implementation detail for me. For me it looks like the downside is more the additional compiler complexity.

@sandstrom
Copy link
Contributor

I'm doing some issue gardening 🌱🌿 🌷 and came upon this issue. Since it's quite old I just wanted to ask if this is still relevant? If it isn't, maybe we can close this issue?

By closing some old issues we reduce the list of open issues to a more manageable set.

@wagenet
Copy link
Member

wagenet commented Jul 23, 2022

I'm closing this due to inactivity. This doesn't mean that the idea presented here is invalid, but that, unfortunately, nobody has taken the effort to spearhead it and bring it to completion. Please feel free to advocate for it if you believe that this is still worth pursuing. Thanks!

@wagenet wagenet closed this as completed Jul 23, 2022
@NullVoxPopuli
Copy link
Contributor

NullVoxPopuli commented Jul 23, 2022

With <template> / strict mode, this gets quite a bit easier.

instead of

{{get (get (get planets name) 'closestPlanet') field}}

you can do

const closestPlanet = (planets, name, field) => planets[name].closestPlaned[field];

<template>
  {{closestPlanet planets name field}}
  assuming planets, name, and field are defined "somewhere"
</template>

and even pre-strict mode, and post 3.25, you can do:

export default class Demo extends Component {
  closestPlanet = (planets, name, field) => planets[name].closestPlaned[field];
}
  {{this.closestPlanet planets name field}}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests