Ember Closure Actions in Depth

, ember.js

I’ve been using Ember’s closure actions in all my projects for a while now and I like them so much that I almost take for granted that everybody has embraced them too.

While it’s true that most ember devs have started using them, I’ve seen that many people haven’t fully grasped all its potential and the new patterns they enable, so I want to explain them a bit more, starting from the basics and going towards more advanced patterns.

What is a closure action?

First things first, let’s see a few actions in the wild.

Which ones of these are closure actions?

1
2
3
4
<button {{action "sayHi"}}>Salute</button>
<button onclick={{action "sayHi"}}></button>
{{my-button action=(action "sayHi")}}
{{my-button action="sayHi"}}

If you said 2nd and 3rd you guessed right.

The 4th example is clearly just a regular attribute passing. It is named action but could be named weasel and would be exactly the same, just a string.

The 1st line is a bit more fuzzy. That line is telling Ember to invoke the “sayHi” action when the button is clicked. Why doesn’t it qualify as a closure action?

We have to start by explaining that the action helper is overloaded and, depending on which context it is used in, it does entirely different things.

action in the “element space”

When invoked within the context of an html element (what is known as the “element’s space”), the action keyword does a quite a lot of stuff. It registers in the global Ember dispatcher one (or some) handlers for events whose target is the element in which it was invoked.

In the first line of the previous example, the helper is registering in the event handler for the click attached automatically by Ember to the root of your app, a handler that will be invoked when the target of the event is that button. It’s also doing the same thing for the keypress event when the pressed key is enter. That handler, in turn, will call the sayHi action on the context of that template.

Apart from all that, the action helper will call preventDefault() on that event, hijacking its default behavior, like by example submitting the form in which that button lives. However, that will not prevent the event from bubbling.

Both behaviors can be tuned from the template with options:

1
<button {{action "sayHi" preventDefault=false bubble=false}}>Salute</button>

By default the action helper registers for DOM click events but you can specify a different event name:

1
<button {{action "sayHi" on="double-click"}}>Salute</button>

As we see, there is quite a process involved here, but this is not a closure action.

action as a closure creator

On the other hand, the second usage of this helper is as closure creator. This is the behavior the helper has in any other situation different than the one described above.

It does something much simpler but also much more powerful. It creates a function with a closure that invokes another function

That’s it. You can think about it almost like this snippet:

1
2
3
function createClosureAction(func, ...args) {
  return func.bind(this, ...args);
}

Note: This is an oversimplification. Closure actions don’t even use Function#bind at all, but it’s close enough to grasp the basics.

It does nothing else. It doesn’t register any event handler, doesn’t prevent default or stop bubbling. It just binds the given function to the current context and arguments. When provided with a string it will assume that it’s the name of an action and will extract it from the current context.

So, if it is such a simple helper, why is it so cool?

The good parts of being a function

Converting your actions to functions you can pass around has many advantages.

Simpler mental model

This one is often understated.

A function is a value. {{my-component foo=bar}} in a template means that we’re passing to the component a property named "foo" whose value is the value in bar. What if bar is a function? Nothing: it’s the same idea. We’re just passing a value.

What if we do {{yield (action “submit”)? Same thing, we’re just yielding a value that happens to be a function. That is all.

Detect errors eagerly

On this usage, {{action “foo”}} is a helper and as such, tries to do its work as soon as the template is rendered. If the current context doesn’t have a function named foo it will fail right away, unlike element’s space usage where it will fail at runtime when that event is fired and, oh surprise, there is no action named foo!!

It still surprises me how many refactoring bugs this simple feature has caught for me.

Return values

Functions have return values. Closure actions are functions. Therefore modus ponendo ponens, closure actions have return values too.

If the component calls this.get("foo")("someArg"), it is just invoking a function and will have access to its return value, if any. This enables bidirectional communication with the parent context.

As a real world example, an {{async-button action=(action “submit”)}} will invoke the action and that action can return a promise. Thanks to having access to that promise, the button can then change to a “loading” state while that returned promise is pending.

Removes logic from middlemen

Actions can be passed down/up as many levels as desired.

When passing actions as just strings with their names to be invoked with the sendAction(actionName) in the receiver component, each call to sendAction will only reach the closest component in the hierarchy. This means that when the logic to be executed lives a few layers up from where the event that triggers it is fired, each one of the intermediate components has to define an action to capture and re-thorw the call to the next level.

That is a lot of coupling.

Closure actions just being functions bound to a given scope means that they can just be passed as simple values from the root to the leaves. Then, the last component in the chain can invoke that action with via this.get("functionName")() and it will be executed with the provided arguments in the correct scope, releasing intermediate nodes in the chain of the burden of capture-and-rethrow actions by its name.

Closure actions as event handlers

When we attach a closure action to an event handler like this:

1
<button onclick={{action "sayHi"}}></button>

Ember.js is just doing

1
button.onclick = wrappedSayHiFunction;

That means that, as with all event handlers attached to DOM elements, it is invoked with the event as first argument. But since the handler function already has one argument bound, the event is received as the second argument instead.

There is little-to-no magic happening here, just regular javascript.

That also means that is up to the user to call preventDefault or stopPropagation on the received event.

Not only actions can be actions

Tipically when using the action helper you will pass a string containing the name of some function that lives in the actions property of your component, like this: <button onclick={{action "sayHi"}}></button>

But did you know that you can extract any function from your component by passed an unquoted reference?

template.hbs
1
<button onclick={{action sayHi}}></button>
component.js
1
2
3
4
5
6
7
8
export default Ember.Component.extend({
  actions: {
    // empty
  },
  sayHi() {
    console.log('Hi!');
  }
});

This is useful by example if you want a component to execute some arbitrary logic that can be decided by its parent, and passed as an argument.

my-button/template.hbs
1
<button onclick={{action clickHandler}}></button>
salutator/template.hbs
1
{{my-button clickHandler=sayHi}}
salutator/component.js
1
2
3
4
5
export default Ember.Component.extend({
  sayHi() {
    console.log('Hi!');
  }
});

Now that you know that it’s possible, I strongly discourage you to do this.

This enters the personal opinion realm but I feel that if we start extracting methods of components to use them as actions, why do we have the actions hash at all?

Instead, when I want to be able to customize from outside a component what an action will do, I define an action inside the component that just delegates all its logic to a method that can be passed in from the outside.

my-button/template.hbs
1
<button onclick={{action "clickHandler"}}></button>
my-button/component.js
1
2
3
4
5
6
7
8
9
10
export default Ember.Component.extend({
  actions: {
    clickHandler() {
      this.clickHandler(...arguments);
    }
  },
  clickHanldler() {
    throw new Error('clickHandler must be provided!!');
  }
});
salutator/template.hbs
1
{{my-button clickHandler=sayHi}}
salutator/component.js
1
2
3
4
5
export default Ember.Component.extend({
  sayHi() {
    console.log('Hi!');
  }
});

The behavior is the same but reading the code of the component is crystal clear that clickHandler is intended to be passed from the outside. I find this approach self-documented and more idiomatic.

Currying

Taken from the wikipedia:

Currying is the technique of translating the evaluation of a function that takes multiple arguments (or a tuple of arguments) into evaluating a sequence of functions

Without deep diving into Haskell theory and monad madness, let me only explain how you can apply this technique for your own benefit with an example.

Having this declaration in the templates:

1
2
3
4
5
6
7
{{my-form onSubmit=(action "submitWithAjax" "/users/registration")}}

<!-- Within my-form.hbs -->
{{my-button onUsage=(action onSubmit data)}}

<!-- Within my-button.hbs -->
<button onclick={{onUsage}}></button>

In the sendWithAjax function of the top-most scope:

1
2
3
4
5
actions: {
  submitWithAjax(url, data, e) {
    // how come do I receive 3 arguments?!?!111
  }
}

Because currying.

Each invocation of the action helper created a new closure and bound the given arguments to the function. The first invocation bound the this to the current context and the first arguments to the string "/users/registration". The second invocation bound the data to the function. Since the context and the first argument were already bound, that argument takes the 2nd position. Last, but not least, that function is assigned to the onclick property of that DOM element, and when it’s invoked the event is passed, occupying the last position in the arguments list.

Translated to javascript, it’s more or less equivalent to:

1
2
3
4
5
let funcOne = func.bind(context, "/users/registration");
// ...
let funcTwo = funcOne.bind(secondContext.data);
// ...
button.onclick = funcTwo;

Using currying each level can augment the action with some extra arguments, and that frees the last level of the chain of the responsibility of holding all the information needed to perform the action. Each piece of data can live in the level where it makes more sense, without leaking outside it.

Extracting values out of the first argument

The action helper accepts a set of key/value pairs as last argument. One special option you can use is the value option. This option holds a path, and the closure action will be invoked with the value contained in that path on the first argument instead of the first argument.

The most common example of this is to extract some value out of the event.

1
2
<input type="text" onchange={{action "logArgs"}}> <!-- Logs `[event]` -->
<input type="text" onchange={{action "logArgs" value="target.value"}}> <!-- Logs the value of the input (e.target.input) -->

But often people doesn’t realize that it works with anything, not only events. Per example:

1
2
<button onclick={{action "logArgs" "abcde" value="length"}}> <!-- Logs `[5]` -->
<button onclick={{action (action "logArgs" "foo" value="length") value="screenX"}}>Press me</button> <!-- Logs `[3, 741]` -->

This is specially useful when combined with the next point.

DDAU and the mut helper

The Data Down - Actions Up approach to propagate state changes in an app advices us to not rely on two-way bindings for mutating state but on explicit function invocations. Consider the next code example:

template.hbs
1
2
3
4
<p>Select shipment type</p>
<button onclick={{action "selectShipmentType" "regularDelivery"}}>Standard delivery</button>
<button onclick={{action "selectShipmentType" "urgent"}}>48h delivery</button>
<button onclick={{action "selectShipmentType" "next-day"}}>Next day delivery</button>
component.js
1
2
3
4
5
actions: {
  selectShipmentType(type /*, e */) {
    this.set('shipmentType', type);
  }
}

Pretty common. Changing the selection invokes the selectShipmentType action with one value, and that action mutates some value. However, setting some state as result of some user interaction is so common that there is a built-in way to avoid having to define such simple functions over and over: the mut helper.

This helper creates a function that will set on some property the first argument it receives. The following code is equivalent.

template.hbs
1
2
3
4
<p>Select shipment type</p>
<button onclick={{action (mut shipmentType) "regularDelivery"}}>Standard delivery</button>
<button onclick={{action (mut shipmentType) "urgent"}}>48h delivery</button>
<button onclick={{action (mut shipmentType) "next-day"}}>Next day delivery</button>

This, in combination with the value option, can save you the tedium of defining super simple functions to extract some attribute from the first argument.

1
2
<p>The mouse X position is: {{mouseX}}</p>
<div class="draw-canvas" onmousemove={{action (mut mouseX) value="screenX"}}></div>

Remember that you can use this not only with events but with any object.

For example, if you want to have an instance of Ember Power Select bound to some queryParam, you can do this:

controller.js
1
2
3
export default Ember.Controller.extend({
  queryParams: ['teacherId']
});
route.js
1
2
3
4
5
6
7
8
export default Ember.Route.extend({
  queryParams: {
    teacherId: { refreshModel: true }
  },
  model({ teacherId }) {
    return this.store.query('homework', { teacherId });
  }
});
helpers/find-by.js
1
2
3
4
5
import Ember from 'ember';

export default Ember.Helper.helper(function([collection, attrName, attrValue]) {
  return collection.find(el => Ember.get(el, attrName) === attrValue);
});
template.hbs
1
2
3
4
<p>Select a teacher to filter homework</p>
{{#power-select options=teachers selected=(find-by teachers 'id' teacherId) onchange=(action (mut teacherId) value="id") as |teacher|}}
  {{teacher.fullName}} - {{teacher.group.name}}
{{/power-select}}

Selecting a teacher will pass that Teacher model to the onchange function. From that teacher we extract only the id with value="id" which is passed to (mut teacherId) as its first argument. That updates the teacherId property in the controller, that is bound to a queryParam in the URL, tehen refreshing the model hook of the route.

Neat.

Summary

Going forward the Ember 2.0 path, closure actions are one of the tools in your belt you’re going to use more often.

Understanding them fully will allow you to squeeze them to your advantage and write simpler and more maintainable code.

Comments: