Named arguments don't work with default values?


#1

Hi,

Ember 3.1 introduced named arguments in components and I wanted to give it a try in a component I wrote. The component implements a musician form and is therefore called musician-form :slight_smile: It has the following API:

// new.hbs
{{musician-form
    musician=model
    bands=bands
    selectedBands=selectedBands
    on-submit=createMusician}}

As is often the case, the form is used both on a new and an edit page. In the latter case, when it’s used to update an already existing musician, the selectedBands is not passed in as I want the bands related to the band to be pre-selected:

// edit.hbs
{{musician-form musician=model bands=bands on-submit=saveMusician}}

Let’s actually see how it is used in the component’s template:

{{!-- app/templates/components/musician-form.hbs --}}
{{#power-select-multiple
  options=bands
  selected=selectedBands
  (...)
  onchange=(action (mut selectedBands)) as |band|}}
  {{band.name}}
{{/power-select-multiple}}

The component initializes the selectedBands if it’s not passed in by the caller, to the bands the musician already belongs to:

// app/components/musician-form.js
export default Component.extend({
  async init() {
    this._super(...arguments);
    if (!this.selectedBands) {
      let bands = await this.musician.get('bands');
      this.set('selectedBands', bands.toArray());
    }
  },
  (...)
});

That works wonderfully.

Let’s switch to named arguments:

{{!-- app/templates/components/musician-form.hbs --}}
{{#power-select-multiple
  options=@bands
  selected=@selectedBands
  (...)
  onchange=(action (mut selectedBands)) as |band|}}
  {{band.name}}
{{/power-select-multiple}}

The first case, passing in selectedBands on the new page still works, but when selectedBands is not passed in, the component breaks – no band is pre-selected for the dropdown, even though the init method still runs the this.set('selectedBands', bands.toArray()) line just fine. So it seems like setting an attribute in the component’s code when it’s not passed in doesn’t make it available as a named argument, so setting it to a default value is not possible. (Note that selected=this.selectedBands works!)

The other thing to note is that (mut @selectedBands) is not possible either, an error is thrown (Assertion Failed: You can only pass a path to mut).

Do you know of a way to make setting default values to arguments work with named arguments? I find it’s a really useful pattern and would love to have it work with this new feature. Thank you.


#2

I had this discussion with @rwjblue and it seems like this is the intended behavior. One reason is because with @myArg you know that comes directly from outside of the component, so any defaults kind of mess with that.

The suggested ways to do default values are using something like

  • Use init hook and set a new value (e.g. myUpdatedArg) using the passed in and the default value
  • Use getters (IMO should be used more in Ember code)
  • {{or @myArg 'default'}}

With the second option, you get the benefit of using {{this.myUpdatedArg}} which you know might have been changed in the component.js.

I think there is some room for the addon ecosystem to do something about better DX. I think https://github.com/ciena-blueplanet/ember-prop-types is positioned really well for this (or maybe ember-decorators).


#3

Thank you for your swift reply. Being able to see at a glance what comes from outside the component indeed has some benefit, I’ll admit.

To set a default value, what kind of getters do you mean?


#4

Something like

get myUpdatedArg() {
  return defaultTo(this.myArg, 'defaultValue');
}

Using https://lodash.com/docs/4.17.10#defaultTo in the example above.

Although not sure if there are any caveats/differences between Ember.Object and ES Classes when using this method. cc @pzuraq


#5

I wrote up a bit about this in this issue thread: