website/content/blog/ember-events-vs-actions.md
James Harton 68d297ef61
All checks were successful
continuous-integration/drone/push Build is passing
feat: import contents of dev.to blog.
2023-08-16 14:53:37 +12:00

176 lines
9 KiB
Markdown

---
title: Events vs Actions in Ember.js
published: false
description: Short explanation of javascript events and how they differ to actions in Ember.
date: 2019-01-28
tags: ember, javascript
---
Recently I was working with some of my team on an Ember component which needed to react to [JavaScript events](http://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Events) they expressed some confusion about the difference between JavaScript events and Ember's [Action system](https://guides.emberjs.com/v3.7.0/templates/actions/). I decided to write up the basics here.
## Blowing bubbles
One of the fundamental behaviours of JavaScript DOM events is bubbling. Let's focus on a `click` event, although the type of event is arbitrary. Suppose we have an HTML page composed like this:
```html
<html>
<body>
<main>
<p>Is TimeCop a better time travel movie than Back To The Future?</p>
<button>Yes</button>
<button>No</button>
<button>Tough Call</button>
</main>
</body>
</html>
```
Supposing I load this page in my browser and I click on the "Tough Call" button (one of three correct answers on this page) then the browser walks down the DOM to find the element under the mouse pointer. It looks at the root element, checks if the coordinates of the click event are within that element's area, if so it iterates the element's children repeating the test until it finds an element that contains the event coordinates and has no children. In our case it's the last `button` element on the screen.
Once the browser has identified the element being clicked it then checks to see if it has any click event listeners. These can be added by using the `onclick` HTML attribute (discouraged), setting the `onclick` property of the element object (also discouraged) or by using the element's `addEventListener` method. If there are event handlers present on the element they are called, one by one, until one of the handlers tells the event to stop propagating, the event is cancelled or we run out of event handlers. The browser then moves on to the element's parent and repeats the process until either the event is cancelled or we run out of parent elements.
## Getting a handle on it
Event handlers are simple javascript functions which accept a single [Event](http://developer.mozilla.org/en-US/docs/Web/API/Event) argument (except for `onerror` which gets additional arguments). [MDN's Event Handlers Documentation is very thorough, you should read it](https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Event_handlers).
There are some tricky factors involving the return value of the function; the rule of thumb is that if you want to cancel the event return `true` otherwise return nothing at all. The `beforeunload` and `error` handlers are the exception to this rule.
## A little less conversation
Ember actions are similar in concept to events, and are triggered by events (`click` by default) but they propagate in a different way. The first rule of Ember is "data down, actions up". What this means is that data comes "down" from the routes (via their `model` hooks) through the controller and into the view. The view emits actions which bubble back "up" through the controller to the routes.
Let's look at a simple example. First the router:
```javascript
import Router from "@ember/routing/router";
Router.map(function () {
this.route("quiz", { path: "/quiz/:slug" });
});
export default Router;
```
Now our quiz route:
```javascript
import Route from "@ember/routing/route";
export default Route.extend({
model({ slug }) {
return fetch(`/api/quizzes/${slug}`).then((response) => response.json());
},
});
```
Now our quiz template:
```handlebars
<p>{{model.question}}</p>
{{#each model.answers as |answer|}}
<button {{action "selectAnswer" answer}}>{{answer}}</button>
{{/each}}
```
### A quick aside about routing
When we load our quiz page Ember first enters the `application` route and calls it's `model` hook. Since we haven't defined an application route in our app Ember generates a default one for us which returns nothing from it's model hook. Presuming we entered the `/quiz/time-travel-movies` URI the router will then enter the `quiz` route and call the model hook which we presume returns a JSON representation of our quiz. This means that both the `application` and the `quiz` route are "active" at the same time. This is a pretty powerful feature of Ember, especially once routes start being deeply nested.
### More bubble blowing
When an action is fired Ember bubbles it up the chain; first to the quiz controller, then to the `quiz` route and then to the parent route and so on until it either finds an action handler or it reaches the application route. This bubbling behaviour is pretty cool because it means we can handle common actions near the top of the route tree (log in or out actions for example) and more specific ones in the places they're needed.
Notably Ember will throw an error if you don't have a handler for an action, so in our example above it will explode because we don't handle our `selectAnswer` in the controller or the route.
### The lonesome component
Ember's "data down, actions up" motto breaks down at the component level. Ember components are supposed to be atomic units of UI state which don't leak side effects. This means that our options for emitting actions out of components are deliberately limited. Actions do behave exactly as you'd expect within a component, except that there's no bubbling behaviour. This means that actions that are specified within a component's template which do not have a corresponding definition in the component's javascript will cause Ember to throw an error.
The main way to allow components to emit actions is to use what ember calls "closure actions" to pass in your action as a callable function on a known property of your component, for example:
```handlebars
{{my-button onSelect=(action "selectAnswer" answer) label=answer}}
```
```javascript
import Component from "@ember/component";
import { resolve } from "rsvp";
export default Component({
tagName: "button",
onSelect: resolve,
actions: {
selectAnswer(answer) {
return this.onSelect(answer);
},
},
});
```
This is particularly good because you can reuse the component in other places without having to modify it for new use cases. This idea is an adaptation of the dependency injection pattern.
### The eventual component
There are three main ways components can respond to browser events. The simplest is to use the `action` handlebars helper to respond to your specific event, for example:
```handlebars
<div
{{action "mouseDidEnter" on="mouseEnter"}}
{{action "mouseDidLeave" on="mouseLeave"}}
>
{{if mouseIsIn "mouse in" "mouse out"}}
</div>
```
As you can see, this can be a bit unwieldy when responding to lots of different events. It also doesn't work great if you want your whole component to react to events, not just elements within it.
The second way to have your component respond to events is to define callbacks in your component. This is done by defining a method on the component with the name of the event you wish to handle. Bummer if you wanted to have a property named `click` or `submit`. There's two things you need to know about Component event handlers; their names are camelised ([full list here](https://guides.emberjs.com/v3.7.0/components/handling-events/#toc_event-names)) and the return types are normalised. Return `false` if you want to cancel the event. Returning anything else has no effect.
```javascript
import Component from "@ember/component";
export default Component({
mouseIsIn: false,
mouseDidEnter(event) {
this.set("mouseIsIn", true);
return false;
},
mouseDidLeave(event) {
this.set("mouseIsIn", false);
return false;
},
});
```
The third way is to use the `didInsertElement` and `willDestroyElement` component lifecycle callbacks to manually manage your events when the component is inserted and removed from the DOM.
```javascript
export default Component({
mouseIsIn: false,
didInsertElement() {
this.onMouseEnter = () => {
this.set("mouseIsIn", true);
};
this.onMouseLeave = () => {
this.set("mouseIsIn", false);
};
this.element.addEventListener("mouseenter", this.onMouseEnter);
this.element.addEventListener("mouseleave", this.onMouseLeave);
},
willRemoveElement() {
this.element.removeEventListener("mouseenter", this.onMouseEnter);
this.element.removeEventListener("mouseleave", this.onMouseLeave);
},
});
```
Note that using either of the last two methods you can use `this.send(actionName, ...arguments)` to trigger events on your component if you think that's cleaner.
## Conclusion
As you can see, actions and events are similar but different. At the most basic level events are used to make changes to _UI_ state and actions are used to make changes to _application_ state. As usual that's not a hard and fast rule, so when asking yourself whether you should use events or actions, as with all other engineering questions, the correct answer is "it depends".