How to Understand Ember Actions in Context

Tags

Action clapperboard

When creating any web application it will at some point need to respond to user input. In Ember, this is accomplished with “Actions.” Often it seems that, although we use actions a lot, we might not have a great grasp on what they do.

We know the basics — an action is a function that is most often triggered by user interactions. But to understand any functions in JavaScript, you really need a good bit of context. When using actions, we often pass them down a component tree (DDAU) so that a user is triggering the action on a child or a grandchild of the component where the action is defined. As a result, we have several layers of JavaScript objects between the function being declared and the function being triggered. So, the question is: what is the context in which the function is being executed? Is it where it is declared, or where it is triggered?

Just so it is clear what I am talking about, below is a component with two functions:

// app/components/parent-component.js
export default Component.extend({
  label: 'parent-component',
  someFunc() {
    return this.label;
  },
  actions: {
    someAction() {
      return this.label;
    }
  }
});

These functions appear to do the same thing. When they are invoked they return the label property on the component. And they are functionally the same. The reason that the actions hash exists is to avoid naming collisions with the base functions of an Ember component. It is also worth mentioning that, if you are using the latest version of Ember, you have access to decorators which change this up a little, but we will get to that later.

Despite the functional similarities between action functions and functions directly on the component, the way that we invoke actions can cause them to vary in surprising ways. The potential gotcha here, as is often the case with JavaScript, has to do with context. This is most obvious when we are passing actions down through layers of components.

Let’s look at a few examples to really see how this works. These examples can be seen in action here

First let’s look at the template for the component above:

{{! app/templates/components/parent-component.hbs}}
PARENT COMPONENT
<ChildComponent 
  @someFunc={{someFunc}}
  @someAction={{action 'someAction'}} />

This component has two functions that it passes down to a child component. The first is a function and the second, our action, uses Ember’s action helper. It is this helper that is the key context with which the action is triggered. If our ChildComponent looks like this:

// app/components/child-component.js
export default Component.extend({
  label: "child-component"
});
{{! app/templates/components/child-component.hbs}}
<h3>Component Actions</h3>
<button onclick={{action @someFunc}}>Some Func</button>
<button onclick={{action @someAction}}>Some Action</button>

What would you expect to be the return value of each of our functions? Here is a hint, it is the action helper that binds the context of the action when it is called. Given this, the return value of @someFunc will be the label on the child-component as we are not using the action helper when we pass that function into the child, the context is that of the child. However, because of our use of the action helper, the context for @someAction is bound to the ParentComponent, so its return value is parent-component.

Manually Binding Context

It is worth pointing out that you can manually bind the context of an action. If you have an action on a service, you can use the action helper to target that service. You can also manually bind the context of a function by using Sergio Arbeo’s ember-bind-helper. The Ember Bind Helper’s README is more than enough to get you started if you decide to take that route. To use Ember’s action helper to bind the context you just have to pass in a target that you have access to in the current context.

{{! app/templates/components/child-component.hbs}}
<button onclick={{action 'serviceAction' target=this.myService}}>Some Service Action</button>
// app/components/child-component.js
export default Component.extend({
  myService: service(),
  label: 'child-component'
});
// app/services/my-component.js
export default Ember.Service.extend({
  label: 'my-service',
  actions: {
    serviceAction(){
      return this.label;
    }
  }
});

This will return my-service, not child-component.

The Path Forward: Decorators

Moving forward we will no longer need to use the actions hash when coding for user interactions. We will reach for the action decorator. Although the new syntax is more closely aligned with regular JavaScript, using the decorator, the function is now declared at the root of the object. As a result, we have to be aware that naming collisions are possible. To see this decorator syntax in action, let’s inject our service into our parent component and rewrite the component using the new class syntax.

// app/components/parent-component.js

import Component from '@ember/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';

export default class ParentComponent extends Component {
  @service myService;
  label = 'parent-component';
  @action
  someAction() {
    return alert(this.label);
  }
}

If you are on the latest version of Ember, that just works. Context in Ember actions is really straightforward. It is likely just one of those things that, when following the Ember way, a developer seldom thinks about. But whether you are using Ember, React, or Vue, understanding context allows us to better use our tools.

DockYard is a digital product agency offering custom software, mobile, and web application development consulting. We provide exceptional professional services in strategy, user experience, design, and full stack engineering using Ember.js and Elixir. With consultants nationwide, we’ve got experts in key markets across the United States, including San Francisco, Kansas City, New Orleans, Indianapolis, Nashville, Cincinnati, Raleigh-Durham, Boston, and New York.