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 +
button, the count will increase by 1. When the user presses the -
button, the count will decrease by 1.
First, let's start with the HTML.
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.
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.
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.
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 thecount
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.
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:
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.
Combining Arguments and State
Instead of allowing the component itself to be responsible for the multiple, let's allow it to be passed in.
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
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
.
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.
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.
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;
}
}