-
-
Notifications
You must be signed in to change notification settings - Fork 407
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
Spreadable Arguments #593
base: master
Are you sure you want to change the base?
Spreadable Arguments #593
Conversation
Updated regarding pzuraqs' comments.
Co-Authored-By: Chris Garrett <me@pzuraq.com>
Discussion in fork PR: |
text/0000-spreadable-arguments.md
Outdated
<!-- Using the same rule on arbitrary spreads though, we no longer have context --> | ||
<SubComponent ...{{this.someSpecificAttrs}} ...{{this.someSpecificArgs}}></SubCompnoent> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Arbitrary spreads would be an awesome addition! I'm unsure, whether they might explode the scope of this RFC, but I would still love to have them eventually!
text/0000-spreadable-arguments.md
Outdated
|
||
A possible component in the wild that could benfit from this could be | ||
[ember-power-select](https://github.com/cibernox/ember-power-select/blob/master/addon/templates/components/power-select.hbs). | ||
This component has to explicitely pass on many arguments to its child components. With "splarguments" half or more of this template could be cut down. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case someone needs further proof how useful this could be, I'll gladly show you this beauty:
https://github.com/kaliber5/ember-bootstrap-power-select/blob/master/addon/templates/components/bs-form/element/control/power-select.hbs
😱🤯🤪
text/0000-spreadable-arguments.md
Outdated
<SubComponent ...{{this.someSpecificAttrs}} ...{{this.someSpecificArgs}}></SubCompnoent> | ||
|
||
<!-- Maybe prepending `@`? --> | ||
<SubComponent ...attributes ...@arguments></SubComponent> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think having an ...@arguments
would be amazing, it greatly corresponds to what We have now
This is awesome. <!-- HBS-->
<MyButton ...@buttonArgs/> // component.js
get buttonArgs(){
return {color: "blue"}
} In that case |
@averydev Though it is a cool idea, what about there is an argument named The reason we use |
@nightire good point. The I only just learned about the
Mulling on it a bit more, How about this:
That makes the whole thing pretty simple to fit into the current mental model. There's just a I'm not sure what would happen if the target isn't a component... either it would splat key/values onto the element as attributes onto the element, or ignore them entirely. V0 probably just ignore them for simplicity sake: Option 1:
Option 2
|
@averydev I had a similar idea to creating a custom helper such as <SubComponent ...{{this.someSpecificAttrs}} ...@{{this.someSpecificArgs}}></SubComponent> |
I'm not convinced by the If the wrapper component has arguments itself, only a subset of all arguments should be passed through to the wrapped component. But this wouldn't be supported by the proposed syntax. It would be an all or nothing choice. I fear many developers would pass all arguments through including the ones of the wrapper component. That might work as long as these arguments aren't used by the wrapped component. But it could break at any point of time as adding new arguments wouldn't require a major version bump. I think a syntax that covers most use cases must support at least exclusion of some arguments. But such a syntax might get so complex that directly adding support for arbitrary arguments might be more valuable as that one supports even more use cases (e.g. dynamic component invocation with different arguments depending on the component). It's another story for TL;DR: The proposed syntax would only support a few use cases. Adding support for arbitrary splat arguments directly should be the focus in my opinion. |
@jelhan that’s a very valid concern, and actually exactly why this proposal is what it is. This is already a common issue with The issue is that creating a fully general spread syntax, that doesn’t just apply to this special case, opens up a large can of worms. What about spreading within helpers and modifiers? What about spreading objects that aren’t The goal was to keep the scope of this particular proposal small, without limiting future possibilities, so that we could iterate and continue to add features just like the ones you’ve outlined. I personally would like to see a very generalized spread syntax, as well as the ability to be more precise when spreading and applying attributes, but I think this would make sense as a first step in that direction, and would help solve some immediate needs now. |
I agree that we should try to solve easy cases first. But I fear that the proposed feature would only support very few use cases. It's easy to assume that it would support more use cases than it can. I think this paragraph in the RFC is misleading:
This is only true if the child component is provided by the same addon. That's the only scenario in which passing through more arguments than needed is safe. Otherwise it's a footgun cause the child component could add an additional argument that clashes with arguments of the wrapper component in any minor release. I think this limitation should at least be mentioned in the RFC. And the Ember Power Select example should be updated to avoid wrong assumptions about covered use cases. I fear an example of such a wrong assumption with ember-bootstrap-power-select by @simonihmig. I agree that it would profit a lot from being able to pass through arguments but I don't think the proposed |
Generally in favor of some way to "passthrough" args, but in order to determine what |
@mehulkar
Eventually, we could add a non-keyword way to spread specific arguments (and attributes). |
I'm thinking about a "picking" ability, something like: {{! forwarding all passing attributes }}
<div ...attributes>
<input>
</div>
{{! forwarding some passing attributes }}
<div ...attributes('class' 'data-theme')>
<input ...attributes('id' 'name' 'type' 'value')>
</div> and it can be applied to |
The main use case of Not sure but exclusion of some attributes might be already supported with current syntax by setting these attributes after But something similar wouldn't work for |
FWIW, I think that this would make a lot of sense without a special syntax, if we gave you the ability to spread arbitrary objects: class MyComponent extends Component {
get restArgs() {
let { foo, bar, ...rest } = this.args;
return rest;
}
} <MySubComponent ...@{{this.restArgs}} /> For attributes, we would need to make them accessible to JS somehow I think. But like I said before, I think this could come later, potentially. It could also be added to this RFC, but that may make it take longer to get consensus on and to implement. |
My thoughts on this. Disclaimer: I read your comments AFTER I had my thoughts sorted, so mine weren't influenced by yours but I will make connections to what you wrote. First of all I separated the problem into a spreadable syntax and then how to combine it with arguments. Spreadable SyntaxThe idea here is that we have a KVO and when spreaded into something, it will receive the keys with their values as individual arguments. What could it look like: class DummyComponent extends Component {
properties = {
foo: 'bar',
baz: 'bam'
};
} {{! dummy-component/template.hbs }}
<MyComponent @params={{...this.properties}}> and here is the interface MyComponentArgs {
params: {
foo: string;
baz: string;
};
} which will receive it that way. That would then allow us to use helpers for modifying the arguments being spread in: <MyComponent @params={{...(filter this.properties 'foo'}}>
<MyComponent @params={{...(assign this.properties this.moreProperties}}> (I moved @pzuraq example code from TS to hbs) I didn't thought about all the technical challenges here. Luckily we have a @pzuraq for this:
Simply spoken: the spreadable syntax must make sure to pass on a KVO that keeps things such as tracking intact. Spreadable ArgumentsThis needs two things: A reserved name and a special slot to pass them down. I think we can all easily agree on using <MyComponent {{...@arguments}}>
<MyComponent @arguments={{...@arguments}}> Arguments are available in the backing up component as <MyComponent {{...this.args}}> Technically this would work I guess ;) but would disconnect the idea of arguments being prefixed with
-> shouldn't they be named the same then? Serving Multiple ProblemsBy accident I'd say this would allow to address the problems @jelhan was describing:
Now that would be possible: <MyComponent {{...(filter @arguments 'foo' 'bar')}} @subsetForSpecialUseCase={{...(assign (filter @arguments 'baz') this.props)}}> which can be consumed as such: {{! my-component/template.hbs }}
<MyHeavyComponentBuilder {{...@arguments}} as |builder|>
<builder.Content {{...@subsetForSpecialUseCase}}>
{{yield}}
</builder.Content>
</MyHeavyComponentBuilder> Synchronosity between
|
{{! dummy-component/template.hbs }}
<MyComponent @params={{...this.properties}}> This seems off to me. Let's think about how this would translate into JS: let args = {
params: ...this.properties
}; This isn't valid syntax. The issue is we need to spread into something. let args = {
params: { ...this.properties }
}; The natural analog in HBS would be something like: <MyComponent @params={{hash ...this.properties}}>
I think that this would be really unfortunate. It would be very convenient to be able to spread an array of arguments into a helper. {{foo ...this.params}} Or, thinking about the opposite. Using rest syntax within a block: <MyComponent as |...params|>
{{foo ...params}}
</MyComponent> This would absolutely be valuable. I think the key thing to figure out is for a handlebars expression, how do we tell if a value is meant to be spread as an object or an array. In JS, we know by the context of the spread: foo(...arr); // spreads like array
let bar = [...arr]; // spreads like array
let baz = { ...arr }; // spreads like object, even though it's an array Unfortunately we don't have a way to distinguish with mustache statements. <!-- foo can receive both named and position args, so ...params could be targeting either -->
{{foo ...params}} I was hoping to punt on this. I think one option would be to allow <!-- ...params spreads as an array, ...@params spreads as an object -->
{{foo ...params ...@params}} But this seems bad to me, since
Agreed here, in that I think we need to make sure we aren't designing a syntax that would prevent future possibilities. I don't think we need to have the whole plan nailed down to land this first though. |
I was only thinking about the case for them to be passed as KVO/object - I think I made it myself to easy because:
I love to extend my thinking on that. Thank you for all that examples you put to it. But as you also said (regarding
That's what I avoided with the limited scope of only treating hashes. The obvious parts then would be: <MyComponent {{hash (assign ...@arguments (foo (array ...this.props)))}} I don't like it ! Putting regular javascript around: <MyComponent {{hash (assign { ...@arguments } (foo [ ...this.props ]))}} or in simplified version for spreadable args only: <MyComponent {{hash { ...@arguments }}} I'd consider it the verbose way that needs syntactic sugar now - would you agree? |
I came across a significant piece of additional motivation for this RFC this week and wanted to write it down before I forgot. There is actually a significant gap in what you can do by manually forwarding arguments. Namely: you cannot forward a non-defined argument as non-defined (as opposed to explicitly defining an argument with a value of Consider this // example.js
import Component from '@ember/component';
export default Component.extend({
value: 123,
}); {{! example.hbs }}
<p>The value is: {{this.value}}</p> Because of how arguments are set on the class, overriding the default value from the prototype, users could simply invoke <Example /> —would render this HTML: <p>The value is: 123</p> Explicitly passing a value argument would override it. So this invocation— <Example @value={{456}} /> —would render this HTML: <p>The value is: 456</p> ...including if you passed <Example @value={{undefined}} /> <p>The value is: </p> This last bit is the key here, because if we want to wrap a Classic component—say, just hypothetically 😭 because you needed to A/B test the rollout of the conversion of important component in your app to Octane—then you would need to do something like this: {{#if useNew}}
<ExampleOctane @value={{@value}} />
{{else}}
<Example @value={{@value}} />
{{/if}} This works fine when the caller does pass a value. But when the caller does not pass a value, the result is that we pass This all makes perfect sense if we think in terms of JS. Object spread by definition does not include any keys which are not defined on the source objects (how could it?): const args = {};
const result = { value: 123, ...args }; // { value: 123 }; const args = { value: 456 };
const result = { value: 123, ...args }; // { value: 456 }; const args = { value: undefined };
const result = { value: 123, ...args }; // { value: undefined }; You can work around this, using the example of using
|
I would like spreadable arguments to avoid having to do this: {{! simplified example! }}
{{#if @model}}
<LinkTo @route={{@route}} @model={{@model}}>{{yield}}</LinkTo>
{{else if @models}}
<LinkTo @route={{@route}} @models={{@models}}>{{yield}}</LinkTo>
{{else}}
<LinkTo @route={{@route}}>{{yield}}</LinkTo>
{{/if}} One would expect to be able to write: <LinkTo
@route={{@route}}
@model={{@model}}
@models={{@models}}
>
{{yield}}
</LinkTo> And would prefer to write: <LinkTo ...@arguments>{{yield}}</LinkTo> But like @chriskrycho says, you can't forward a non defined argument. So this particular example results in |
Is there still interest in moving this forward? |
yes. I think it's spread is a suuuuuper nice feature for any language to have in 2020+ |
I've had this on my todo list to work on it for almost 2 years 🙃 |
Agreed. Would love to have this feature! I echo @amk221's use case. |
Indeed, it would be amazing to have this! @amk221 indeed
and just pass this is really not as elegant as |
This would be huge! |
Does anyone have the bandwidth to help me/others get this over the finish line? I feel like my legacy in the Ember community will forever be proposing this RFC and then never getting it done 😅 |
@Alonski we discussed this RFC today in framework core and we're very interested in moving it forward. Ping me on Discord to schedule some office hours sessions and we'll get the ball rolling. |
Dmed you @wycats exciting! |
I ran into a use case today where this feature would come in handy. For complex design system components like a tooltip or a dropdown it would be great to be able to write a custom component wrapping a base component provided by an addon, but tweaking the layout and setting defaults. To avoid having to maintain the addon's component interface, passing all arguments to the underlying component would be great. One thing I don't yet see addressed is how this would fit in with the TypeScript/Glint component interface. Currently there is: interface Signature {
Args: {};
Element: HTMLElement;
Blocks: {};
} How would these extra arguments be defined? And for template type checking, the target Ember component where these are used, would have to be known? (similar to |
From the TS POV, it’s not really any different than doing it for a function, and because TypeScript is structurally typed, the rest of it "just works" in a case like this. You just need your wrapping component's signature to expose its types, possibly "proxying" at the type level if you don't otherwise care, and the rest of it would be relatively straightforward to make work, given that Imagine something like this: import Button, { ButtonSignature } from 'my-fancy-ui-lib';
import type { TOC } from '@ember/component/template-only';
interface WrappedButtonSig {
Args: ButtonSignature['Args'] & {
onFocus: () => void;
onBlur: () => void;
};
Blocks: ButtonSignature: ['Blocks'];
Element: ButtonSignature['Element'];
}
export const WrappedButton: TOC<WrappedButtonSig> =
<template>
<Button
{{on "focus" @onFocus}}
{{on "blur" @onBlur}}
...attributes
...@args
>
{{yield}}
</Button>
</template>; Notice that this is all just hypothetical; the point is that it's straightforward to see how it would work in TS. |
@Alonski, @marcemira and @wycats - Sounds like you folks need to get together and align on this. |
This seems like an obvious and uncontroversial feature but we need a "Detailed Design" that isn't "TBD". People who are interested in this might want to attend the weekly spec meeting to get help, the next one is https://discord.gg/emberjs?event=1147176319147311184 |
came across another use case for spreadable everything today. Attributes:
Arguments:
Blocks:
|
Rendered