Alternatives to mut

05 February 2021

The mut helper is a useful helper to define an action that updates a property in your template. Defining this action in the backing class (controller or component) is usually boilerplate code so it's nice to be able to do it "inline" instead. If you haven't yet created the class, mut can spare you from having to do that altogether.

However, it has a few shortcomings that Chris Garrett superbly explained in his post, "On {{mut}} and 2-way binding". My beef with it is that it's both a getter and a setter, which can lead to weird scenarios. This is also the reason you have to use it in conjunction with a "function generating" helper like action (in the pre-Octane world) or fn.

Before I go on, let me say that there's nothing wrong with using mut - I'm not trying to convince you to stop using it. The post is for those who haven't quite warmed up to mut (or had a fallout, like I had) and would like to explore a few alternatives.

1. Define the event handler in JavaScript

The most flexible of all the approaches is to simply define the event handler in the backing JS class:

1<input
2  name="email"
3  type="email"
4  value={{this.email}}
5  {{on "input" this.updateEmail}}
6/>
1class UserForm extends Component {
2  @action
3  updateEmail(event) {
4    let { value } = event.target;
5    this.email = value;
6  }
7}

This has the advantage that you have all the power of JavaScript at your disposal and can implement anything given the DOM event as the input:

 1class UserForm extends Component {
 2  @action
 3  updateEmail(event) {
 4    let { value } = event.target;
 5    if (value.includes('#')) {
 6      console.error('You cannot use # in an email address.');
 7      return;
 8    }
 9    this.email = value;
10  }
11}

The drawback is that you need to define the JS class and that in lots of cases, the code is pure boilerplate. Kinda un-Emberlike (that's not a word).

2. ember-set-helper (the author's recommendation)

The ember-set-helper add-on defines a set helper (no surprises there) that works similarly to mut, without the quirkiness of the latter.

It can be used to set a specific value, if the last argument is passed:

1<button
2  type="button"
3  {{on "click" (set this "showConfirmation" true)}}
4>
5  Delete
6</button>

If the last argument (the value to set the property to) is missing, it returns a function that sets the value (this.showConfirmation) to the one it is called with. That feature is what makes it suitable for more dynamic scenarios:

1<PowerSelect
2  options={{this.currencies}}
3  selected={{this.selectedCurrency}}
4  onChange={{set this "selectedCurrency"}} as |currency|
5>
6  {{currency.abbreviation}}
7</PowerSelect>

This works becuse PowerSelect calls the onChange method with the selected item - set then sets this.selectedCurrency to it.

Let's see the classic input event handling scenario:

1<input
2  name="email"
3  type="email"
4  value={{this.email}}
5  {{on "input" (set this "email")}}
6/>

The classical input handling example doesn't quite work with it yet, as set in that scenario is called with the DOM event, so the value property will be set to it:

Input value set to input event

We need a way to extract target.value from the event before we assign it to the value property. Something like the get helper would work, but get doesn't return a function so we cannot use it in this context.

Let's create our own helper, extract:

1import { helper } from '@ember/component/helper';
2import { get } from '@ember/object';
3
4export default helper(function extract([path, fn]) {
5  return function (object) {
6    fn(get(object, path));
7  };
8});

The helper returns a function that, when called with an object, extracts a property path from it (in our case, that will be target.value) and then calls a function (also passed in) with the extracted value.

Armed with extract, let's handle the event from the template:

1<input
2  name="email"
3  type="email"
4  value={{this.email}}
5  {{on "input" (extract "target.value" (set this "email"))}}
6/>

It now works as intended, the email property is updated as we type in the input box.

Input value set correctly

If you already use ember-composable-helpers

ember-composable-helpers is one of the most popular add-ons in the Ember ecosystem. It provides dozens of useful template helpers to work with arrays, objects and actions.

One of the helpers is called pick which does exactly what the above extract helper - and I peeked under the hood to see how it's implemented. If you already have that add-on in your project's dependencies, you can start using pick.

3. Managing the state in the template

If you have some state that doesn't need to live on the backing class of the template (the controller in case of route templates, the component class in the case of component templates), you can define and manage that state solely in the template.

One add-on that does this just great is ember-microstates. The idea behind microstates is that each piece of state owns its operations - numbers can increment, boolean values can toggle, etc.

A common example is showing a modal when a button is clicked and then closing it when the user clicks the cancel button, or the little x in the corner.

A perfect use for microstates:

 1{{#let (state false) as |showModal|}}
 2  <button
 3    type="button"
 4    {{on "click" (fn showModal.set true)}}
 5  >
 6    See options
 7  </button>
 8
 9  {{#if (value-of showModal)}}
10    <ModalDialog
11      @onClose={{fn showModal.set false}}
12    >
13      {{!-- ... --}}
14    </ModalDialog>
15  {{/if}}
16{{/let}}

The state helper creates the microstate with a default value. Notice it has a set method that can be used to update its value.

I really like this because it keeps the state locally, right next to where it's used which makes it easier (quicker) to read. The drawback is that you can't access the state defined this way in your JavaScript, the class that backs the template, so you can't use this solution in more complex scenarios.

Conclusion

As I said, if you're perfectly happy with mut, it's totally fine to keep using it - it's not going away any time soon. If you're not however, I hope I've given you a lead in finding appropriate alternatives for your use case.

Share on Twitter