Site editor

A note on actions


by Sam Selikoff

I was in Slack the other day and some folks were discussing actions. There was some confusion over all the different ways you can use them: nested calls to the action helper, passing around closure actions, currying arguments...

It made me realize that actions have changed quite a bit over the past two years. The reasons for those changes are not at all obvious to newcomers, and even devs with more experience — myself included — get confused from time to time!

Let's go back to the beginning and try to clear things up.


Why do we use actions in the first place? Why not just use functions?

Take the following example:

<button onclick={{showMessage}}>
  Click me
</button>
export default Controller.extend({

  showMessage() {
    alert('You clicked me!');
  }

});

This template attaches the showMessage function as an event listener to the button's click event. And it works! (Click Example 1).

So — why introduce the concept of "actions" at all?

The answer is context. By default, JavaScript functions that get passed around lose their context. This means that our event handler above is a "naked" function, and its this value is undefined.

This didn't cause any problems for our showMessage function, but what if we had wanted to update an instance property on our controller?

<button onclick={{incrementCounter}}>
  Click me
</button>
export default Controller.extend({

  counter: 0,

  incrementCounter() {
    this.incrementProperty('counter');
  }

});

This doesn't work (click Example 2). The problem is this is undefined, so this.incrementProperty is not a function. (You can see this if you open the console.)

To solve this, we need to bind our function so that this always refers to our controller. Now, we could bind it ourselves in the constructor (click Example 3):

export default Controller.extend({

  counter: 0,

  init() {
    this.super(...arguments);
    // Bind our function so `this` is properly set
    this.incrementCounter = this.incrementCounter.bind(this);
  },

  incrementCounter() {
    this.incrementProperty('counter');
  }

});

As you can imagine, doing this for every function in your app would be tedious. And this is one of the main reasons why the action helper was created.

You'll recall that the context of a controller's template is the controller itself. This means that the template already has access to the correct value of this.

The {{action}} helper, then, essentially does the binding work for us. You pass in your function, and it returns a new function that's bound to the controller.

Let's remove our call to .bind and attach our function using the {{action}} helper.

<button onclick={{action incrementCounter}}>
  Click me!
</button>

<p>The counter is {{counter}}</p>
import Ember from 'ember';

export default Ember.Controller.extend({

  counter: 0,

  incrementCounter() {
    this.incrementProperty('counter');
  }

});

Boom! Bound functions for free (click Example 4).


The next question is, why do we typically put our functions in an actions hash? There are two main reasons:

  1. You get your own namespace, so you can name your actions common words like destroy and not worry about colliding with methods on the base Ember.Object class.

  2. Separating your actions improves clarity. Actions typically result from user interactions and kick off complex data flows in your application. Having them all in their own hash makes it easier for other developers to see how things get started.

Now, we could move our function to the actions hash

actions: {
  incrementCounter() {
    this.incrementProperty('counter');
  }
}

and then attach our function using actions.incrementCounter

<button onclick={{action actions.incrementCounter}}>

but Ember wanted to bless this pattern, and reinforce it across all of our apps. For this reason, the action helper accepts a string, which it uses to look up the function on the actions hash for us:

<button onclick={{action 'incrementCounter'}}>

This is the standard way to attach actions. You can read this as "plucking" the incrementCounter function off of the actions hash, binding its this value to the current controller, and attaching it to the button's click event listener.

In fact, attaching an action to an element's click listener is so common there's an even simpler way to do it:

<button {{action 'incrementCounter'}}>

Here's the working example (click Example 5). Our template now looks clean and familiar.

The {{action}} helper does a few more things for us, like calling preventDefault() on the event (so anchor tags don't open new pages by default). All this behavior can be customized, but the main point here is that we have a convenient way to create event handlers for the most common situations.


One last point. The {{action}} helper accepts arguments, and passes these along to your functions:

<button {{action 'incrementCounterBy' 2}}>
  Add 2
</button>

<button {{action 'incrementCounterBy' 4}}>
  Add 4
</button>
import Ember from 'ember';

export default Ember.Controller.extend({

  counter: 0,

  actions: {
    incrementCounterBy(val) {
      this.set('counter', this.get('counter') + val);
    }
  }

});

Click Example 6 to see this in action Actions, in action... get it? I'll be here all night.

Now, this is where things get interesting.

We know that {{action}} takes a function, and returns a bound function. Does this mean we can nest {{action}} calls?

Let's try it. We'll wrap the first button in our previous example in another {{action}} helper:

<button {{action (action 'incrementCounterBy' 2)}}>
  Add 2
</button>

It works (click Example 7). The inner call returns a bound function, and the outer call does the same thing (again). So, any function can be passed into the {{action}} helper, including other actions.

Now — why would we do this? In this example, there's no reason. But the inner action is actually doing more than it might seem.

We know that {{action}} binds a function's this value, its context. But it also curries its arguments. This means that 2 in the above inner action call is passed up along with the bound function.

Thus, this call

(action 'incrementCounterBy' 2)

actually returns a bound, partially applied function known as a closure action. This closure action can now be passed around and invoked elsewhere, and both its context and any of its arguments will come along for the ride.

This means we can do things like this:

<button {{action (action 'incrementCounterBy' 2) 4}}>
  Add 2 then 4
</button>

Check it out here (click Example 8). Not the most practical example, but it illustrates the power of the action helper.

Typically you'll see closure actions being passed across component boundaries. Here's another contrived example:

{{! templates/application.hbs }}
{{incrementing-component
  incrementer=(action 'incrementCounterBy' 2)}}

{{incrementing-component}} now has access to this closure action internally via the incrementer property, and it can call it again with additional arguments:

{{! templates/components/incrementing-component.hbs }}
<button {{action incrementer 4}}>
  Call incrementer with 4 as an additional arg
</button>

Hopefully this helped you better understand actions! You should now be more prepared the next time you come across this versatile helper.

More resources:

Questions?

us, or ask in #media on Discord