Skip to content

An ability to specify default values for @named arguments #479

Closed

Description

@lolmaus

Previously, I was able to provide default values to arguments like this:

component.js:

Component.extend({
  foo: 'bar'
})

template.hbs:

{{foo}}

result:

bar

It was a simple, efficient and very declarative way of specifying default values. It both worked well and was obvious to developers reading the code.


With named args, this no longer works. Though an argument passed externally does overwrite this.foo, setting an initial value to this.foo does not affect @foo in the template.

As a result, there is no way of specifying a default values for arguments!

The only workaround I'm aware of is to keep using {{foo}} or {{this.foo}} instead of {{@foo}}. This works, but it defeats the purpose of @-named args in templates.


Thus, we need a simple, declarative way of specifying default values for arguments used in components.

A developer should be able to tell which arguments are gonna have which default values by simply looking at the component JS file. Those definitions should not be obscured inside a hook such as init.

Activity

  1. lolmaus commented on Apr 19, 2019

    @lolmaus
    Author

    To be clear, I do NOT want to be using {{@foo}} for arguments without default values and {{this.foo}} for arguments with default values. Such approach creates a lot of confusion.

    I want a way to provide default values for named args and keep using them as {{@foo}}, otherwise the named args RFC makes no sense to me.

    This is true both for Classic and Glimmer components.

  2. NullVoxPopuli commented on Apr 19, 2019

    @NullVoxPopuli
    SponsorContributor

    Update (2021-05-23) (and after having spent a lot more time with Octane, and teaching it to folks)

    I no longer think the opposite of @rwjblue
    I am more... 🤷

    To be clear, I do NOT want to be using {{@foo}} for arguments without default values and {{this.foo}} for arguments with default values. Such approach creates a lot of confusion.

    fwiw, I think the opposite.

    we know that {{@foo}} comes from the caller.
    we know that {{this.foo}} is a properties in the current context/component.

    (just for clarity, and thoroughness), at present, the only way to get the same behavior as in classic components is via:

    get foo() {
      return this.args.foo || 'some default value';
    }

    and since args are readonly, we can't modify them -- we can't do any sort of dynamic default value setting (like what is possible in classic components).

    The only option I really see for still using {{@foo}} and having a default value, is if we set static default values.

    export default class MyComponent extends Component {
      static defaultArgs = {
        foo: 'some default value'
      };
    }

    however, this introduces an extra layer of complexity, and the merge of args with defaultArgs happens behind the scenes, and may just add 'one more thing' to a beginners set of things to learn / have to deal with.

    my preference for a default value pattern would be something like this:

    export default class MyComponent extends Component {
      @defaultArg('foo', () => 'the default value') foo;
    }

    and then still accessed via {{this.foo}}, because it's then a thing defined on the component.

    However, this still uses a string to reference an arg, which I don't like, so the getter is my go-to for now.
    (I avoid strings / any dynamic code via strings at all costs)

  3. chriskrycho commented on Apr 19, 2019

    @chriskrycho
    Contributor

    One thing to consider for a design around this is that one of the points of arguments is that they're immutable; so you'd want an approach—whether using decorators or something else—that lets that guarantee still be upheld for the idea of defaults.

    Personally, I strongly prefer the explicitness of using a getter and saying "This is my own derived state," however it is derived. That makes it clear in the template that when I see @foo I'm looking at something that was passed in without any kind of transformation or alteration. A thing that can be passed optionally or can be given a different value has different semantics.

    That said, optional arguments with defaults in JS are a thing, so I'm not opposed to coming up with a comparable solution here; I just want it to be the case that we don't lose the other benefits of named args.

  4. lolmaus commented on Apr 19, 2019

    @lolmaus
    Author

    @NullVoxPopuli: my preference for a default value pattern would be something like this:
    and then still accessed via {{this.foo}}

    @chriskrycho: I strongly prefer the explicitness of using a getter and saying "This is my own derived state," however it is derived.

    I do not want to access some of my arguments via {{@foo}} and others via {{this.foo}}. It's hard to remember which is which. When you have a dozen of arguments, it's a huge pain.

    Also, default values for arguments are a very common use case. It's not some caprice that developers, be they quirky enough to desire it, should figure out on their own.

    The worst is that when providing a default value with a getter but not passing any value from the parent template, {{@foo}} and {{this.foo}} will have different values. And if I confuse one for another, I'll have unexpected results, with the code looking completely legit and leaving me clueless.

    I don't want this gotcha to be scattered all over my code like rakes (gif inside). tom and jerry rake gif

    @NullVoxPopuli:

    export default class MyComponent extends Component {
      @defaultArg('foo', () => 'the default value') foo;
    }

    How is this ☝️ better than these 👇 ?

    export default class MyComponent extends Component {
      foo = 'a default value';
    
      get bar() {
        return this.attrs.bar || 'another default value';
      }
    }

    Either way you still need to revert to {{this.foo}} in templates, which defeats the purpose of @-named args.


    @chriskrycho: That makes it clear in the template that when I see @foo I'm looking at something that was passed in without any kind of transformation or alteration. A thing that can be passed optionally or can be given a different value has different semantics.

    But arguments passed externally do get written into the this context of the component! That's the only thing I can be 100% certain about.

    And I want to avoid mistakes and confusion, my only option is to always use {{this.foo}}. It will work predictably and will never cause any issues.

    But I don't want to be a black sheep. And I do want to see a distinction between arguments and properties in my templates. It's just arguments with default values are still arguments! They've always been.

  5. lolmaus commented on Apr 19, 2019

    @lolmaus
    Author

    Note that there's no linter (and can hardly imagine one to exist) for warning you that you're using {{@foo}} whereas a default value is available.

  6. NullVoxPopuli commented on Apr 19, 2019

    @NullVoxPopuli
    SponsorContributor

    When you have a dozen of arguments, it's a huge pain.

    I think maybe we have something else to talk about now <3

    Can you give an example of something where'd you have this many args?

    . It's hard to remember which is which

    fwiw, the point is not to remember, but to be able to discover. Apps are too big to keep in your head, ya know?

    How is this point_up better than these point_down ?

    imo, the getter is better, because there is no string access on args.

    my only option is to always use {{this.foo}}.

    that defeats the purpose of read-only args. ya don't always need a default. :-\

  7. lolmaus commented on Apr 19, 2019

    @lolmaus
    Author

    Oh wow, doing get foo() {} and passing @foo from a parent template crashes Ember! 🤦‍♂️

  8. lolmaus commented on Apr 19, 2019

    @lolmaus
    Author

    that defeats the purpose of read-only args.

    That's what I said in the top post, and this is exactly the reason to request an RFC for default values.

  9. NullVoxPopuli commented on Apr 19, 2019

    @NullVoxPopuli
    SponsorContributor

    Oh wow, doing get foo() {} and passing @foo from a parent template crashes Ember! man_facepalming

    that's probably because we have to support classic accessing for those not fully on the named args train.

    once we get rid of classic arg accessing, that won't be an issue

  10. lolmaus commented on Apr 19, 2019

    @lolmaus
    Author

    @chriskrycho: Personally, I strongly prefer the explicitness of using a getter and saying "This is my own derived state," however it is derived. That makes it clear in the template that when I see @foo I'm looking at something that was passed in without any kind of transformation or alteration. A thing that can be passed optionally or can be given a different value has different semantics.

    I can agree with that for Glimmer components. They are more strict and are a new entity, so a shift of a mental model is acceptable.

    But everyone's mental model for Classic components is that an argument can be accessed in a template the same way, regardless of whether it has a default value or not. Adding (or removing, for that matter!) a default value to/from an existing argument did not force me to update my template. It just worked, and that's how it should be.

    The named arguments RFC changed that. 😒 But note how adding a default value now requires rewriting a template from {{@foo}} to {{this.foo}}, but removing a default value does not require switching back to {{@foo}}.

    It is an inconsistency and a source of confusion. I've been always subconsciously uncomfortable using @ & this in Classic component templates and now I understand why.

  11. Panman82 commented on Apr 19, 2019

    @Panman82

    This was discussed elsewhere (apparently not on the RFC) and I seem to recall the answer (for the time being) was to be {{or @argName "static value"}} or {{or @argName this.argDefault}}. The only issue with that is what if the @argName is falsy and that's what you'd want. Maybe we need a {{default}} helper to only pass the second/default value when the arg is undefined. Related: #388

  12. NullVoxPopuli commented on Apr 19, 2019

    @NullVoxPopuli
    SponsorContributor

    It is an inconsistency and a source of confusion.

    you could still use classic / ambiguous access: {{foo}}

    where,

    import { Component } from '@ember/component';
    
    export default class MyComponent extends Component.extend({
      foo: 'default value',
    }) {
     // class body
    }
  13. lolmaus commented on Apr 19, 2019

    @lolmaus
    Author

    @Panman8201:

    {{or @argName "static value"}}
    

    There are two huge issues with this approach:

    1. It's a pretty universal convention to have default values defined near the top of the component's JS class.
    2. What if the value is used multiple times in a template? You'll have to provide the default value multiple times. It's tedious and prone to regressions.
  14. lolmaus commented on Apr 19, 2019

    @lolmaus
    Author

    @NullVoxPopuli: you could still use classic / ambiguous access: {{foo}}

    Which equally defeats the purpose of named arguments RFC.

    BTW, you don't need to use .extend() for that. Class property works.

  15. Panman82 commented on Apr 19, 2019

    @Panman82

    @lolmaus
    There are two huge issues with this approach:

    1. It's a pretty universal convention to have default values defined near the top of the component's JS class.
    2. What if the value is used multiple times in a template? You'll have to provide the default value multiple times. It's tedious and prone to regressions.
    1. Putting defaults on the component JavaScript class won't be a viable solution for "template only" components. So the solution for this issue has to be in the template.
    2. That's why I put {{or @argName this.argDefault}} as another example as a way to show the option of putting the default value in one place to reference. Another template-only solution would be to {{let "some default" as |argDefault|}} and then {{or @argName argDefault}}.
  16. 21 remaining items

  17. lolmaus commented on May 2, 2019

    @lolmaus
    Author

    repeating the same complaints isn't changing anything here

    I've been reiterating on my complaints in a hope to achieve agreement that the issue is legit.


    If you have a proposal

    I've been thinking of employing decorators:

    class MyComponent extends Component{
      @arg name!: string;
      @arg prefix?: string;
      @arg role = 'user';
    
      // Alternative:
      @arg('user') role: string;
    }

    Not sure how to do it for Glimmer components, though. :( Maybe a class decorator?

    interface Args {
      name: string;
      prefix?: string;
      role?: string;
    }
    
    @args({
      role: 'user'
    })
    class MyComponent extends Component<Args> {
    }
  18. pzuraq commented on May 2, 2019

    @pzuraq
    Contributor

    @lolmaus we absolutely think your concerns are legit.

    For context, when originally designing namespaced args and Glimmer components, this issue did come up, and we discussed it in depth quite a few times. In all those discussions, we kept coming back to the many issues with the way that arguments work in classic components, and decided that the pros of namespaced arguments and separation of concerns outweighed the cons. The explicitness may feel verbose, but the extra safety it provides is generally worth it. This is similar to our current stance on Mixins.

    That said, we also intentionally left room for future solutions here, such as a separate defaultArgs export. We realize this is a common enough use case that the verbosity is not ideal, and personally I really do want to see something better here. I also want that solution to have the benefits I discussed above:

    1. Separate from both the template and the class definitions, so neither is required
    2. Isolated from component state and incoming args state, so that defaulting doesn't become a magnet for business logic (as it can be currently)
    3. Easier to automate documentation for
    4. Ideally, a better typing strategy, especially assuming we do get template typing at some point. While I agree classic components were somewhat easier to type, I actually feel like there is some type smell there, especially around having to use ! to specify required values.

    I realize that not having this ability right now is frustrating, but I think this is also part of the philosophy Ember is taking on right now - iterative steps and improvements, day over day and year over year, rather than complete rewrites that attempt to solve every problem all at once. There are plenty more features, ideas, and improvements we have in the wings for the post-Octane world, and we'll keep on shipping them one at a time 😄

    Discussions like this help us prioritize and gather ideas for how to solve the problems better, so we absolutely appreciate you bringing this up, I just want to reiterate, we get where you're coming from, and we're taking it seriously. We just need to do the design work, and iterate toward a solid solution.

  19. gossi commented on May 5, 2019

    @gossi

    I'm late to the party. I found this discussion somewhere left it open in a tab and now I had the time to read through it, totally worth it.
    I can say, I totally had the same experience as @lolmaus with this. With sparkles rising, I created the @arg to quickly serve my direct needs. I wouldn't recommend using it today anymore, though it was a good learning experiment for me all along the way with sparkles (now glimmer) components and decorators. This project basically funnels into ember-decorators/argument#95

    However, all along the way, you need to do a mindshift - that is particularly not easy when trying to bake old patterns into a new system (which easily become anti-patterns as in this case). So some things I learned, that I hope that help you (since not everything is finalized, those things may change):

    • Glimmer is template only, means you need to shift your priority (from code to template); it is: template (declarative) first, programming second. That also goes for didUpdate() => {{did-update}} (here I also had a missunderstanding of what it can do, it is here to make explicit calls if one of the args is changing - very helpful). That said doing {{or}} or {{optional}} plenty of times in a template is tedious and it is in our craft/nature as developers to seek for a smarter solution that stimulates our brain 😁

    • Set your initial state in the respective initializer (init() for classic, constructor() for glimmer):

    interface MyArgs {
      foo?: string;
    }
    
    export default class MyComponent extends Component<MyArgs> {
      foo: string;
      constructor(...) {
        this.foo = this.args.foo || 'default goes here';
      }
    }
    • If this.foo needs to be a dynamic value (depending on the passed in @foo) then use a getter (and praise auto-tracking)

    • this.foo and @foo are two separate and explicit things. Do not try to use them for the same thing (as with args on classic components). Just don't. (Re)think your architecture for that component, it will thank you (most of the times).

    • That said, not everything has landed in ember/glimmer/octane yet, things that will make our lives easier (e.g. not attributes to glimmer components yet, when using the {{component}} helper).

    The best tip here (and I gonna repeat myself for this): It's a mindshift, embrace it. Throw your "old habited" balast over board and come into a clean, explicit and more structured ember-world :)

    //cc @locks @jenweber -> is that something for the guides, too? Regarding the migration part?

  20. pzuraq commented on Jul 15, 2019

    @pzuraq
    Contributor

    I've been thinking about this more, along with the ability to specify some sort of ArgTypes similar to React's PropTypes, and one thing I'm coming back to and realizing is that in the future, we won't necessarily be guaranteed to have one component per-file. This is still something that needs to be figured out, what our SFC/template import syntax looks like, but if we do land on something that allows multiple components per file, my suggestion of export const defaultArgs won't work.

    I think it may actually make sense to use static class properties instead. It could look something like:

    export class MyClassBasedComponent extends Component {
      static defaultArgs = {};
    }
    
    export const MyTemplateOnlyComponent = <template></template>;
    
    MyTemplateOnlyComponent.defaultArgs = {};

    And the same could be done for some sort of ArgTypes system. The other option would be to have a different syntax specifically for TO-components, but I don't think that would be ideal. It'd make converting from a TO-component to a class-component much harder, and would make it harder to statically analyze.

  21. maxbogue commented on Nov 22, 2019

    @maxbogue

    I found reading through the comments for this quite frustrating because it seems I hold very different things important than most of the people here. That's going to make the rest of this post feel somewhat rant-y, which I apologize in advance for.

    The named arguments RFC broke an extremely basic and commonly feature of components, which are the modern building block of front-end UI. The fact that it was accepted and implemented without any support for default values (all the current solutions are extremely bug-prone because anyone using @foo syntax won't get the default value) represents, in my mind, not some strength of Ember's new iterative approach but a massive disconnect with users and a huge threat to the framework's viability for new users relative to other popular options.

    The two main considerations being expressed in this thread preventing a solution being agreed upon (many of the decorator-based solutions have seemed great to me) are things that I utterly disagree with. They are:

    1. That it is important to know whether a value comes from a caller or the default value. Why? Much like in JS functions, I don't see why it would. It's a value, you use it. If you care, then default values are not the feature you wanted in the first place. Many members of the Ember community seem to value "not needing to look at the JS to know the value", but from my perspective coming from other frameworks like Vue that's probably a relic of Ember's long history of failing to co-locate the template and the JS in any way. Putting the two closer together is the solution (as evidenced by other frameworks gravitating that way), not breaking things like default argument values so you don't need to pull up the JS file that's 6 directories up and 5 directories down...

    2. That it is worth prioritizing compatibility with template-only components over getting a solution to this issue using JS in place. Multiple people have brought this up as a concern but nobody has offered up a solution. That's because this feature is almost certainly not compatible with template-only components, nor should it need to be. I agree that it's cool to not need a class if you don't have anything besides the template to define for a component, but I think it's extremely dangerous to the future of the framework to start requiring new features to somehow support them.

    It's hard to participate in a debate like this where the disagreements seem so fundamental, so thanks for bearing with me.

  22. pzuraq commented on Nov 22, 2019

    @pzuraq
    Contributor

    @maxbogue I understand the frustration. The core team has been in a holding pattern recently as we put the final touches on Octane, so we haven't been actively working on or reviewing RFCs, which is why progress has been slow.

    I don't think we're as philosophically disconnected as it may seem though. For your first point, it's important to realize that you do get a clear distinction in a function definition as to what is an argument, and what is a local value.

    function myFunction(arg = 'foo') {
      let localValue = 'bar';
    }

    What Ember did historically would be, more or less, translated to JS (not in a literal way, but in a semantically similar hypothetical) like this:

    function myFunction() {
      this.arg = this.arg || 'foo';
      this.localValue = 'bar';
    }

    Personally I think this is much more difficult to reason about, and I think this is why a lot of folks both in the community and on the core team want to make sure we don't reintroduce semantics like this.

    This doesn't mean that everyone is against defaults in general. JS also didn't have default values for arguments for quite some time, but they have definitely been valuable since they were introduced, and I think that we can probably find a solution that works for all of our use cases, and still maintains the semantics we worked for with named arguments.

    Re: prioritizing template-only components (something I've mentioned a few times in this thread), IMO it's is not prioritizing them over class based components, but making sure we don't design ourselves into a corner. If we were to come up with a design for defaults that, for instance, limited our options for designing template-imports, that would not be ideal. If we were to come up with a design that had no way of translating into TO components, that would also not be ideal. It's mostly about doing our due-diligence, and making sure we've considered as much as possible in the design phase.

    I'm hopeful that we'll be able to make progress on this soon! And again, I understand the frustration and hope the explanation helps a bit. Thanks for your feedback!

  23. maxbogue commented on Nov 22, 2019

    @maxbogue

    @maxbogue I understand the frustration. The core team has been in a holding pattern recently as we put the final touches on Octane, so we haven't been actively working on or reviewing RFCs, which is why progress has been slow.

    I don't think we're as philosophically disconnected as it may seem though. For your first point, it's important to realize that you do get a clear distinction in a function definition as to what is an argument, and what is a local value.

    function myFunction(arg = 'foo') {
      let localValue = 'bar';
    }

    What Ember did historically would be, more or less, translated to JS like this:

    function myFunction() {
      this.arg = this.arg || 'foo';
      this.localValue = 'bar';
    }

    Personally I think this is much more difficult to reason about, and I think this is why a lot of folks both in the community and on the core team want to make sure we don't reintroduce semantics like this.

    I like @foo syntax as a way to differentiate what is an argument, I just don't see the problem in wrapping the default value up into that (same as JS). Not against the syntax wholesale, just that it was implemented without solving this use-case.

    Re: prioritizing template-only components (something I've mentioned a few times in this thread), IMO it's is not prioritizing them over class based components, but making sure we don't design ourselves into a corner. If we were to come up with a design for defaults that, for instance, limited our options for designing template-imports, that would not be ideal. If we were to come up with a design that had no way of translating into TO components, that would also not be ideal. It's mostly about doing our due-diligence, and making sure we've considered as much as possible in the design phase.

    I guess it's pretty unclear to me how you would ever do something like this with TO components, because templates are just templates and the JS is everything else about a component. This is pretty clearly part of the everything else. Trying to make every component feature going forward TO-compatible seems pretty problematic to me.

  24. pzuraq commented on Nov 22, 2019

    @pzuraq
    Contributor

    I guess it's pretty unclear to me how you would ever do something like this with TO components, because templates are just templates and the JS is everything else about a component

    So, for some extra context here, one thing that is being discussed a lot is making TO components much more viable for use in the majority of component situations. Today they're fairly limited in what they can do, but with template imports it may be possible to make them much more powerful.

    import PowerSelect from 'ember-power-select';
    
    function getCityNames(cities) {
      return cities.map(c => c.name);
    }
    
    function getCountryNames(cities) {
      let countries = new Set();
    
      cities.forEach((city) => countries.add(city.country));
    
      return Array.from(countries);
    }
    
    export default hbs`
      City: 
      
      <PowerSelect 
        @selected={{@selectedCity}}
        @options={{getCityNames @cities}}
        @onChange={{@onCityChange}}
      />
    
      Country:
    
      <PowerSelect
        @selected={{@selectedCountry}}
        @options={{getCountryNames @cities}}
        @onChange={{@onCountryChange}}
      />
    `;

    Or a frontmatter version:

    ---
    import PowerSelect from 'ember-power-select';
    
    function getCityNames(cities) {
      return cities.map(c => c.name);
    }
    
    function getCountryNames(cities) {
      let countries = new Set();
    
      cities.forEach((city) => countries.add(city.country));
    
      return Array.from(countries);
    }
    ---
    
    City: 
    
    <PowerSelect 
      @selected={{@selectedCity}}
      @options={{getCityNames @cities}}
      @onChange={{@onCityChange}}
    />
    
    Country:
    
    <PowerSelect
      @selected={{@selectedCountry}}
      @options={{getCountryNames @cities}}
      @onChange={{@onCountryChange}}
    />

    With this type of a setup, it would be possible to use TO components for many more use cases, especially when no root state exists on the component. Helper functions can take the place of computed properties/getters, and you could define modifiers inline as well potentially. Today, those would all have to be global, and the cost to setting them up is pretty prohibitive because of that.

    This isn't all set in stone at all, template imports could also end up being much more minimal (e.g. only allow import syntax), but I do think it demonstrates the possibilities, and why we would like to make sure we think about them in general when designing new component APIs.

  25. Panman82 commented on Nov 22, 2019

    @Panman82

    Got to be honest, I'm not a fan of putting JavaScript in frontmatter, that belongs in the .js file. I'd rather work out the single file components solution. Unless this is the SFC solution, in that case, I still don't like it. 😛

  26. pzuraq commented on Nov 23, 2019

    @pzuraq
    Contributor

    There are a few different general directions that are being discussed, some that are even more minimal (no JS except import statements allowed). I posted two of the options mainly to demonstrate why some folks think TO components are worth investing into, myself included, and because I don't want to give the impression that anything has been decided - still plenty of RFCs to go before we finalize the syntax!

  27. webark commented on Nov 23, 2019

    @webark

    @pzuraq This is a bit of a side tangent, but what’s the main driving force behind having that TO component you showed (which wasn’t just a template, but verbiage aside) over a standard/glimmer/single file/class component? I get the explicit import, but the rest just seems like needless confusion, and could be easily done with a template and a class rather then template and functions.

    Is it performance? Memory usage? “Ergonomics”? If there’s a better place for this question, point me there and i’ll read ask their.

  28. pzuraq commented on Nov 24, 2019

    @pzuraq
    Contributor

    @webark let's continue this discussion on Discuss, started a thread over there

  29. wagenet commented on Jul 23, 2022

    @wagenet
    Member

    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!

  30. NullVoxPopuli commented on Jul 23, 2022

    @NullVoxPopuli
    SponsorContributor

    I think someone had an idea that layered on top of: #779 (and #800 / #748 )
    and was something like:

    interface Signature {
      Args: {
        foo?: string;
      }
    }
    
    export const Foo: TOC<Signature> = <template @foo="default value">
      always shows something: {{@foo}}
    </template>

    tho, maybe ??= would have better semantics? idk?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions