UNPKG

ember-source

Version:

A JavaScript framework for creating ambitious web applications

254 lines (227 loc) 8.49 kB
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 };