/ember-on-helper

Complimentary `{{on}}` template helper for listening to events on `document`, `window` or other event targets

Primary LanguageJavaScriptMIT LicenseMIT

ember-on-helper

Build Status npm version Download Total Ember Observer Score Ember Versions ember-cli Versions code style: prettier dependencies devDependencies

An {{on}} template helper complimentary to the RFC #471 "{{on}} modifier".

Installation

ember install ember-on-helper

Compatibility

  • Ember.js v2.18 or above
  • ember-cli v2.13 or above

But why?

You would use the {{on}} modifier to register event listeners on elements that are in the realm of your current template. But sometimes you need to register event listeners on elements or even on generic EventTargets that are outside of the control of your template, e.g. document or window.

⚠️👉 WARNING: Do not overuse this helper. If you want to bind to an element that is controlled by Glimmer, but maybe just not by the current template, do not reach for a manual document.querySelector(). Instead, think about your current template and state setup and try to use a true "Data Down, Actions Up" pattern or use a shared Service as a message bus.

Usage

Pretty much exactly the same as the {{on}} modifier, except for that the {{on}} helper expects one more positional parameter upfront: the evenTarget.

{{on eventTarget eventName eventListener}}

As with the {{on}} modifier, you can also pass optional event options as named parameters:

{{on eventTarget eventName eventListener capture=bool once=bool passive=bool}}

Simple Example

Click anywhere in the browser window, fam.

{{on this.document "click" this.onDocumentClick}}
import Component from '@glimmer/component';
import { action } from '@ember/object';

export default class TomstersWitnessComponent extends Component {
  document = document;

  @action
  onDocumentClick(event: MouseEvent) {
    console.log(
      'Do you have a minute to talk about our Lord and Savior, Ember.js?'
    );
  }
}

This is essentially equivalent to:

didInsertElement() {
  super.didInsertElement();

  document.addEventListener('click', this.onDocumentClick);
}

In addition to the above {{on}} will properly tear down the event listener, when the helper is removed from the DOM. It will also re-register the event listener, if any of the passed parameters change.

The @action decorator is used to bind the onDocumentClick method to the component instance. This is not strictly required here, since we do not access this, but in order to not break with established patterns, we do it anyway.

Listening to Events on window or document

You will often want to use the {{on}} helper to listen to events which are emitted on window or document. Because providing access to these globals in the template as shown in Simple Example is quite cumbersome, {{on}} brings two friends to the party:

  • {{on-document eventName eventListener}}
  • {{on-window eventName eventListener}}

They work exactly the same way as {{on}} and also accept event options.

Listening to Multiple Events

You can use the {{on}} helper multiple times in the same template and for the same event target, even for the same event.

{{on this.someElement "click" this.onClick}}
{{on this.someElement "click" this.anotherOnClick}}
{{on this.someElement "mousemove" this.onMouseMove}}

Event Options

All named parameters will be passed through to addEventListener as the third parameter, the options hash.

{{on-document "scroll" this.onScroll passive=true}}

This is essentially equivalent to:

didInsertElement() {
  super.didInsertElement();

  document.addEventListener('scroll', this.onScroll, { passive: true });
}

once

To fire an event listener only once, you can pass the once option:

{{on-window "click" this.clickOnlyTheFirstTime once=true}}
{{on-window "click" this.clickEveryTime}}

clickOnlyTheFirstTime will only be fired the first time the page is clicked. clickEveryTime is fired every time the page is clicked, including the first time.

capture

To listen for an event during the capture phase already, use the capture option:

{{on-document "click" this.triggeredFirst capture=true}}

<button {{on "click" this.triggeredLast}}>
  Click me baby, one more time!
</button>

passive

If true, you promise to not call event.preventDefault(). This allows the browser to optimize the processing of this event and not block the UI thread. This prevent scroll jank.

If you still call event.preventDefault(), an assertion will be raised.

{{on-document "scroll" this.trackScrollPosition passive=true}}>

Internet Explorer 11 Support

Internet Explorer 11 has a buggy and incomplete implementation of addEventListener: It does not accept an options parameter and sometimes even throws a cryptic error when passing options.

This is why this addon ships a tiny ponyfill for addEventLisener that is used internally to emulate the once, capture and passive option. This means that all currently known options are polyfilled, so that you can rely on them in your logic.

Currying / Partial Application

If you want to curry the function call / partially apply arguments, you can do so using the {{fn}} helper:

{{#each this.videos as |video|}}
  {{on video.element "play" (fn this.onPlay video)}}
  {{on video.element "pause" (fn this.onPause video)}}
{{/each}}
import Component from '@ember/component';
import { action } from '@ember-decorators/object';

interface Video {
  element: HTMLVideoElement;
  title: string;
}

export default class UserListComponent extends Component {
  videos: Video[];

  @action
  onPlay(video: Video, event: MouseEvent) {
    console.log(`Started playing '${video.title}'.`);
  }

  @action
  onPlay(video: Video, event: MouseEvent) {
    console.log(`Paused '${video.title}'.`);
  }
}

preventDefault / stopPropagation / stopImmediatePropagation

The old {{action}} modifier used to allow easily calling event.preventDefault() like so:

<a href="/" {{action this.someAction preventDefault=true}}>Click me</a>

You also could easily call event.stopPropagation() to avoid bubbling like so:

<a href="/" {{action this.someAction bubbles=false}}>Click me</a>

You can still do this using ember-event-helpers:

<a href="/" {{on "click" (prevent-default this.someAction)}}>Click me</a>
<a href="/" {{on "click" (stop-propagation this.someAction)}}>Click me</a>

Attribution

This addon is a straight copy of ember-on-modifier, the polyfill for the {{on}} modifier.