ember-source
Version:
A JavaScript framework for creating ambitious web applications
254 lines (227 loc) • 8.49 kB
JavaScript
import { g as buildUntouchableThis, h as check, e as reifyNamed } from './untouchable-this-B3DVwpHS.js';
import { registerDestructor } from '../@glimmer/destroyable/index.js';
import { k as setInternalModifierManager } from './api-BawZUDYD.js';
import { v as valueForRef } from './reference-BoPB2LfI.js';
import { n as createUpdatableTag } from './cache-B7dqAS38.js';
const untouchableContext = buildUntouchableThis('`on` modifier');
class OnModifierState {
tag = createUpdatableTag();
element;
args;
listener = null;
constructor(element, args) {
this.element = element;
this.args = args;
registerDestructor(this, () => {
let {
element,
listener
} = this;
if (listener) {
let {
eventName,
callback,
options
} = listener;
removeEventListener(element, eventName, callback, options);
}
});
}
// Update this.listener if needed
updateListener() {
let {
element,
args,
listener
} = this;
let selector;
{
const el = this.element;
selector = el.tagName.toLowerCase() + (el.id ? `#${el.id}` : '') + Array.from(el.classList).map(c => `.${c}`).join('');
}
let arg0 = args.positional[0];
let eventName = check(arg0 ? valueForRef(arg0) : undefined);
if (!eventName) {
throw new Error(`You must pass a valid DOM event name as the first argument to the \`on\` modifier on ${selector}`);
}
let arg1 = args.positional[1];
let userProvidedCallback = check(arg1 ? valueForRef(arg1) : undefined);
if (typeof userProvidedCallback !== 'function') {
throw new Error(`You must pass a function as the second argument to the \`on\` modifier; you passed ${userProvidedCallback === null ? 'null' : typeof userProvidedCallback}. While rendering:\n\n${args.positional[1]?.debugLabel ?? '(unknown)'} on ${selector}`);
}
if (args.positional.length !== 2) {
throw new Error(`You can only pass two positional arguments (event name and callback) to the \`on\` modifier, but you provided ${args.positional.length}. Consider using the \`fn\` helper to provide additional arguments to the \`on\` callback on ${selector}`);
}
let once = undefined;
let passive = undefined;
let capture = undefined;
{
let {
once: _once,
passive: _passive,
capture: _capture,
...extra
} = reifyNamed(args.named);
once = check(_once);
passive = check(_passive);
capture = check(_capture);
if (Object.keys(extra).length > 0) {
throw new Error(`You can only \`once\`, \`passive\` or \`capture\` named arguments to the \`on\` modifier, but you provided ${Object.keys(extra).join(', ')} on ${selector}`);
}
}
let shouldUpdate = false;
if (listener === null) {
shouldUpdate = true;
} else {
shouldUpdate = eventName !== listener.eventName || userProvidedCallback !== listener.userProvidedCallback || once !== listener.once || passive !== listener.passive || capture !== listener.capture;
}
let options = undefined;
// we want to handle both `true` and `false` because both have a meaning:
// https://bugs.chromium.org/p/chromium/issues/detail?id=770208
if (shouldUpdate) {
if (once !== undefined || passive !== undefined || capture !== undefined) {
options = {
once,
passive,
capture
};
}
}
if (shouldUpdate) {
let callback = userProvidedCallback;
{
callback = userProvidedCallback.bind(untouchableContext);
if (passive) {
let _callback = callback;
callback = event => {
event.preventDefault = () => {
throw new Error(`You marked this listener as 'passive', meaning that you must not call 'event.preventDefault()': \n\n${userProvidedCallback.name || `{anonymous function}`}`);
};
return _callback(event);
};
}
}
this.listener = {
eventName,
callback,
userProvidedCallback,
once,
passive,
capture,
options
};
if (listener) {
removeEventListener(element, listener.eventName, listener.callback, listener.options);
}
addEventListener(element, eventName, callback, options);
}
}
}
let adds = 0;
let removes = 0;
function removeEventListener(element, eventName, callback, options) {
removes++;
element.removeEventListener(eventName, callback, options);
}
function addEventListener(element, eventName, callback, options) {
adds++;
element.addEventListener(eventName, callback, options);
}
/**
The `{{on}}` modifier lets you easily add event listeners (it uses
[EventTarget.addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)
internally).
For example, if you'd like to run a function on your component when a `<button>`
in the components template is clicked you might do something like:
```app/components/like-post.gjs
import Component from '@glimmer/component';
import { action } from '@ember/object';
export default class LikePostComponent extends Component {
saveLike = () => {
// someone likes your post!
// better send a request off to your server...
}
<template>
<button {{on 'click' this.saveLike}}>Like this post!</button>
</template>
}
```
### Arguments
`{{on}}` accepts two positional arguments, and a few named arguments.
The positional arguments are:
- `event` -- the name to use when calling `addEventListener`
- `callback` -- the function to be passed to `addEventListener`
The named arguments are:
- capture -- a `true` value indicates that events of this type will be dispatched
to the registered listener before being dispatched to any EventTarget beneath it
in the DOM tree.
- once -- indicates that the listener should be invoked at most once after being
added. If true, the listener would be automatically removed when invoked.
- passive -- if `true`, indicates that the function specified by listener will never
call preventDefault(). If a passive listener does call preventDefault(), the user
agent will do nothing other than generate a console warning. See
[Improving scrolling performance with passive listeners](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Improving_scrolling_performance_with_passive_listeners)
to learn more.
The callback function passed to `{{on}}` will receive any arguments that are passed
to the event handler. Most commonly this would be the `event` itself.
If you would like to pass additional arguments to the function you should use
the `{{fn}}` helper.
For example, in our example case above if you'd like to pass in the post that
was being liked when the button is clicked you could do something like:
```app/components/like-post.hbs
<button {{on 'click' (fn this.saveLike @post)}}>Like this post!</button>
```
In this case, the `saveLike` function will receive two arguments: the click event
and the value of `@post`.
### Function Context
In the example above, we used an arrow function to ensure that `likePost` is
properly bound to the `items-list`, but let's explore what happens if we
left out the arrow function:
```app/components/like-post.gjs
import Component from '@glimmer/component';
export default class LikePostComponent extends Component {
saveLike() {
// ...snip...
}
}
```
In this example, when the button is clicked `saveLike` will be invoked,
it will **not** have access to the component instance. In other
words, it will have no `this` context, so please make sure your functions
are bound (via an arrow function or other means) before passing into `on`!
@method on
@public
*/
class OnModifierManager {
getDebugName() {
return 'on';
}
getDebugInstance() {
return null;
}
get counters() {
return {
adds,
removes
};
}
create(_owner, element, _state, args) {
return new OnModifierState(element, args);
}
getTag({
tag
}) {
return tag;
}
install(state) {
state.updateListener();
}
update(state) {
state.updateListener();
}
getDestroyable(state) {
return state;
}
}
const on = setInternalModifierManager(new OnModifierManager(), {});
export { on as o };