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

Sugar for multi-assignment with let block helper #602

Open
sclaxton opened this issue Mar 9, 2020 · 13 comments
Open

Sugar for multi-assignment with let block helper #602

sclaxton opened this issue Mar 9, 2020 · 13 comments

Comments

@sclaxton
Copy link

sclaxton commented Mar 9, 2020

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.

@nightire
Copy link

nightire commented Mar 10, 2020

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

@jelhan
Copy link
Contributor

jelhan commented Mar 11, 2020

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.

@Gaurav0
Copy link
Contributor

Gaurav0 commented Mar 21, 2020

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.

@sclaxton
Copy link
Author

sclaxton commented Mar 21, 2020

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

@rwjblue
Copy link
Member

rwjblue commented May 20, 2020

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.

@sclaxton
Copy link
Author

sclaxton commented May 28, 2020

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

@jelhan
Copy link
Contributor

jelhan commented May 28, 2020

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.

@sclaxton
Copy link
Author

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

@wagenet
Copy link
Member

wagenet commented Jul 23, 2022

Leaving this open for now since it's part of the meta issue.

@wagenet
Copy link
Member

wagenet commented Jul 25, 2022

Is there a path forward here?

1 similar comment
@wagenet

This comment was marked as duplicate.

@bertdeblock
Copy link
Member

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.

@chriskrycho
Copy link
Contributor

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.

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

8 participants