Named arguments in Ember.js

23 July 2018

Ember 3.1 introduced the concept of named arguments that implemented the similarly named RFC.

As I started to use them, there were a couple of things I didn't understand and started a discussion on the official forums. In the following post, I'd like to summarize what I learned, hopefully decreasing the time you need to get the details right.

(If you already know all about them, you're probably better off using your time in another way – come play a game of chess with me.)

The syntax

The example I'm going to use is taken from the app (and book) I'm currently developing/writing, Rock and Roll with Ember.js Encore.

The component implements rendering and managing the state of a musician form and thus called musician-form. It receives the musician object and an action it carries out after the form has been successfully saved:

1{{musician-form musician=model afterSave=gotoMusicianDetails}}

So how do we convert this call to named arguments? Let's see:

1{{musician-form musician=model afterSave=gotoMusicianDetails}}

Right, the calling site didn't change one bit, it's the component's template where the conversion needs to be made:

 1{{!-- app/templates/components/musician-form.hbs --}}
 2{{#if showErrors.name}}
 3  <div>{{v-get @musician 'name' 'message'}}</div>
 4{{/if}}
 5<div class="mb2">
 6  <label for="name">Name</label>
 7</div>
 8<div class="mb2">
 9  {{input type="text"
10    id="name"
11    value=@musician.name
12    focus-out=(action (mut showErrors.name) true)}}
13</div>

See the beautiful @ sigil in the call to v-get on line 3 and @musician.name on line 11? That's what distinguishes a named argument from other dynamic expressions in the template.

The rationale of named arguments

So why is this a big thing? Or, in other words, what advantage using @musician.name gives us over musician.name?

The biggest benefit is that they establish an immutable binding between the value that was passed in and the one used in the template. In more comprehensible terms, the guarantee that named arguments give us is that the value that gets used in the template is the value that was passed in.

As a corollary of this property of named arguments, when you look at a component's template and see named arguments, you know that their value comes from the invocation, where the component was called from. You don't even have to look at the component's JavaScript file to understand the behavior.

(As another corollary, you can turn all argument calls in template-only components to named arguments.)

Where can I not use named arguments?

Let's take a look at the component's template again:

 1{{!-- app/templates/components/musician-form.hbs --}}
 2{{#if showErrors.name}}
 3  <div>{{v-get @musician 'name' 'message'}}</div>
 4{{/if}}
 5<div class="mb2">
 6  <label for="name">Name</label>
 7</div>
 8<div class="mb2">
 9  {{input type="text"
10    id="name"
11    value=@musician.name
12    focus-out=(action (mut showErrors.name) true)}}
13</div>

showErrors.name on line 12 is not used as a named argument. That tells us it doesn't come from "outside", but is a property of the JavaScript class backing the template, musician-form.js. We thus know where to look to understand (or fix) its behavior.

The difference between using named arguments and regular arguments

It's important to understand that the difference between named and (for lack of a better word) regular arguments doesn't come from the caller. You also don't need to define an argument is named.

It's whether you use the @ sign in front of it or you don't that makes the difference. To demonstrate this, let's pass another argument in that restricts the number of bands that can be chosen. We'll call it maxBands:

1{{musician-form musician=model (...) maxBands=3}}

And let's display it in our template:

1{{!-- app/templates/components/musician-form.hbs --}}
2<div>
3  <label for="bands">Bands (max. {{@maxBands}}</label>
4</div>
5<div>
6  {{#power-select-multiple ...}}
7</div>

So far so good:

Max. number of bands shown correctly

Let's now tweak this a little bit and say we want to assign a default value of 5 unless a value is explicitly passed in by the caller. We wring out a few lines of code to implement this and remove the maxBands=3 from where the component is called from so that the default value is used:

1// app/components/musician-form.js
2Component.extend({
3  init() {
4    this._super(...arguments);
5    this.set('maxBands', this.maxBands || 5);
6  }
7});

Uh-oh, that's not quite what we expected, the value is empty: Max. number of bands shown incorrectly

We used a named argument, {{@maxBands}}, and named arguments use the value from the caller unchanged, as it was passed in. {{@maxBands}} is undefined, as maxBands wasn't passed in.

Let's see how that changes if we use a regular argument in the template, {{maxBands}}:

1{{!-- app/templates/components/musician-form.hbs --}}
2<div>
3  <label for="bands">Bands (max. {{maxBands}}</label>
4</div>
5<div>
6  {{#power-select-multiple ...}}
7</div>

What do you think the displayed value will be? Default value shown correctly

The default value is displayed correctly as {{maxBands}} will pick up the the change in the value of this.maxBands, unlike {{@maxBands}} that creates an immutable binding. So that's an area where you don't want to use named arguments in the template – by using a regular argument you indicate to the reader (that can always include your future self) that they need to look at the backing component code to understand what's going on.

Conclusion

Named arguments are an improvement both in terms of clarity in templates (you know at a glance where they come from, so you spend less time figuring it out) and debuggability. Using them also has a positive effect on performance, and if you want to learn more about this, I advise you to read the original RFC which is quite short and the the point.

Named arguments are great, start using them today.

Thanks to Ricardo Mendes, Robert Jackson, Ilya Radchenko, jelhan, and Edward Faulkner for helping my understanding of named arguments and thus making the below post possible.

Share on Twitter