Component State and Actions

While you can accomplish a lot in Ember using HTML templating, you'll need JavaScript to make your application interactive.

Let's start with a small example, a counter component. When the user presses the +1 button, the count will increase by 1. When the user presses the -1 button, the count will decrease by 1.

First, let's start with the HTML.

<p>0</p>

<button type="button">+1</button>
<button type="button">-1</button>

Tracked Properties

To make this work, we will need to stop hard coding the number, and we will need to wire up the buttons.

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

export default class CounterComponent extends Component {
  @tracked count = 0;
}

There are a few things going on here, but the most important part is @tracked count = 0. This line creates a dynamic value called count, which you can stick inside of the template instead of hard coding it.

<p>0</p>
<p>{{this.count}}</p>

<button type="button">+1</button>
<button type="button">-1</button>

When we use {{this.count}} in the component template, we're referring to a property that we defined in the JavaScript class.

The output looks the same as before, but now the 0 comes from JavaScript, and after some more work, we can change its value with the buttons.

HTML Modifiers and Actions

Next, we want to wire up the buttons. When the user presses +1, we want this.count to go up by 1. When the user presses -1, we want it to go down by 1.

To attach an event handler to an HTML tag, we use the on HTML modifier. HTML modifiers are an Ember syntax that allow us to attach logic to a tag.

<p>{{this.count}}</p>

<button type="button">+1</button>
<button type="button" {{on "click" this.increment}}>+1</button>
<button type="button">-1</button>
<button type="button" {{on "click" this.decrement}}>-1</button>

To make those event handlers do something, we will need to define actions in the component JavaScript. An action is a JavaScript method that can be used from a template.

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class CounterComponent extends Component {
  @tracked count = 0;

  @action
  increment() {
    this.count = this.count + 1;
  }

  @action
  decrement() {
    this.count = this.count - 1;
  }
}

Now, when the +1 and -1 buttons get clicked, the number displayed will change.

Passing Arguments to Actions

Our counter has two different actions, increment and decrement. But both actions are mostly doing the same thing. The only difference is that increment changes the count by +1, while decrement changes it by -1.

First, let's turn our increment and decrement methods into a single change method that takes the amount as a parameter.

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class CounterComponent extends Component {
  @tracked count = 0;

  @action
  change(amount) {
    this.count = this.count + amount;
  }
  @action
  increment() {
    this.count = this.count + 1;
  }

  @action
  decrement() {
    this.count = this.count - 1;
  }
}

Next, we'll update the template to turn the click handler into a function that passes an amount (for example, 1 and -1) in as an argument, using the fn helper.

<p>{{this.count}}</p>

<button type="button" {{on "click" this.increment}}>+1</button>
<button type="button" {{on "click" (fn this.change 1)}}>+1</button>
<button type="button" {{on "click" this.decrement}}>-1</button>
<button type="button" {{on "click" (fn this.change -1)}}>-1</button>
Zoey says...
An event handler takes a function as its second argument. When there are no arguments to the function, you can pass it directly, just like in JavaScript. Otherwise, you can build a function inline by using the fn syntax.

Computed Values

Let's say we want to add a button to our counter that allows us to double the current count. Every time we press the button, the current count doubles.

Based on what we've already learned, we'll need:

  • A multiple, a piece of state that represents the number to multiply the count by
  • An action to double the multiple
  • A button in the template that calls the action

But we'll also need a way to multiply the count by the multiple and show it in the template.

Let's start with what we know already. We'll add the multiple tracked property and an action called double that doubles the multiple.

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class CounterComponent extends Component {
  @tracked count = 0;
  @tracked multiple = 1;

  @action
  double() {
    this.multiple = this.multiple * 2;
  }

  @action
  change(amount) {
    this.count = this.count + amount;
  }
}

Then, we'll update the template to call the double action. We'll also add this.multiple to our output to help us confirm that our button is working.

<p>{{this.count}}</p>
<p>× {{this.multiple}}</p>

<button type="button" {{on "click" (fn this.change 1)}}>+1</button>
<button type="button" {{on "click" (fn this.change -1)}}>-1</button>

<button type="button" {{on "click" this.double}}>Double It</button>

To get the multiplied number into the template, we'll use a JavaScript getter.

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class CounterComponent extends Component {
  @tracked count = 0;
  @tracked multiple = 1;

  get total() {
    return this.count * this.multiple;
  }

  @action
  double() {
    this.multiple = this.multiple * 2;
  }

  @action
  change(amount) {
    this.count = this.count + amount;
  }
}

The getter does not need any special annotations. As long as you've marked the properties that can change with @tracked, you can use JavaScript to compute new values from those properties.

We can now update the template to use the total property:

<p>{{this.count}}</p>
<p>× {{this.multiple}}</p>
<p>= {{this.total}}</p>

<button type="button" {{on "click" (fn this.change 1)}}>+1</button>
<button type="button" {{on "click" (fn this.change -1)}}>-1</button>

<button type="button" {{on "click" this.double}}>Double It</button>

And we're all done! If we try to click the plus, minus, or double buttons in any order, we can watch as these three outputs stay up-to-date perfectly.

Zoey says...

You might have been tempted to make total a @tracked property and update it in the double and change actions. But this kind of "push-based" approach creates a lot of bugs. What happens if you create a new way to update multiple or amount properties and forget to update total at the same time?

When you use getters and functions to derive the state you need, you're taking advantage of the benefits of declarative programming. In declarative programming, you describe what you need, not how to get it, which reduces the number of places where you can make mistakes.

Making a total getter that computed the total from the amount and multiple properties was more declarative than setting total in all of the places that could have affected it. If you had changed total directly, you would have taken the "imperative" approach).

Combining Arguments and State

Instead of allowing the component itself to be responsible for the multiple, let's allow it to be passed in.

<Counter @multiple={{this.multiple}} />

<button type="button" {{on "click" this.double}}>Double It</button>
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class DoubleItComponent extends Component {
  @tracked multiple = 1;

  @action
  double() {
    this.multiple = this.multiple * 2;
  }
}

In the Counter component, instead of tracking the multiple internally, we take it as an argument. In the template, we refer to the argument as @multiple.

<p>{{this.count}}</p>
<p>× {{@multiple}}</p>
<p>= {{this.total}}</p>

<button type="button" {{on "click" (fn this.change 1)}}>+1</button>
<button type="button" {{on "click" (fn this.change -1)}}>-1</button>

In templates, we refer to arguments by prefixing them with the @ sign (in this case @multiple). In order to compute this.total, we'll need to refer to the multiple argument from JavaScript.

We refer to a component's argument from JavaScript by prefixing them with this.args..

In JavaScript, we refer to it as this.args.multiple.

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class CounterComponent extends Component {
  @tracked count = 0;
  @tracked multiple = 1;

  get total() {
    return this.count * this.multiple;
    return this.count * this.args.multiple;
  }

  @action
  change(amount) {
    this.count = this.count + amount;
  }
}

The total is now computed by multiplying a piece of local state (this.count) with an argument (this.args.multiple). You can mix and match local state and arguments however you wish, which allows you to easily break up a component into smaller pieces.

Combining Arguments and Actions

We can also pass actions down to components via their arguments, which allows child components to communicate with their parents and notify them of changes to state. For instance, if we wanted to add back the doubling button we had previously, we could using an action passed down via arguments.

<p>{{this.count}}</p>
<p>× {{@multiple}}</p>
<p>= {{this.total}}</p>

<button type="button" {{on "click" (fn this.change 1)}}>+1</button>
<button type="button" {{on "click" (fn this.change -1)}}>-1</button>

<button type="button" {{on "click" this.double}}>Double It</button>
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class CounterComponent extends Component {
  @tracked count = 0;

  get total() {
    return this.count * this.args.multiple;
  }

  @action
  change(amount) {
    this.count = this.count + amount;
  }

  @action
  double() {
    this.args.updateMultiple(this.args.multiple * 2);
  }
}

Now, the Counter calls the updateMultiple argument (which we expect to be a function) with the new value for multiple, and the parent component can update the multiple.

<Counter @multiple={{this.multiple}} @updateMultiple={{this.updateMultiple}} />
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class DoubleItComponent extends Component {
  @tracked multiple = 1;

  @action
  updateMultiple(newMultiple) {
    this.multiple = newMultiple;
  }
}

Learn more

You will frequently create components in an app. Establishing patterns early can help reduce bugs and unforeseen issues. Learn more from the chapter Patterns for Components.

Actions are JavaScript methods that you can call from a template. Find out how you can use actions with recommended patterns from the chapter Patterns for Actions.

© 2020 Yehuda Katz, Tom Dale and Ember.js contributors
Licensed under the MIT License.
https://guides.emberjs.com/v3.25.0/components/component-state-and-actions