Awesome
ember-on-helper
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 EventTarget
s 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 sharedService
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.