rfcs
rfcs copied to clipboard
Sugar for multi-assignment with `let` block helper
Using block let
to assign multiple template variables quickly becomes a pain to read the more variables you want to assign, especially when you're assigning values derived from more complex expressions:
{{#let (if cond valueA valueB) (some-helper arg1 arg2) (hash a=1 b=2) as |var1 var2 var3|}}
{{var1}}
{{var2}}
{{var3}}
{{/let}}
This is already pretty painful to read and it's a simple toy example.
IMO the reason why this is painful to read is because there's cognitive overhead required to positionally map the values with the variable they're being assigned to. To fix this, I've seen the following pattern used:
{{#let (hash
var1=(if cond valueA valueB)
var2=(some-helper arg1 arg2)
var3=(hash a=1 b=2))
as |vars|}}
{{vars.var1}}
{{vars.var2}}
{{vars.var3}}
{{/let}}
This fixes the mental overhead of positional mapping, but it introduces the overhead of naming the hash of the variables as well as the overhead of having to grab those variables off the hash everywhere.
I propose the following syntactic sugar for multi-assignment with block let
:
{{#let
(if cond valueA valueB) as |var1|
(some-helper arg1 arg2) as |var2|
(hash a=1 b=2) as |var3|}}
{{var1}}
{{var2}}
{{var3}}
{{/let}}
This solves both of the issues with the current methods of multi-assignment: no mental positional mapping required and no hash overhead.
Another alternative proposal that solves the current issues with block let
multi-assignment is the inline let
proposal (as implemented in ember-let). As discussed here, adding inline let
isn't as straight forward because it introduces new scoping semantics into the templating language, which may or may not be desirable. The advantage of the above syntactic sugar solution is that the semantics of the language remain the same.
I'm not sure this is a good idea because it ultimately beaks the consistency of template syntax, it perhaps makes new users getting confused about as |...|
. They may think like: hmm...can I use multiple as |...|
for other helpers/keywords/components? If not, what makes the let helper so special?"
But let's assume this is an acceptable change, then I'm thinking a more intuitive solution as:
{{!
1. not to introduce a new scoping semantic
2. declare variables like JS version `let`
3. use them as local variables
}}
{{#let
foo=(if cond valueA valueB)
bar=(some-helper arg1 arg2)
qux=(hash a=1 b=2)
}}
{{foo}}
{{bar}}
{{qux.a}} {{qux.b}}
{{/let}}
{{!
1. namespace is optional by using extra `as ||`
2. namespace will collect all local variables automatically, the user only need to name it
}}
{{#let
foo=(if cond valueA valueB)
bar=(some-helper arg1 arg2)
qux=(hash a=1 b=2)
as |context|}}
{{context.foo}}
{{context.bar}}
{{context.qux.a}} {{context.qux.b}}
{{/let}}
This is enough for like 95% usage, and I have some more ideas around it:
{{!
this.fetchedParams is an async task like what Promise.all or RSVP.hash did
and I'm always dreaming about the possibility of basic deconstruction
the as |...| part is optional
}}
{{#let |paramsA paramsB|=(await this.fetchedParams)}} as |promise|
{{#let
foo=(component (concat "custom-ui/" paramsA.name))
bar=(component (concat "custom-ui/" paramsB.name)) as |UI|
}}
<UI.foo /> and <UI.bar />
{{/let}}
{{else}}
{{!
render else block if this.fetchedParams rejected
I'm not sure this is possible or not for let helper
but since if/unless/each can have it, maybe let can have it.
I wish the let helper can automatically detect if there are any async operations
and use else block to handle the rejected promise
}}
<Common::ErrorMessage @message={{promise.message}} />
{{/let}}
Okay, I know the second part is a bit impractical, it's just a rough idea came up suddenly when I saw this topic. Rational rejection is welcomed. 😝
I like the await
helper proposal. The else
it's more like a try
/catch
block. I think it would make sense to discuss both separately from the multi-assignment.
I agree with @nightire that this will be confusing to new users. Specifically they will not understand why they can't do similar things elsewhere, like:
{{#each
@model as |item|
stocks as |stock|
}}
{{item.name}}
{{stock.quantity}}
{{/each}}
even though it clearly doesn't make sense.
@Gaurav0 I was actually thinking about that example in particular. I don't think it "clearly doesn't make sense". In fact, it intuitively looks like you're traversing two lists in parallel, which may or may not be useful, but it makes sense to me...
I do agree that a better mental model for what this syntax means and where it can and cannot be used would be necessary. Still thinking about that.
Just to throw another example in the ring, I think we could probably make something like this work:
{{#let foo=(whatever) bar="something else" as |@bar @foo|}}
{{/let}}
Basically allowing named arguments to be yielded and have them match the hash args not be positional at all.
@rwjblue Liking your variation of this. Definitely an improvement over the current state. Do you envision this being syntactic sugar for destructuring block params in general? I.e. something like this would work:
template.hbs
{{#full-name firstName="Rob" lastName="Jackson" as |@fullName|}}
{{!-- ... --}}
{{/full-name}}
full-name.hbs
{{yield (hash fullName=(concat @firstName " " @lastName))}}
Edit:
Guess the only downside to this would be that @arg
no longer unequivocally means it's a component argument, since it could also be a named block param...
What yielded argument would be destructed? The first argument? The first argument, which is an object? It's easier for helpers cause they can not return multiple arguments.
@jelhan Yeah, I had this thought immediately after my reply haha. each
is actually an example of a block helper that does yield multiple positional params, which presents a problem for this hmmm.
Leaving this open for now since it's part of the meta issue.
Is there a path forward here?
Is there a path forward here?
Personally, I don't see this as a big issue, but I think that's because I hardly let
multiple things in one go.
If I would, I would use the hash
approach. I guess I'm wondering if the issue is big enough for introducing a new let
syntax.
I suspect that at least some of the felt need for multi-item let
(or a non-block let
) will be resolved by the combination of <template>
and the default helper manager. That would be extra true if we had object or tuple shorthand syntax (or, more generally, a subset of JS expression syntax). Not eliminated, per se, but dramatically reduced.