The {{on}} modifier (3.11)

The newest (and last) way to handle events in Ember!

Summary

The {{on}} modifier is the single new way to attach event handlers in Ember.

Finally, we have a uniform interface that works on both elements and components! No more {{action}}, onclick={{action}}, did I forget an extra {{action (action this.toggle)}} again...?

Nope. {{on}} is the only game in town from here on out.

Let's see how it works.

As we learned in the last lesson, the new way to define actions is at the source, in our component's JavaScript files.

Let's say we have a <Toggle /> component that looks like this:

import Component from "@ember/component";
import { action } from "@ember/object";

export default Component.extend({
  tagName: "",

  isChecked: false,

  toggle: action(function () {
    this.toggleProperty("isChecked");
  }),

  maybeToggle: action(function (event) {
    if (event.which === 32) {
      this.toggle();
    }
  }),
});

and our template wires up these events using the onclick and onkeydown attributes:

<span
  role='checkbox'
  tabindex='0'
  aria-checked={{if this.isChecked 'true' 'false'}}
  onclick={{this.toggle}}
  onkeydown={{this.maybeToggle}}
>
  {{! additional HTML }}
</span>

This works, but this attribute API is somewhat inconsistent across DOM elements and event names. Also, it's unclear how this component will be serialized if we want to server-render it using FastBoot.

Enter {{on}}. Here's what it looks like:

  <span
    role='checkbox'
    tabindex='0'
    aria-checked={{if this.isChecked 'true' 'false'}}
-   onclick={{this.toggle}}
+   {{on 'click' this.toggle}}
-   onkeydown={{this.maybeToggle}}
+   {{on 'keydown' this.toggle}}
  >
    {{! additional HTML }}
  </span>

First, {{on}} is a modifier, meaning it runs in element space of any HTML element. Second, it accepts two arguments: the eventName, and a function handler.

The eventName being an argument on its own is really nice, since it's the same argument that the vanilla JS API element.addEventListener(eventName) accepts:

// plain JS
element.addEventListener("click", handler); // "click" here is the eventName

And the handler is any action, meaning any properly bound function. It can either live on the component instance or be passed in as an argument.

{{on 'click' this.toggle}}
{{on 'click' @handleClick}}

Finally, {{on}} can be used not only on HTML elements, but also on Ember components!

Let's say our parent template uses our <Toggle /> component like this:

<p>
  Admin:
  <Toggle
    @checked={{this.isAdmin}}
    @onChange={{this.toggleIsAdmin}}
  />
</p>

What if we wanted to add more event listeners to <Toggle />? Well we can, using {{on}}:

<p>
  Admin:
  <Toggle
    @checked={{this.isAdmin}}
    @onChange={{this.toggleIsAdmin}}
    {{on 'mouseenter' this.showTip}}
    {{on 'mouseleave' this.hideTip}}
  />
</p>

All we need to do is use ...attributes to forward these modifiers to an element of our choosing within Toglge's template:

<span
  role='checkbox'
  tabindex='0'
  aria-checked={{if this.isChecked 'true' 'false'}}
  ...attributes
  {{on 'click' this.toggle}}
  {{on 'keydown' this.toggle}}
>
  {{! additional HTML }}
</span>

And now these event listeners will be forwarded to the root <span> tag in our component.

Hopefully you can see how much simpler and consistent this approach is, compared to the previous few iterations of the action helper and modifier!

Questions?

us, or ask in #media on Discord