From 056483f8952d52f5232f82b20149100037344cb3 Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Thu, 4 Feb 2021 23:39:58 -0800 Subject: [PATCH] [FEATURE modernized-built-in-components] Get events from dispatcher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of hard-coding a list of events to bind (copied from the `EventDispatcher` – which could be reopened and modified), this refactor uses a modifier to dynamically read the list from the `EventDispatcher` at runtime instead. Part of #19270 --- .../glimmer/lib/components/input.ts | 390 +++++++++--------- .../glimmer/lib/templates/input.hbs | 26 +- 2 files changed, 207 insertions(+), 209 deletions(-) diff --git a/packages/@ember/-internals/glimmer/lib/components/input.ts b/packages/@ember/-internals/glimmer/lib/components/input.ts index f51eb6680fe..090249bfefb 100644 --- a/packages/@ember/-internals/glimmer/lib/components/input.ts +++ b/packages/@ember/-internals/glimmer/lib/components/input.ts @@ -3,20 +3,23 @@ */ import { hasDOM } from '@ember/-internals/browser-environment'; import { tracked } from '@ember/-internals/metal'; +import { Owner } from '@ember/-internals/owner'; import { guidFor } from '@ember/-internals/utils'; import { jQuery, jQueryDisabled } from '@ember/-internals/views'; import { EMBER_MODERNIZED_BUILT_IN_COMPONENTS } from '@ember/canary-features'; import { assert, deprecate, warn } from '@ember/debug'; -import { - JQUERY_INTEGRATION, - MOUSE_ENTER_LEAVE_MOVE_EVENTS, - SEND_ACTION, -} from '@ember/deprecated-features'; +import { JQUERY_INTEGRATION, SEND_ACTION } from '@ember/deprecated-features'; import { action } from '@ember/object'; -import { setComponentTemplate, setInternalComponentManager } from '@glimmer/manager'; +import { Maybe, Option } from '@glimmer/interfaces'; +import { + setComponentTemplate, + setInternalComponentManager, + setInternalModifierManager, +} from '@glimmer/manager'; import { isConstRef, isUpdatableRef, Reference, updateRef, valueForRef } from '@glimmer/reference'; import { untrack } from '@glimmer/validator'; import InternalManager from '../component-managers/internal'; +import InternalModifier, { InternalModifierManager } from '../modifiers/internal'; import InputTemplate from '../templates/input'; import InternalComponent from './internal'; @@ -52,10 +55,30 @@ if (hasDOM && EMBER_MODERNIZED_BUILT_IN_COMPONENTS) { isValidInputType = (type: string) => type !== ''; } +type EventListener = (event: Event) => void; +type VirtualEventListener = (value: string, event: Event) => void; + function NOOP() {} const UNINITIALIZED: unknown = Object.freeze({}); +function elementForEvent(event: Event): HTMLInputElement { + assert( + '[BUG] Event target must be the element', + event.target instanceof HTMLInputElement + ); + + return event.target; +} + +function valueForEvent(event: Event): string { + return elementForEvent(event).value; +} + +function devirtualize(callback: VirtualEventListener): EventListener { + return (event: Event) => callback(valueForEvent(event), event); +} + /** * This interface paves over the differences between these three cases: * @@ -386,11 +409,11 @@ class Input extends InternalComponent { } @action checkedDidChange(event: Event): void { - this.checked = this.elementFor(event).checked; + this.checked = elementForEvent(event).checked; } @action valueDidChange(event: Event): void { - this.value = this.valueFor(event); + this.value = valueForEvent(event); } @action change(event: Event): void { @@ -408,34 +431,19 @@ class Input extends InternalComponent { } @action keyUp(event: KeyboardEvent): void { - let value = this.valueFor(event); - switch (event.key) { case 'Enter': - this.callbackFor('enter')(value, event); - this.callbackFor('insert-newline')(value, event); + this.callbackFor('enter')(event); + this.callbackFor('insert-newline')(event); break; case 'Escape': - this.callbackFor('escape-press')(value, event); + this.callbackFor('escape-press')(event); break; } } - private elementFor(event: Event): HTMLInputElement { - assert( - '[BUG] Event target must be the element', - event.target instanceof HTMLInputElement - ); - - return event.target; - } - - private valueFor(event: Event): string { - return this.elementFor(event).value; - } - - private callbackFor(type: string): (value: string, event: Event) => void { + private callbackFor(type: string, shouldDevirtualize = true): EventListener { let callback = this.arg(type); if (callback) { @@ -443,7 +451,12 @@ class Input extends InternalComponent { `The \`@${type}\` argument to the component must be a function`, typeof callback === 'function' ); - return callback as (value: string, event: Event) => void; + + if (shouldDevirtualize) { + return devirtualize(callback as VirtualEventListener); + } else { + return callback as EventListener; + } } else { return NOOP; } @@ -570,161 +583,145 @@ if (EMBER_MODERNIZED_BUILT_IN_COMPONENTS) { }); } - // Event callbacks - { - let defineGetterForDeprecatedEventCallback = ( - eventName: string, - methodName: string = eventName, - virtualEvent?: string - ): void => { + type EventsMap = Record; + + const EVENTS = new WeakMap(); + + const getEventsMap = (owner: Owner): EventsMap => { + let events = EVENTS.get(owner); + + if (events === undefined) { + let eventDispatcher = owner.lookup>( + 'event_dispatcher:main' + ); + assert( - `[BUG] There is already a getter for _${methodName} on Input`, - !Object.getOwnPropertyDescriptor(Input.prototype, `_${methodName}`) + 'missing event dispatcher', + eventDispatcher !== null && typeof eventDispatcher === 'object' ); - let descriptor = Object.getOwnPropertyDescriptor(Input.prototype, methodName); + assert( + 'missing _finalEvents on event dispatcher', + '_finalEvents' in eventDispatcher && + eventDispatcher?._finalEvents !== null && + typeof eventDispatcher?._finalEvents === 'object' + ); - Object.defineProperty(Input.prototype, `_${methodName}`, { - get(this: Input): unknown { - return (event: Event): void => { - let value = this['valueFor'].call(this, event); - - if (methodName in this.args) { - deprecate( - `Passing the \`@${methodName}\` argument to is deprecated. ` + - `This would have overwritten the internal \`${methodName}\` method on ` + - `the component and prevented it from functioning properly. ` + - `Instead, please use the {{on}} modifier, i.e. \`\` ` + - `instead of \`\` or \`{{input ${methodName}=...}}\`.`, - true /* TODO !descriptor */, - { - id: 'ember.built-in-components.legacy-attribute-arguments', - for: 'ember-source', - since: {}, - until: '4.0.0', - } - ); - - deprecate( - `Passing the \`@${methodName}\` argument to is deprecated. ` + - `Instead, please use the {{on}} modifier, i.e. \`\` ` + - `instead of \`\` or \`{{input ${methodName}=...}}\`.`, - true /* TODO descriptor */, - { - id: 'ember.built-in-components.legacy-attribute-arguments', - for: 'ember-source', - since: {}, - until: '4.0.0', - } - ); - - let callback = this['callbackFor'].call(this, methodName); - callback(value, event); - } else if (virtualEvent && virtualEvent in this.args) { - deprecate( - `Passing the \`@${virtualEvent}\` argument to is deprecated. ` + - `Instead, please use the {{on}} modifier, i.e. \`\` ` + - `instead of \`\` or \`{{input ${virtualEvent}=...}}\`.`, - true /* TODO false */, - { - id: 'ember.built-in-components.legacy-attribute-arguments', - for: 'ember-source', - since: {}, - until: '4.0.0', - } - ); - - this['callbackFor'].call(this, virtualEvent)(value, event); - } - }; - }, - }); + EVENTS.set(owner, (events = eventDispatcher._finalEvents)); + } - if (descriptor) { - const superGetter = descriptor.get; + return events; + }; - assert( - `[BUG] Expecting ${methodName} on Input to be a getter`, - typeof superGetter === 'function' - ); + class DeprecatedInputEventHandlersModifier extends InternalModifier { + static toString(): string { + return '@ember/component/input/deprecated-events-handler'; + } - Object.defineProperty(Input.prototype, methodName, { - get(this: Input): unknown { - if (methodName in this.args) { - return this[`_${methodName}`]; - } else if (virtualEvent && virtualEvent in this.args) { - let superCallback = superGetter.call(this); - let virtualCallback = this[`_${methodName}`]; - - return (event: Event) => { - superCallback(event); - virtualCallback(event); - }; - } else { - return superGetter.call(this); - } - }, - }); + private listeners = new Map(); + + install(): void { + let { element, eventsMap, input, listenerFor, listeners } = this; + + let entries: [eventName: string, methodName: string, isVirtualEvent?: boolean][] = [ + ...Object.entries(eventsMap), + ['focusin', 'focus-in', true], + ['focusout', 'focus-out', true], + ['keypress', 'key-press', true], + ['keyup', 'key-up', true], + ['keydown', 'key-down', true], + ]; + + for (let [eventName, methodName, isVirtualEvent] of entries) { + let listener = listenerFor.call(input, eventName, methodName, isVirtualEvent); + + if (listener) { + listeners.set(eventName, listener); + element.addEventListener(eventName, listener); + } } - }; - let deprecatedEventCallbacks: Array< - string | Parameters - > = [ - // EventDispatcher - ['touchstart', 'touchStart'], - ['touchmove', 'touchMove'], - ['touchend', 'touchEnd'], - ['touchcancel', 'touchCancel'], - ['keydown', 'keyDown', 'key-down'], - ['keyup', 'keyUp', 'key-up'], - ['keypress', 'keyPress', 'key-press'], - ['mousedown', 'mouseDown'], - ['mouseup', 'mouseUp'], - ['contextmenu', 'contextMenu'], - 'click', - ['dblclick', 'doubleClick'], - ['focusin', 'focusIn', 'focus-in'], - ['focusout', 'focusOut', 'focus-out'], - 'submit', - 'input', - 'change', - ['dragstart', 'dragStart'], - 'drag', - ['dragenter', 'dragEnter'], - ['dragleave', 'dragLeave'], - ['dragover', 'dragOver'], - 'drop', - ['dragend', 'dragEnd'], - ]; + Object.freeze(listeners); + } - if (MOUSE_ENTER_LEAVE_MOVE_EVENTS) { - deprecatedEventCallbacks.push( - ['mouseenter', 'mouseEnter'], - ['mouseleave', 'mouseLeave'], - ['mousemove', 'mouseMove'] - ); - } else { - Object.assign(Input.prototype, { - _mouseEnter: NOOP, - _mouseLeave: NOOP, - _mouseMove: NOOP, - }); + remove(): void { + let { element, listeners } = this; + + for (let [event, listener] of Object.entries(listeners)) { + element.removeEventListener(event, listener); + } + + this.listeners = new Map(); } - deprecatedEventCallbacks.forEach((args) => { - if (Array.isArray(args)) { - defineGetterForDeprecatedEventCallback(...args); + private get eventsMap(): EventsMap { + return getEventsMap(this.owner); + } + + private get input(): Input { + let input = this.positional(0); + assert('must pass the component as first argument', input instanceof Input); + return input; + } + + private listenerFor( + this: Input, + eventName: string, + methodName: string, + isVirtualEvent = false + ): Option { + assert('must be called with the component as this', this instanceof Input); + + if (methodName in this.args) { + deprecate( + `Passing the \`@${methodName}\` argument to is deprecated. ` + + `This would have overwritten the internal \`${methodName}\` method on ` + + `the component and prevented it from functioning properly. ` + + `Instead, please use the {{on}} modifier, i.e. \`\` ` + + `instead of \`\` or \`{{input ${methodName}=...}}\`.`, + true, // !(methodName in this), + { + id: 'ember.built-in-components.legacy-attribute-arguments', + for: 'ember-source', + since: {}, + until: '4.0.0', + } + ); + + deprecate( + `Passing the \`@${methodName}\` argument to is deprecated. ` + + `Instead, please use the {{on}} modifier, i.e. \`\` ` + + `instead of \`\` or \`{{input ${methodName}=...}}\`.`, + true, // methodName in this, + { + id: 'ember.built-in-components.legacy-attribute-arguments', + for: 'ember-source', + since: {}, + until: '4.0.0', + } + ); + + return this['callbackFor'].call(this, methodName, isVirtualEvent); } else { - defineGetterForDeprecatedEventCallback(args); + return null; } - }); + } } + setInternalModifierManager( + new InternalModifierManager( + DeprecatedInputEventHandlersModifier, + 'deprecated-input-event-handlers' + ), + DeprecatedInputEventHandlersModifier + ); + + Input.prototype['DeprecatedInputEventHandlersModifier'] = DeprecatedInputEventHandlersModifier; + // String actions if (SEND_ACTION) { interface View { - send(action: string, value: string, event: Event): void; + send(action: string, ...args: unknown[]): void; } let isView = (target: {}): target is View => { @@ -734,36 +731,63 @@ if (EMBER_MODERNIZED_BUILT_IN_COMPONENTS) { let superCallbackFor = Input.prototype['callbackFor']; Object.assign(Input.prototype, { - callbackFor(this: Input, type: string): (value: string, event: Event) => void { + callbackFor(this: Input, type: string, shouldDevirtualize = true): EventListener { const actionName = this.arg(type); if (typeof actionName === 'string') { - deprecate( - `Passing actions to components as strings (like \`\`) is deprecated. ` + - `Please use closure actions instead (\`\`).`, - false, - { - id: 'ember-component.send-action', - for: 'ember-source', - since: {}, - until: '4.0.0', - url: 'https://emberjs.com/deprecations/v3.x#toc_ember-component-send-action', - } - ); + // TODO: eagerly issue a deprecation for this as well (need to fix tests) + // + // deprecate( + // `Passing actions to components as strings (like \`\`) is deprecated. ` + + // `Please use closure actions instead (\`\`).`, + // false, + // { + // id: 'ember-component.send-action', + // for: 'ember-source', + // since: {}, + // until: '4.0.0', + // url: 'https://emberjs.com/deprecations/v3.x#toc_ember-component-send-action', + // } + // ); const { caller } = this; assert('[BUG] Missing caller', caller && typeof caller === 'object'); + let callback: Function; + if (isView(caller)) { - return (value: string, event: Event) => caller.send(actionName, value, event); + callback = (...args: unknown[]) => caller.send(actionName, ...args); } else { assert( `The action '${actionName}' did not exist on ${caller}`, typeof caller[actionName] === 'function' ); - return caller[actionName]; + callback = caller[actionName]; + } + + let deprecated = (...args: unknown[]) => { + deprecate( + `Passing actions to components as strings (like \`\`) is deprecated. ` + + `Please use closure actions instead (\`\`).`, + false, + { + id: 'ember-component.send-action', + for: 'ember-source', + since: {}, + until: '4.0.0', + url: 'https://emberjs.com/deprecations/v3.x#toc_ember-component-send-action', + } + ); + + return callback(...args); + }; + + if (shouldDevirtualize) { + return devirtualize(deprecated as VirtualEventListener); + } else { + return deprecated as EventListener; } } else { return superCallbackFor.call(this, type); @@ -777,13 +801,11 @@ if (EMBER_MODERNIZED_BUILT_IN_COMPONENTS) { let superCallbackFor = Input.prototype['callbackFor']; Object.assign(Input.prototype, { - callbackFor(this: Input, type: string): (value: string, event: Event) => void { - let callback = superCallbackFor.call(this, type); + callbackFor(this: Input, type: string, shouldDevirtualize = true): EventListener { + let callback = superCallbackFor.call(this, type, shouldDevirtualize); if (jQuery && !jQueryDisabled) { - return (value: string, event: Event) => { - callback(value, new jQuery.Event(event)); - }; + return (event: Event) => callback(new jQuery.Event(event)); } else { return callback; } diff --git a/packages/@ember/-internals/glimmer/lib/templates/input.hbs b/packages/@ember/-internals/glimmer/lib/templates/input.hbs index 348633c84b3..08e7ef864df 100644 --- a/packages/@ember/-internals/glimmer/lib/templates/input.hbs +++ b/packages/@ember/-internals/glimmer/lib/templates/input.hbs @@ -54,31 +54,7 @@ {{on "paste" this.valueDidChange}} {{on "cut" this.valueDidChange}} - {{!-- deprecated native event callbacks --}} - {{on "touchstart" this._touchStart}} - {{on "touchmove" this._touchMove}} - {{on "touchend" this._touchEnd}} - {{on "touchcancel" this._touchCancel}} - {{on "keydown" this._keyDown}} - {{on "keypress" this._keyPress}} - {{on "mousedown" this._mouseDown}} - {{on "mouseup" this._mouseUp}} - {{on "contextmenu" this._contextMenu}} - {{on "click" this._click}} - {{on "dblclick" this._doubleClick}} - {{on "focusin" this._focusIn}} - {{on "focusout" this._focusOut}} - {{on "submit" this._submit}} - {{on "dragstart" this._dragStart}} - {{on "drag" this._drag}} - {{on "dragenter" this._dragEnter}} - {{on "dragleave" this._dragLeave}} - {{on "dragover" this._dragOver}} - {{on "drop" this._drop}} - {{on "dragend" this._dragEnd}} - {{on "mouseenter" this._mouseEnter}} - {{on "mouseleave" this._mouseLeave}} - {{on "mousemove" this._mouseMove}} + {{this.DeprecatedInputEventHandlersModifier this}} /> {{~else~}} {{~#let (component '-checkbox') (component '-text-field') as |Checkbox TextField|~}}