Deep Dive on Ember Events

The difference between Ember actions and DOM events and why it matters, plus a really rad flowchart.

A few days ago, I was working on a really exciting new feature. As part of rolling out these changes, I implemented an onboarding tour — a sequence of tooltips that teach users how to interact with the different parts.

I made one small change to a template in one of our Ember apps, and everything broke. Can you guess what happened?

Before: Clicking on “Next Step of the Tour” opened The Thing.

<div class="cool-new-button" {{action "toggleTheThing"}}>
  {{#if shouldShowThisTooltip}}
<div class="tour-tooltip">
This button can open and close The Thing!

<a {{action "toggleTheThingAndAdvanceToNextStep" bubbles=false}}>
Next Step of the Tour
</a>
</div>
{{/if}}
</div>

After: Clicking on “Next Step of the Tour” opened The Thing and immediately closed it — so fast that it looked like nothing happened.

<div class="cool-new-button" onClick={{action "toggleTheThing"}}>
  {{#if shouldShowThisTooltip}}
<div class="tour-tooltip">
This button can open and close The Thing!

<a {{action "toggleTheThingAndAdvanceToNextStep" bubbles=false}}>
Next Step of the Tour
</a>
</div>
{{/if}}
</div>

The Only Change: <div {{action "toggleTheThing"}}> became<div onClick={{action "toggleTheThing"}}>

Me: um, what?

Gif of Kandi Burruss speaking with caption “So I’m just confused” (source)

So off I went to the debugging races, and with a little bit of work came to the conclusion that despite their visual similarity, {{action "foo"}} and onClick={{action "foo"}} represent completely different ways of listening for clicks.

Using onClick={{action "foo"}} listens for DOM events that the browser sends directly, whereas {{action "foo"}} listens for actions fired by Ember in response to browser events. I could tell these two types of event listeners had subtly different behavior, but couldn’t yet articulate what or why.

I realized that I was just scratching the surface of how events are handled in Ember and there was a lot more to learn.

Gif of Taraji P. Henson writing math equations on a chalkboard in Hidden Figures movie (source)

I spent the next three days staring at my computer screen, muttering on my walks home from work, and dancing in my kitchen when I finally managed to get a comprehensive understanding of how DOM events and Ember actions fit together.

This is what I’ve learned!


Interactive Demo: Events in Ember

If you’re a visual person and want to get your hands on some code to explore these ideas for yourself, I built a handy Ember Twiddle demo that explains three common ways of listening to events in Ember and the way they interact.

If you’d prefer to get a clear overview of everything before you try out the demo, keep reading!

Screenshot of Ember Twiddle Demo. Link to interactive demo.

The Basics

Let’s start with a few definitions to make sure we’re all on the same page.

DOM: Document Object Model, an API that describes how web pages work — how HTML is rendered on a page, what events happen as users interact with that page, and more. Browsers like Chrome, Firefox, and Safari implement this API in order to display websites. (For more info, check out this DOM documentation.)

DOM node: A single element in the DOM. For example, a <div> is a single DOM node. Nodes can have parents and children:

<div id="parent">                  <!-- This is a node. -->
<button id="child"></button> <!-- This is also a node. -->
</div>

DOM event: A standard way of describing that something has happened on a page. This includes things like clicks, keyboard presses, form submissions, and dragging elements around the screen. (You can see the full list of DOM events in this Event reference.)

You can add an event listener to any DOM node that should do something when a particular event happens to it; for example, you might care when a button is clicked or when a user types inside a text field. The listener is a JavaScript function that is called by the browser whenever that event happens on the specified node. The listener is passed an Event object with relevant information when it is called.

By default, browsers call the listener for the target node of the event and then call the event listener of every parent of that node — a process called propagation. This is helpful; say you have a button with an icon and some text inside it. If a user clicks exactly on the icon, you still want the button to receive the click. However, any node in the chain can override this behavior and stop propagation in its listener to prevent its ancestors from firing their listeners— ensuring that nothing else happens as a result of that event.

Ember action: An Ember-specific abstraction on top of DOM events. Ember actions are also functions at their heart, but have access to extra application context and logic (like attributes or functions defined in the associated controller or component). Ember actions are fired as a result of DOM events, but are called by the framework — not the browser — and may or may not have access to the original DOM event that caused them.


Order of Events in Ember

When a user clicks on your fancy new button, how exactly does a function defined in your component get fired? What happens if a previous action calls stopPropagation() on the event, or another action has set bubbles=false?

The basic overview is:

  1. A DOM event is created
  2. All the native DOM event listeners are fired, starting from the target node and walking up the tree — unless one of those stops propagation
  3. All the Ember action listeners are fired, starting from the target node and walking up the tree — unless one of those stops propagation

The full(er) overview is explained in this super rad flowchart I made (the brainchild of previously referenced kitchen dance).

Pro tip: all the colors in this chart match the colors used for different event listeners in the Ember Twiddle demo, so cross-reference at your leisure!

Flowchart titled: “How do events and actions work in Ember?” Link to plaintext outline of chart .

Types of Event Listeners in Ember

How do you know whether you’re using a DOM event listener or an Ember action listener? When will your action have access to the original DOM event? Can your action prevent other actions from being fired?

There are three main ways of adding listeners in Ember:

  1. Adding an Ember action listener to a component with an event name attribute.
{{some-component click=(action “handleClick”)}}

2. Adding an Ember action listener to a DOM node by modifying the element with the action helper.

<div {{action "handleClick"}}></div>
<div {{action "handleDoubleClick" on="doubleClick"}}></div>

3. Adding a DOM event listener to a DOM node by using an event HTML attribute.

<div onclick={{action "handleClick"}}></div>

(Pro tip: this is a great time to check out the Ember Twiddle demo if you haven’t yet! You can see for yourself how different combinations of these events bubble from children to parents, and modify the source code that runs the demo to explore further.)

Screenshot of click Component Attribute in Ember Twiddle demo.

1. Component Event Attributes

Listener Type: Ember action

Access to original DOM event: Yes

Supported events: defined by Ember.Component

Can it stop propagation:
Yes for other Ember actions, because those are fired after this action.*
No for DOM events, because those are fired before this action.

*You cannot pass bubbles=false as part of a closure action hash (like click=(action "foo" bubbles=false)). If you want to stop propagation, you have to call event.stopPropagation() in your handler. See this GitHub issue or the documentation for the action helper for more information.

Code examples for click Attribute actions. Link to examples in GitHub Gist.
Screenshot of Element Modifier in Ember Twiddle demo.

2. Element Modifiers

Listener Type: Ember action

Access to original DOM event: No

Supported events: defined by Ember.Templates.helpers

Can it stop propagation:
Yes for other Ember actions, because those are fired after this action.*
No for DOM events, because those are fired before this action.

*This type of action doesn’t have access to the original DOM event so it cannot call event.stopPropagation(). If you want to stop propagation, you have to set bubbles=false on the action helper.

Code examples for Element Modifier actions. Link to examples in GitHub Gist.
Screenshot of onClick HTML Attribute in Ember Twiddle demo.

3. DOM Event Attributes

Listener Type: DOM Event

Access to original DOM event: Yes

Supported events: defined by DOM API

Can it stop propagation:
Yes for both DOM events and Ember actions, because both of those are fired after this event.* **

*Setting bubbles=false on the action helper doesn’t actually stop propagation for DOM events — that’s an Ember-specific abstraction. If you want to stop propagation, you have to call event.stopPropagation() in your handler.

**This is where it gets extra funky — calling event.stopPropagation() will prevent any Ember actions from firing because of that event — even child actions, which totally goes against normal DOM propagation order but which you can see in action in this Ember Twiddle. Why? It’s because calling stopPropagation at this point prevents the event from bubbling up to the special <div id="root"> node that is the parent node of everything inside an Ember app. That node’s handler is what kicks off the process of firing Ember actions — see the flowchart above if this is still confusing!

Code examples for onClick Attribute actions. Link to examples in GitHub Gist.

Learnings and Recommendations

Gif of Robby Novak (aka Kid President) holding eyes open (source)

All of this may be interesting, but how does it relate to the bug that originally led me down this rabbit hole?

And what does this mean for you when you’re handling events in Ember?

Let’s take another look at that buggy code example:

<div class="cool-new-button" onClick={{action "toggleTheThing"}}>
{{#if shouldShowThisTooltip}}
<div class="tour-tooltip">
This button can open and close The Thing!

<a {{action "toggleTheThingAndAdvanceToNextStep" bubbles=false}}>
Next Step of the Tour
</a>
</div>
{{/if}}
</div>

We’ve got a child node that is using an Ember action listener to toggleTheThingAndAdvanceToTheNextStep. When it handles the Ember event, it stops any other Ember events from firing (thanks to bubbles=false). This seems like it should prevent the parent’s action from being fired.

However — before the child’s action handler is ever called, the DOM event listener (toggleTheThing) of its parent node is called. The child hasn’t had a chance yet to stop propagation. The parent action toggles The Thing open, and the child action later toggles it back shut.

In this example, events would fire in this order (provided no handlers stop propagation):

  1. DOM events on <a>
  2. DOM events on <div class="tour-tooltip">
  3. DOM events on <div class="cool-new-button"> (opens The Thing)
  4. Ember actions on <a> (closes The Thing)
  5. Ember actions on <div class="tour-tooltip">
  6. Ember actions on <div class="cool-new-button">

Takeaways

Gif of fish from Finding Nemo with caption “Now what?” (source)
  • DOM events always fire before Ember actions.
  • Attaching actions directly to DOM event attributes (like onclick) uses the browser’s DOM events API directly.
  • Attaching actions to Ember attributes (like a component’s click attribute or modifying an element with the action helper) uses Ember’s actions API, an abstraction on top of the DOM events API.
  • You can use bubbles=false to stop an event from propagating only if you are using the action template helper in regular form not in closure form (e.g. {{action "foo" bubbles=false}} works but click=(action "foo" bubbles=false) does not).
  • You can use event.stopPropagation() to stop an event from propagating only if you are using a handler that has access to the original DOM event — either by using an HTML event attribute like onclick or by using a component event attribute like click.
  • Calling event.stopPropagation() on a DOM event handler will stop any Ember actions from firing because of that event — risky business!
  • For consistency and to prevent subtle bugs, I recommend always using Ember actions over DOM events.

One possible exception to always using Ember actions: if you want to optionally add an event to a DOM node. Currently, Ember template helpers do not support modifying elements with an inline conditional:

<div {{if shouldRespondToClick (action "handleClick")}}></div>

Since that’s not valid Handlebars, you have to do this instead:

<div onClick={{if shouldRespondToClick (action "handleClick")}}</div>

Alternatively, you can perform this logic check in your action handler and return early: if (!this.get('shouldRespondToClick')) { return; }. However, your element will have styling associated with click-ability, so you may also need to optionally add a style that sets cursor: default.

That’s a lot of work just to keep events more consistent; it’s up to you to decide which approach you prefer.

Gif of Michelle Obama and Missy Elliott dancing in a car (source)

The biggest takeaway though: now you know how events in Ember work! And you can make informed decisions and debug with confidence.

Commence kitchen dancing, or in the words of my personal favorite Slack emoji, :corgi: on.


P.S. Do you love nerding out about software and learning about technology? You should join us at Square so we can learn and build great products together!

Show your support

Clapping shows how much you appreciated Marie Chatfield’s story.