diff --git a/.gitignore b/.gitignore index 4a4c99b4f3e..06c00d62e84 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,4 @@ npm-debug.log # couple of the files. Once it is, we can switch this over to just ignoring # `types/stable` entirely. types/stable +.puppeteer-cache/ diff --git a/package.json b/package.json index b01ee55a34f..6d0acd143dc 100644 --- a/package.json +++ b/package.json @@ -211,6 +211,7 @@ "@ember/-internals/views/lib/mixins/action_support.js": "ember-source/@ember/-internals/views/lib/mixins/action_support.js", "@ember/-internals/views/lib/system/event_dispatcher.js": "ember-source/@ember/-internals/views/lib/system/event_dispatcher.js", "@ember/-internals/views/lib/system/utils.js": "ember-source/@ember/-internals/views/lib/system/utils.js", + "@ember/-internals/views/lib/views/core-view-utils.js": "ember-source/@ember/-internals/views/lib/views/core-view-utils.js", "@ember/-internals/views/lib/views/core_view.js": "ember-source/@ember/-internals/views/lib/views/core_view.js", "@ember/-internals/views/lib/views/states.js": "ember-source/@ember/-internals/views/lib/views/states.js", "@ember/application/index.js": "ember-source/@ember/application/index.js", diff --git a/packages/@ember/-internals/deprecations/index.ts b/packages/@ember/-internals/deprecations/index.ts index 89ad88ed625..ac5395192c5 100644 --- a/packages/@ember/-internals/deprecations/index.ts +++ b/packages/@ember/-internals/deprecations/index.ts @@ -20,7 +20,7 @@ export function isRemoved(options: DeprecationOptions) { return emberVersionGte(options.until); } -interface DeprecationObject { +export interface DeprecationObject { options: DeprecationOptions; test: boolean; isEnabled: boolean; @@ -122,6 +122,13 @@ export const DEPRECATIONS = { until: '7.0.0', url: 'https://deprecations.emberjs.com/id/using-amd-bundles', }), + DEPRECATE_EVENTED: deprecation({ + id: 'ember-evented', + for: 'ember-source', + since: { available: '6.12.0' }, + until: '8.0.0', + url: 'https://deprecations.emberjs.com/id/ember-evented', + }), }; export function deprecateUntil(message: string, deprecation: DeprecationObject) { diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts b/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts index 0b0da6ec97d..7021b4c8d3d 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts @@ -5,7 +5,12 @@ import { setOwner, } from '@ember/-internals/owner'; import { enumerableSymbol, guidFor } from '@ember/-internals/utils'; -import { addChildView, setElementView, setViewElement } from '@ember/-internals/views'; +import { + addChildView, + sendCoreViewEvent, + setElementView, + setViewElement, +} from '@ember/-internals/views'; import type { Nullable } from '@ember/-internals/utility-types'; import { assert, debugFreeze } from '@ember/debug'; import { _instrumentStart } from '@ember/instrumentation'; @@ -305,20 +310,20 @@ export default class CurlyComponentManager addChildView(parentView, component); } - component.trigger('didReceiveAttrs'); + sendCoreViewEvent(component, 'didReceiveAttrs'); let hasWrappedElement = component.tagName !== ''; // We usually do this in the `didCreateElement`, but that hook doesn't fire for tagless components if (!hasWrappedElement) { if (isInteractive) { - component.trigger('willRender'); + sendCoreViewEvent(component, 'willRender'); } component._transitionTo('hasElement'); if (isInteractive) { - component.trigger('willInsertElement'); + sendCoreViewEvent(component, 'willInsertElement'); } } @@ -342,7 +347,7 @@ export default class CurlyComponentManager } if (isInteractive && hasWrappedElement) { - component.trigger('willRender'); + sendCoreViewEvent(component, 'willRender'); } endUntrackFrame(); @@ -407,7 +412,7 @@ export default class CurlyComponentManager if (isInteractive) { beginUntrackFrame(); - component.trigger('willInsertElement'); + sendCoreViewEvent(component, 'willInsertElement'); endUntrackFrame(); } } @@ -420,8 +425,8 @@ export default class CurlyComponentManager didCreate({ component, isInteractive }: ComponentStateBucket): void { if (isInteractive) { component._transitionTo('inDOM'); - component.trigger('didInsertElement'); - component.trigger('didRender'); + sendCoreViewEvent(component, 'didInsertElement'); + sendCoreViewEvent(component, 'didRender'); } } @@ -443,13 +448,13 @@ export default class CurlyComponentManager component.setProperties(props); component[IS_DISPATCHING_ATTRS] = false; - component.trigger('didUpdateAttrs'); - component.trigger('didReceiveAttrs'); + sendCoreViewEvent(component, 'didUpdateAttrs'); + sendCoreViewEvent(component, 'didReceiveAttrs'); } if (isInteractive) { - component.trigger('willUpdate'); - component.trigger('willRender'); + sendCoreViewEvent(component, 'willUpdate'); + sendCoreViewEvent(component, 'willRender'); } endUntrackFrame(); @@ -464,8 +469,8 @@ export default class CurlyComponentManager didUpdate({ component, isInteractive }: ComponentStateBucket): void { if (isInteractive) { - component.trigger('didUpdate'); - component.trigger('didRender'); + sendCoreViewEvent(component, 'didUpdate'); + sendCoreViewEvent(component, 'didRender'); } } diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/root.ts b/packages/@ember/-internals/glimmer/lib/component-managers/root.ts index 00aca8f633c..2edd819fde1 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/root.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/root.ts @@ -20,6 +20,7 @@ import CurlyComponentManager, { initialRenderInstrumentDetails, processComponentInitializationAssertions, } from './curly'; +import { sendCoreViewEvent } from '@ember/-internals/views'; class RootComponentManager extends CurlyComponentManager { component: Component; @@ -47,13 +48,13 @@ class RootComponentManager extends CurlyComponentManager { // We usually do this in the `didCreateElement`, but that hook doesn't fire for tagless components if (!hasWrappedElement) { if (isInteractive) { - component.trigger('willRender'); + sendCoreViewEvent(component, 'willRender'); } component._transitionTo('hasElement'); if (isInteractive) { - component.trigger('willInsertElement'); + sendCoreViewEvent(component, 'willInsertElement'); } } diff --git a/packages/@ember/-internals/glimmer/lib/renderer.ts b/packages/@ember/-internals/glimmer/lib/renderer.ts index 2a0847d4818..6cb7976a4b0 100644 --- a/packages/@ember/-internals/glimmer/lib/renderer.ts +++ b/packages/@ember/-internals/glimmer/lib/renderer.ts @@ -3,7 +3,7 @@ import { ENV } from '@ember/-internals/environment'; import type { InternalOwner } from '@ember/-internals/owner'; import { getOwner } from '@ember/-internals/owner'; import { guidFor } from '@ember/-internals/utils'; -import { getViewElement, getViewId } from '@ember/-internals/views'; +import { getViewElement, getViewId, sendCoreViewEvent } from '@ember/-internals/views'; import { assert } from '@ember/debug'; import { _backburner, _getCurrentRunLoop } from '@ember/runloop'; import { @@ -917,7 +917,7 @@ export class Renderer extends BaseRenderer { this.cleanupRootFor(view); if (this.state.isInteractive) { - view.trigger('didDestroyElement'); + sendCoreViewEvent(view, 'didDestroyElement'); } } diff --git a/packages/@ember/-internals/glimmer/lib/utils/curly-component-state-bucket.ts b/packages/@ember/-internals/glimmer/lib/utils/curly-component-state-bucket.ts index 5a4fb529a6f..e07d740200f 100644 --- a/packages/@ember/-internals/glimmer/lib/utils/curly-component-state-bucket.ts +++ b/packages/@ember/-internals/glimmer/lib/utils/curly-component-state-bucket.ts @@ -1,4 +1,9 @@ -import { clearElementView, clearViewElement, getViewElement } from '@ember/-internals/views'; +import { + clearElementView, + clearViewElement, + getViewElement, + sendCoreViewEvent, +} from '@ember/-internals/views'; import { registerDestructor } from '@glimmer/destroyable'; import type { CapturedNamedArguments } from '@glimmer/interfaces'; import type { Reference } from '@glimmer/reference'; @@ -46,8 +51,8 @@ export default class ComponentStateBucket { if (isInteractive) { beginUntrackFrame(); - component.trigger('willDestroyElement'); - component.trigger('willClearRender'); + sendCoreViewEvent(component, 'willDestroyElement'); + sendCoreViewEvent(component, 'willClearRender'); endUntrackFrame(); let element = getViewElement(component); diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/append-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/append-test.js index 3e89c2297a3..17df9c34069 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/append-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/append-test.js @@ -1,4 +1,12 @@ -import { moduleFor, RenderingTestCase, strip, runTask } from 'internal-test-helpers'; +import { + moduleFor, + RenderingTestCase, + strip, + runTask, + expectDeprecation, + testUnless, +} from 'internal-test-helpers'; +import { DEPRECATIONS } from '../../../../deprecations'; import { set } from '@ember/object'; @@ -38,7 +46,9 @@ class AbstractAppendTest extends RenderingTestCase { this.ids.push(component.elementId); } - [`@test (new) lifecycle hooks during component append`](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_EVENTED.isRemoved)} @test (new) lifecycle hooks during component append`]( + assert + ) { let hooks = []; let componentsByName = {}; @@ -56,7 +66,13 @@ class AbstractAppendTest extends RenderingTestCase { } componentsByName[name] = this; pushHook('init'); - this.on('init', () => pushHook('on(init)')); + expectDeprecation( + () => { + this.on('init', () => pushHook('on(init)')); + }, + /Evented#on` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); } didReceiveAttrs() { @@ -278,7 +294,9 @@ class AbstractAppendTest extends RenderingTestCase { ); } - [`@test lifecycle hooks during component append`](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_EVENTED.isRemoved)} @test lifecycle hooks during component append`]( + assert + ) { let hooks = []; let oldRegisterComponent = this.registerComponent; @@ -299,7 +317,13 @@ class AbstractAppendTest extends RenderingTestCase { } componentsByName[name] = this; pushHook('init'); - this.on('init', () => pushHook('on(init)')); + expectDeprecation( + () => { + this.on('init', () => pushHook('on(init)')); + }, + /Evented#on` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); } didReceiveAttrs() { diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/curly-components-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/curly-components-test.js index 2bf955d092e..45e8a361702 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/curly-components-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/curly-components-test.js @@ -7,7 +7,10 @@ import { equalsElement, runTask, runLoopSettled, + expectDeprecation, + testUnless, } from 'internal-test-helpers'; +import { DEPRECATIONS } from '../../../../deprecations'; import { action } from '@ember/object'; import { run } from '@ember/runloop'; @@ -3159,48 +3162,60 @@ moduleFor( runTask(() => set(this.context, 'foo', 5)); } - ['@test triggering an event only attempts to invoke an identically named method, if it actually is a function (GH#15228)']( + [`${testUnless(DEPRECATIONS.DEPRECATE_EVENTED.isRemoved)} @test triggering an event only attempts to invoke an identically named method, if it actually is a function (GH#15228)`]( assert ) { - assert.expect(3); + assert.expect(5); let payload = ['arbitrary', 'event', 'data']; - this.registerComponent('evented-component', { - ComponentClass: Component.extend({ - someTruthyProperty: true, - - init() { - this._super(...arguments); - this.trigger('someMethod', ...payload); - this.trigger('someTruthyProperty', ...payload); - }, - - someMethod(...data) { - assert.deepEqual( - data, - payload, - 'the method `someMethod` should be called, when `someMethod` is triggered' - ); - }, - - listenerForSomeMethod: on('someMethod', function (...data) { - assert.deepEqual( - data, - payload, - 'the listener `listenerForSomeMethod` should be called, when `someMethod` is triggered' - ); - }), - - listenerForSomeTruthyProperty: on('someTruthyProperty', function (...data) { - assert.deepEqual( - data, - payload, - 'the listener `listenerForSomeTruthyProperty` should be called, when `someTruthyProperty` is triggered' - ); - }), - }), - }); + expectDeprecation( + () => { + this.registerComponent('evented-component', { + ComponentClass: Component.extend({ + someTruthyProperty: true, + + init() { + this._super(...arguments); + expectDeprecation( + () => { + this.trigger('someMethod', ...payload); + this.trigger('someTruthyProperty', ...payload); + }, + /Evented#trigger` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); + }, + + someMethod(...data) { + assert.deepEqual( + data, + payload, + 'the method `someMethod` should be called, when `someMethod` is triggered' + ); + }, + + listenerForSomeMethod: on('someMethod', function (...data) { + assert.deepEqual( + data, + payload, + 'the listener `listenerForSomeMethod` should be called, when `someMethod` is triggered' + ); + }), + + listenerForSomeTruthyProperty: on('someTruthyProperty', function (...data) { + assert.deepEqual( + data, + payload, + 'the listener `listenerForSomeTruthyProperty` should be called, when `someTruthyProperty` is triggered' + ); + }), + }), + }); + }, + /`on\(\)` event decorator is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); this.render(`{{evented-component}}`); } diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/life-cycle-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/life-cycle-test.js index 1c4f3bed6c0..e953ffef663 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/life-cycle-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/life-cycle-test.js @@ -6,6 +6,7 @@ import { A as emberA } from '@ember/array'; import { getViewElement, getViewId } from '@ember/-internals/views'; import { Component } from '../../utils/helpers'; +import { addListener } from '@ember/-internals/metal'; class LifeCycleHooksTest extends RenderingTestCase { constructor() { @@ -174,7 +175,7 @@ class LifeCycleHooksTest extends RenderingTestCase { assertNoElement('init', this); assertState('init', 'preRender', this); - this.on('init', () => pushHook('on(init)')); + addListener(this, 'init', () => pushHook('on(init)')); schedule('afterRender', () => { this.isInitialRender = false; diff --git a/packages/@ember/-internals/glimmer/tests/integration/event-dispatcher-test.js b/packages/@ember/-internals/glimmer/tests/integration/event-dispatcher-test.js index 60c37711e7d..7a40f1f169d 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/event-dispatcher-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/event-dispatcher-test.js @@ -1,4 +1,11 @@ -import { moduleFor, RenderingTestCase, runTask } from 'internal-test-helpers'; +import { + expectDeprecation, + moduleFor, + RenderingTestCase, + runTask, + testUnless, +} from 'internal-test-helpers'; +import { DEPRECATIONS } from '../../../deprecations'; import { Component } from '../utils/helpers'; import { _getCurrentRunLoop } from '@ember/runloop'; @@ -149,7 +156,9 @@ moduleFor( } } - ['@test event listeners are called when event is triggered'](assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_EVENTED.isRemoved)} @test event listeners are called when event is triggered`]( + assert + ) { let receivedEvent; let browserEvent; @@ -159,7 +168,13 @@ moduleFor( init() { super.init(); Object.keys(SUPPORTED_EMBER_EVENTS).forEach((browserEvent) => { - this.on(SUPPORTED_EMBER_EVENTS[browserEvent], (event) => (receivedEvent = event)); + expectDeprecation( + () => { + this.on(SUPPORTED_EMBER_EVENTS[browserEvent], (event) => (receivedEvent = event)); + }, + /Evented#on` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); }); } }, diff --git a/packages/@ember/-internals/glimmer/tests/integration/syntax/each-test.js b/packages/@ember/-internals/glimmer/tests/integration/syntax/each-test.js index e4ed8e69213..3c704fffbda 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/syntax/each-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/syntax/each-test.js @@ -1,4 +1,12 @@ -import { moduleFor, RenderingTestCase, applyMixins, strip, runTask } from 'internal-test-helpers'; +import { + moduleFor, + RenderingTestCase, + applyMixins, + strip, + runTask, + expectDeprecation, +} from 'internal-test-helpers'; +import { DEPRECATIONS } from '../../../../deprecations'; import { notifyPropertyChange, on } from '@ember/-internals/metal'; import { get, set, computed } from '@ember/object'; @@ -1116,21 +1124,30 @@ moduleFor( } ); -moduleFor( - 'Syntax test: {{#each}} with array proxies, content is updated after init', - class extends EachTest { - createList(items) { - let wrapped = emberA(items); - let proxy = ArrayProxy.extend({ - setup: on('init', function () { - this.set('content', emberA(wrapped)); - }), - }).create(); +if (!DEPRECATIONS.DEPRECATE_EVENTED.isRemoved) { + moduleFor( + 'Syntax test: {{#each}} with array proxies, content is updated after init', + class extends EachTest { + createList(items) { + let wrapped = emberA(items); + let proxy; + expectDeprecation( + () => { + proxy = ArrayProxy.extend({ + setup: on('init', function () { + this.set('content', emberA(wrapped)); + }), + }).create(); + }, + /`on\(\)` event decorator is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); - return { list: proxy, delegate: wrapped }; + return { list: proxy, delegate: wrapped }; + } } - } -); + ); +} moduleFor( 'Syntax test: {{#each as}} undefined path', diff --git a/packages/@ember/-internals/meta/lib/meta.ts b/packages/@ember/-internals/meta/lib/meta.ts index 7d429b5f13e..c366af2c88a 100644 --- a/packages/@ember/-internals/meta/lib/meta.ts +++ b/packages/@ember/-internals/meta/lib/meta.ts @@ -355,7 +355,7 @@ export class Meta { addToListeners( eventName: string, target: object | null, - method: Function | PropertyKey, + method: ((...args: any[]) => void) | PropertyKey, once: boolean, sync: boolean ) { diff --git a/packages/@ember/-internals/metal/lib/events.ts b/packages/@ember/-internals/metal/lib/events.ts index ab77fb52c86..eb6a645a7bd 100644 --- a/packages/@ember/-internals/metal/lib/events.ts +++ b/packages/@ember/-internals/metal/lib/events.ts @@ -6,6 +6,7 @@ import { meta as metaFor, peekMeta } from '@ember/-internals/meta'; import { setListeners } from '@ember/-internals/utils'; import type { AnyFn } from '@ember/-internals/utility-types'; import { assert } from '@ember/debug'; +import { DEPRECATIONS, deprecateUntil } from '@ember/-internals/deprecations'; /* The event system uses a series of nested hashes to store listeners on an @@ -38,11 +39,24 @@ import { assert } from '@ember/debug'; @param {Boolean} once A flag whether a function should only be called once @public */ +export function addListener( + obj: object, + eventName: string, + target: Target, + method: PropertyKey | ((this: Target, ...args: any[]) => void), + once?: boolean, + sync?: boolean +): void; +export function addListener( + obj: object, + eventName: string, + method: PropertyKey | ((...args: any[]) => void) +): void; export function addListener( obj: object, eventName: string, - target: object | Function | null, - method?: Function | PropertyKey, + target: object | PropertyKey | ((...args: any[]) => void) | null, + method?: PropertyKey | ((...args: any[]) => void), once?: boolean, sync = true ): void { @@ -52,10 +66,13 @@ export function addListener( ); if (!method && 'function' === typeof target) { - method = target; + // SAFETY: This should be correct. It may be possible to get TS to infer it. + method = target as (...args: any[]) => void; target = null; } + assert('target should be object or null', target === null || typeof target === 'object'); + metaFor(obj).addToListeners(eventName, target, method!, once === true, sync); } @@ -76,8 +93,19 @@ export function addListener( export function removeListener( obj: object, eventName: string, - targetOrFunction: object | Function | null, - functionOrName?: string | Function + target: object | null, + methodOrName: string | ((...args: any[]) => void) +): void; +export function removeListener( + obj: object, + eventName: string, + method: (...args: any[]) => void +): void; +export function removeListener( + obj: object, + eventName: string, + targetOrFunction: object | ((...args: any[]) => void) | null, + functionOrName?: string | ((...args: any[]) => void) ): void { assert( 'You must pass at least an object, event name, and method or target and method/method name to removeListener', @@ -143,7 +171,7 @@ export function sendEvent( continue; } if (once) { - removeListener(obj, eventName, target, method); + removeListener(obj, eventName, target, method as string | ((...args: any[]) => void)); } if (!target) { target = obj; @@ -199,6 +227,7 @@ export function hasListeners(obj: object, eventName: string): boolean { @method on @static + @deprecated Use native JavaScript events or a dedicated event library instead. @for @ember/object/evented @param {String} eventNames* @param {Function} func @@ -206,6 +235,11 @@ export function hasListeners(obj: object, eventName: string): boolean { @public */ export function on(...args: [...eventNames: string[], func: T]): T { + deprecateUntil( + 'The `on()` event decorator is deprecated. Use native JavaScript events or a dedicated event library instead.', + DEPRECATIONS.DEPRECATE_EVENTED + ); + let func = args.pop(); let events = args as string[]; diff --git a/packages/@ember/-internals/metal/lib/observer.ts b/packages/@ember/-internals/metal/lib/observer.ts index f1f8035fdbe..0cabeb7532b 100644 --- a/packages/@ember/-internals/metal/lib/observer.ts +++ b/packages/@ember/-internals/metal/lib/observer.ts @@ -43,7 +43,14 @@ export function addObserver( ): void { let eventName = changeEvent(path); - addListener(obj, eventName, target, method, false, sync); + addListener( + obj, + eventName, + target as any, + method as PropertyKey | ((...args: any[]) => void), + false, + sync + ); let meta = peekMeta(obj); @@ -77,7 +84,7 @@ export function removeObserver( deactivateObserver(obj, eventName, sync); } - removeListener(obj, eventName, target, method); + removeListener(obj, eventName, target as any, method as string | ((...args: any[]) => void)); } function getOrCreateActiveObserversFor(target: object, sync: boolean) { diff --git a/packages/@ember/-internals/metal/tests/computed_test.js b/packages/@ember/-internals/metal/tests/computed_test.js index 516da6905f6..1a20fc2166c 100644 --- a/packages/@ember/-internals/metal/tests/computed_test.js +++ b/packages/@ember/-internals/metal/tests/computed_test.js @@ -984,16 +984,16 @@ moduleFor( class LazyObject { value = 123; + /* eslint-disable no-dupe-class-members */ @computed('_value') - // eslint-disable-next-line no-dupe-class-members get value() { return get(this, '_value'); } - // eslint-disable-next-line no-dupe-class-members set value(value) { set(this, '_value', value); } + /* eslint-enable no-dupe-class-members */ static create() { obj = new LazyObject(); diff --git a/packages/@ember/-internals/metal/tests/events_test.js b/packages/@ember/-internals/metal/tests/events_test.js index 109792c39b7..c50f3e83bd3 100644 --- a/packages/@ember/-internals/metal/tests/events_test.js +++ b/packages/@ember/-internals/metal/tests/events_test.js @@ -1,6 +1,7 @@ import { on, addListener, removeListener, sendEvent, hasListeners } from '..'; import Mixin from '@ember/object/mixin'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'internal-test-helpers'; +import { DEPRECATIONS } from '../../deprecations'; moduleFor( 'system/props/events_test', @@ -139,17 +140,26 @@ moduleFor( assert.equal(hasListeners(obj, 'event!'), true, 'has listeners'); } - ['@test a listener can be added as part of a mixin'](assert) { + [`${testUnless( + DEPRECATIONS.DEPRECATE_EVENTED.isRemoved + )} @test a listener can be added as part of a mixin`](assert) { let triggered = 0; - let MyMixin = Mixin.create({ - foo1: on('bar', function () { - triggered++; - }), - - foo2: on('bar', function () { - triggered++; - }), - }); + let MyMixin; + expectDeprecation( + () => { + MyMixin = Mixin.create({ + foo1: on('bar', function () { + triggered++; + }), + + foo2: on('bar', function () { + triggered++; + }), + }); + }, + /`on\(\)` event decorator is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); let obj = {}; MyMixin.apply(obj); @@ -158,32 +168,63 @@ moduleFor( assert.equal(triggered, 2, 'should invoke listeners'); } - [`@test 'on' asserts for invalid arguments`]() { + [`${testUnless( + DEPRECATIONS.DEPRECATE_EVENTED.isRemoved + )} @test 'on' asserts for invalid arguments`]() { expectAssertion(() => { - Mixin.create({ - foo1: on('bar'), - }); + expectDeprecation( + () => { + Mixin.create({ + foo1: on('bar'), + }); + }, + /`on\(\)` event decorator is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); }, 'on expects function as last argument'); expectAssertion(() => { - Mixin.create({ - foo1: on(function () {}), - }); + expectDeprecation( + () => { + Mixin.create({ + foo1: on(function () {}), + }); + }, + /`on\(\)` event decorator is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); }, 'on called without valid event names'); } - ['@test a listener added as part of a mixin may be overridden'](assert) { + [`${testUnless( + DEPRECATIONS.DEPRECATE_EVENTED.isRemoved + )} @test a listener added as part of a mixin may be overridden`](assert) { let triggered = 0; - let FirstMixin = Mixin.create({ - foo: on('bar', function () { - triggered++; - }), - }); - let SecondMixin = Mixin.create({ - foo: on('baz', function () { - triggered++; - }), - }); + let FirstMixin; + expectDeprecation( + () => { + FirstMixin = Mixin.create({ + foo: on('bar', function () { + triggered++; + }), + }); + }, + /`on\(\)` event decorator is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); + + let SecondMixin; + expectDeprecation( + () => { + SecondMixin = Mixin.create({ + foo: on('baz', function () { + triggered++; + }), + }); + }, + /`on\(\)` event decorator is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); let obj = {}; FirstMixin.apply(obj); diff --git a/packages/@ember/-internals/utils/index.ts b/packages/@ember/-internals/utils/index.ts index 329a6f7e793..2e310ef344d 100644 --- a/packages/@ember/-internals/utils/index.ts +++ b/packages/@ember/-internals/utils/index.ts @@ -33,3 +33,9 @@ export { teardownMandatorySetter, setWithMandatorySetter, } from './lib/mandatory-setter'; +export { + DEPRECATION, + setDeprecation, + findDeprecation, + disableDeprecations, +} from './lib/mixin-deprecation'; diff --git a/packages/@ember/-internals/utils/lib/mixin-deprecation.ts b/packages/@ember/-internals/utils/lib/mixin-deprecation.ts new file mode 100644 index 00000000000..373e5181940 --- /dev/null +++ b/packages/@ember/-internals/utils/lib/mixin-deprecation.ts @@ -0,0 +1,44 @@ +import type { DeprecationObject } from '@ember/-internals/deprecations'; +import type Mixin from '@ember/object/mixin'; + +/** @internal */ +export const DEPRECATION = Symbol('DEPRECATION'); + +/** @internal */ +export function setDeprecation( + mixin: Mixin, + value: { message: string; deprecation: DeprecationObject } | null +) { + // SAFETY: Using symbol as index on Mixin - this is internal deprecated API tooling + (mixin as any)[DEPRECATION] = value; +} + +let deprecationsEnabled = true; + +export function findDeprecation( + mixin: Mixin +): { message: string; deprecation: DeprecationObject } | null { + if (!deprecationsEnabled) { + return null; + } + // SAFETY: Using symbol as index on Mixin - this is internal deprecated API tooling + if ((mixin as any)[DEPRECATION]) { + return (mixin as any)[DEPRECATION]; + } + for (let childMixin of mixin.mixins ?? []) { + let deprecation = findDeprecation(childMixin); + if (deprecation) { + return deprecation; + } + } + return null; +} + +export function disableDeprecations(callback: () => T): T { + try { + deprecationsEnabled = false; + return callback(); + } finally { + deprecationsEnabled = true; + } +} diff --git a/packages/@ember/-internals/views/index.ts b/packages/@ember/-internals/views/index.ts index a84d01a080b..43c1afd840c 100644 --- a/packages/@ember/-internals/views/index.ts +++ b/packages/@ember/-internals/views/index.ts @@ -18,6 +18,7 @@ export { export { default as EventDispatcher } from './lib/system/event_dispatcher'; export { default as ComponentLookup } from './lib/component_lookup'; export { default as CoreView } from './lib/views/core_view'; +export { sendCoreViewEvent, hasCoreViewListener } from './lib/views/core-view-utils'; export { default as ActionSupport } from './lib/mixins/action_support'; export { MUTABLE_CELL } from './lib/compat/attrs'; export { default as ViewStates } from './lib/views/states'; diff --git a/packages/@ember/-internals/views/lib/views/core-view-utils.ts b/packages/@ember/-internals/views/lib/views/core-view-utils.ts new file mode 100644 index 00000000000..5de7023627a --- /dev/null +++ b/packages/@ember/-internals/views/lib/views/core-view-utils.ts @@ -0,0 +1,14 @@ +import { hasListeners, sendEvent } from '@ember/-internals/metal'; +import type CoreView from './core_view'; + +export function sendCoreViewEvent(view: CoreView, name: string, args: any[] = []) { + sendEvent(view, name, args); + let method = (view as any)[name]; + if (typeof method === 'function') { + return method.apply(view, args); + } +} + +export function hasCoreViewListener(view: CoreView, name: string) { + return typeof (view as any)[name] === 'function' || hasListeners(view, name); +} diff --git a/packages/@ember/-internals/views/lib/views/core_view.ts b/packages/@ember/-internals/views/lib/views/core_view.ts index 241b233da65..8b0fa45f8d5 100644 --- a/packages/@ember/-internals/views/lib/views/core_view.ts +++ b/packages/@ember/-internals/views/lib/views/core_view.ts @@ -1,10 +1,11 @@ import type { Renderer, View } from '@ember/-internals/glimmer'; import { inject } from '@ember/-internals/metal'; import { ActionHandler } from '@ember/-internals/runtime'; -import Evented from '@ember/object/evented'; import { FrameworkObject } from '@ember/object/-internals'; import type { ViewState } from './states'; import states from './states'; +import Evented from '@ember/object/evented'; +import { disableDeprecations } from '@ember/-internals/utils'; /** `CoreView` is an abstract class that exists to give view-like behavior @@ -24,7 +25,7 @@ import states from './states'; */ interface CoreView extends Evented, ActionHandler, View {} -class CoreView extends FrameworkObject.extend(Evented, ActionHandler) { +class CoreView extends disableDeprecations(() => FrameworkObject.extend(Evented, ActionHandler)) { isView = true; declare _states: typeof states; diff --git a/packages/@ember/-internals/views/lib/views/states.ts b/packages/@ember/-internals/views/lib/views/states.ts index c9480ffbdf4..1bdf1a1df98 100644 --- a/packages/@ember/-internals/views/lib/views/states.ts +++ b/packages/@ember/-internals/views/lib/views/states.ts @@ -4,6 +4,7 @@ import { assert } from '@ember/debug'; import { flaggedInstrument } from '@ember/instrumentation'; import { join } from '@ember/runloop'; import { DEBUG } from '@glimmer/env'; +import { hasCoreViewListener, sendCoreViewEvent } from './core-view-utils'; export interface ViewState { enter?(view: Component): void; @@ -45,11 +46,13 @@ const HAS_ELEMENT: Readonly = Object.freeze({ // Handle events from `Ember.EventDispatcher` handleEvent(view: Component, eventName: string, event: Event) { - if (view.has(eventName)) { + if (hasCoreViewListener(view, eventName)) { // Handler should be able to re-dispatch events, so we don't // preventDefault or stopPropagation. return flaggedInstrument(`interaction.${eventName}`, { event, view }, () => { - return join(view, view.trigger, eventName, event); + return join(() => { + return sendCoreViewEvent(view, eventName, [event]); + }); }); } else { return true; // continue event propagation diff --git a/packages/@ember/object/evented.ts b/packages/@ember/object/evented.ts index 2134d344206..83be072e82f 100644 --- a/packages/@ember/object/evented.ts +++ b/packages/@ember/object/evented.ts @@ -1,4 +1,6 @@ import { addListener, removeListener, hasListeners, sendEvent } from '@ember/-internals/metal'; +import { DEPRECATIONS, deprecateUntil } from '@ember/-internals/deprecations'; +import { setDeprecation } from '@ember/-internals/utils'; import Mixin from '@ember/object/mixin'; export { on } from '@ember/-internals/metal'; @@ -46,6 +48,7 @@ export { on } from '@ember/-internals/metal'; @class Evented @public + @deprecated Use native JavaScript events or a dedicated event library instead. */ interface Evented { /** @@ -63,6 +66,7 @@ interface Evented { parameter is used the callback method becomes the third argument. @method on + @deprecated Use native JavaScript events or a dedicated event library instead. @param {String} name The name of the event @param {Object} [target] The "this" binding for the callback @param {Function|String} method A function or the name of a function to be called on `target` @@ -85,6 +89,7 @@ interface Evented { becomes the third argument. @method one + @deprecated Use native JavaScript events or a dedicated event library instead. @param {String} name The name of the event @param {Object} [target] The "this" binding for the callback @param {Function|String} method A function or the name of a function to be called on `target` @@ -113,6 +118,7 @@ interface Evented { ``` @method trigger + @deprecated Use native JavaScript events or a dedicated event library instead. @param {String} name The name of the event @param {Object...} args Optional arguments to pass on @public @@ -122,6 +128,7 @@ interface Evented { Cancels subscription for given name, target, and method. @method off + @deprecated Use native JavaScript events or a dedicated event library instead. @param {String} name The name of the event @param {Object} target The target of the subscription @param {Function|String} method The function or the name of a function of the subscription @@ -138,6 +145,7 @@ interface Evented { Checks to see if object has any subscriptions for named event. @method has + @deprecated Use native JavaScript events or a dedicated event library instead. @param {String} name The name of the event @return {Boolean} does the object have a subscription for event @public @@ -146,27 +154,56 @@ interface Evented { } const Evented = Mixin.create({ on(name: string, target: object, method?: string | Function) { - addListener(this, name, target, method); + deprecateUntil( + '`Evented#on` is deprecated. Use native JavaScript events or a dedicated event library instead.', + DEPRECATIONS.DEPRECATE_EVENTED + ); + // SAFETY: The types are not actually correct, but it's not worth the effort to fix them, since we're deprecating this API. + addListener(this, name, target, method as PropertyKey | ((...args: any[]) => void)); return this; }, one(name: string, target: object, method?: string | Function) { - addListener(this, name, target, method, true); + deprecateUntil( + '`Evented#one` is deprecated. Use native JavaScript events or a dedicated event library instead.', + DEPRECATIONS.DEPRECATE_EVENTED + ); + // SAFETY: The types are not actually correct, but it's not worth the effort to fix them, since we're deprecating this API. + addListener(this, name, target, method as PropertyKey | ((...args: any[]) => void), true); return this; }, trigger(name: string, ...args: any[]) { + deprecateUntil( + '`Evented#trigger` is deprecated. Use native JavaScript events or a dedicated event library instead.', + DEPRECATIONS.DEPRECATE_EVENTED + ); sendEvent(this, name, args); }, off(name: string, target: object, method?: string | Function) { - removeListener(this, name, target, method); + deprecateUntil( + '`Evented#off` is deprecated. Use native JavaScript events or a dedicated event library instead.', + DEPRECATIONS.DEPRECATE_EVENTED + ); + // SAFETY: The types are not actually correct, but it's not worth the effort to fix them, since we're deprecating this API. + removeListener(this, name, target, method as string | ((...args: any[]) => void)); return this; }, has(name: string) { + deprecateUntil( + '`Evented#has` is deprecated. Use native JavaScript events or a dedicated event library instead.', + DEPRECATIONS.DEPRECATE_EVENTED + ); return hasListeners(this, name); }, }); +setDeprecation(Evented, { + message: + 'Evented is deprecated. Use native JavaScript events or a dedicated event library instead.', + deprecation: DEPRECATIONS.DEPRECATE_EVENTED, +}); + export default Evented; diff --git a/packages/@ember/object/events.ts b/packages/@ember/object/events.ts index 3f4559266fc..af1230fbc07 100644 --- a/packages/@ember/object/events.ts +++ b/packages/@ember/object/events.ts @@ -1 +1,78 @@ -export { addListener, removeListener, sendEvent } from '@ember/-internals/metal'; +import { + addListener as originalAddListener, + removeListener as originalRemoveListener, + sendEvent as originalSendEvent, +} from '@ember/-internals/metal'; +import { DEPRECATIONS, deprecateUntil } from '@ember/-internals/deprecations'; + +export function addListener( + obj: object, + eventName: string, + target: Target, + method: PropertyKey | ((this: Target, ...args: any[]) => void), + once?: boolean, + sync?: boolean +): void; +export function addListener( + obj: object, + eventName: string, + method: PropertyKey | ((...args: any[]) => void) +): void; +export function addListener( + obj: object, + eventName: string, + target: object | PropertyKey | ((...args: any[]) => void) | null, + method?: PropertyKey | ((...args: any[]) => void), + once?: boolean, + sync?: boolean +): void { + deprecateUntil( + 'Importing `addListener` from `@ember/object/events` is deprecated. Use native JavaScript events or a dedicated event library instead.', + DEPRECATIONS.DEPRECATE_EVENTED + ); + return originalAddListener( + obj, + eventName, + target as any, + method as PropertyKey | ((...args: any[]) => void), + once, + sync + ); +} + +export function removeListener( + obj: object, + eventName: string, + target: Target, + method: PropertyKey | ((this: Target, ...args: any[]) => void) +): void; +export function removeListener( + obj: object, + eventName: string, + method: PropertyKey | ((...args: any[]) => void) +): void; +export function removeListener( + obj: object, + eventName: string, + target: object | PropertyKey | ((...args: any[]) => void) | null, + method?: PropertyKey | ((...args: any[]) => void) +): void { + deprecateUntil( + 'Importing `removeListener` from `@ember/object/events` is deprecated. Use native JavaScript events or a dedicated event library instead.', + DEPRECATIONS.DEPRECATE_EVENTED + ); + return originalRemoveListener( + obj, + eventName, + target as any, + method as string | ((...args: any[]) => void) + ); +} + +export const sendEvent = (...args: Parameters) => { + deprecateUntil( + 'Importing `sendEvent` from `@ember/object/events` is deprecated. Use native JavaScript events or a dedicated event library instead.', + DEPRECATIONS.DEPRECATE_EVENTED + ); + return originalSendEvent(...args); +}; diff --git a/packages/@ember/object/mixin.ts b/packages/@ember/object/mixin.ts index 32b550c2cf9..77e88ce0137 100644 --- a/packages/@ember/object/mixin.ts +++ b/packages/@ember/object/mixin.ts @@ -4,8 +4,16 @@ import { INIT_FACTORY } from '@ember/-internals/container'; import type { Meta } from '@ember/-internals/meta'; import { meta as metaFor, peekMeta } from '@ember/-internals/meta'; -import { guidFor, observerListenerMetaFor, ROOT, wrap } from '@ember/-internals/utils'; +import { + guidFor, + observerListenerMetaFor, + ROOT, + wrap, + DEPRECATION, + findDeprecation, +} from '@ember/-internals/utils'; import { assert } from '@ember/debug'; +import { deprecateUntil, type DeprecationObject } from '@ember/-internals/deprecations'; import { DEBUG } from '@glimmer/env'; import { type ComputedDecorator, @@ -14,6 +22,8 @@ import { type ComputedPropertySetter, type ComputedDescriptor, isClassicDecorator, + addListener, + removeListener, } from '@ember/-internals/metal'; import { ComputedProperty, @@ -27,7 +37,6 @@ import { defineDecorator, defineValue, } from '@ember/-internals/metal'; -import { addListener, removeListener } from '@ember/object/events'; const a_concat = Array.prototype.concat; const { isArray } = Array; @@ -385,10 +394,12 @@ function updateObserversAndListeners(obj: object, key: string, fn: Function, add } if (listeners !== undefined) { - let updateListener = add ? addListener : removeListener; - for (let listener of listeners) { - updateListener(obj, listener, null, key); + if (add) { + addListener(obj, listener, null, key as PropertyKey); + } else { + removeListener(obj, listener, null, key); + } } } } @@ -543,6 +554,9 @@ export default class Mixin { /** @internal */ properties: { [key: string]: any } | undefined; + /** @internal */ + [DEPRECATION]: { message: string; deprecation: DeprecationObject } | null = null; + /** @internal */ ownerConstructor: any; @@ -621,6 +635,15 @@ export default class Mixin { return this; } + if (DEBUG) { + for (let mixin of args) { + let dep = mixin instanceof Mixin ? findDeprecation(mixin) : null; + if (dep) { + deprecateUntil(dep.message, dep.deprecation); + } + } + } + if (this.properties) { let currentMixin = new Mixin(undefined, this.properties); this.properties = undefined; diff --git a/packages/@ember/object/tests/es-compatibility-test.js b/packages/@ember/object/tests/es-compatibility-test.js index 33cff30a067..35372ca5f70 100644 --- a/packages/@ember/object/tests/es-compatibility-test.js +++ b/packages/@ember/object/tests/es-compatibility-test.js @@ -9,7 +9,14 @@ import { sendEvent, } from '@ember/-internals/metal'; import Mixin from '@ember/object/mixin'; -import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers'; +import { + moduleFor, + AbstractTestCase, + runLoopSettled, + expectDeprecation, + testUnless, +} from 'internal-test-helpers'; +import { DEPRECATIONS } from '../../-internals/deprecations'; moduleFor( 'EmberObject ES Compatibility', @@ -276,37 +283,48 @@ moduleFor( SubEmberObject.metaForProperty('foo'); } - '@test observes / removeObserver on / removeListener interop'(assert) { + [`${testUnless(DEPRECATIONS.DEPRECATE_EVENTED.isRemoved)} @test observes / removeObserver on / removeListener interop`]( + assert + ) { let fooDidChangeBase = 0; let fooDidChangeA = 0; let fooDidChangeB = 0; let someEventBase = 0; let someEventA = 0; let someEventB = 0; - class A extends EmberObject.extend({ - fooDidChange: observer('foo', function () { - fooDidChangeBase++; - }), - - onSomeEvent: on('someEvent', function () { - someEventBase++; - }), - }) { - init() { - super.init(); - this.foo = 'bar'; - } - - fooDidChange() { - super.fooDidChange(); - fooDidChangeA++; - } - - onSomeEvent() { - super.onSomeEvent(); - someEventA++; - } - } + let A; + expectDeprecation( + () => { + A = class extends ( + EmberObject.extend({ + fooDidChange: observer('foo', function () { + fooDidChangeBase++; + }), + + onSomeEvent: on('someEvent', function () { + someEventBase++; + }), + }) + ) { + init() { + super.init(); + this.foo = 'bar'; + } + + fooDidChange() { + super.fooDidChange(); + fooDidChangeA++; + } + + onSomeEvent() { + super.onSomeEvent(); + someEventA++; + } + }; + }, + /`on\(\)` event decorator is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); class B extends A { fooDidChange() { diff --git a/packages/@ember/object/tests/evented_test.js b/packages/@ember/object/tests/evented_test.js index 03c65149b8a..2e7a920252d 100644 --- a/packages/@ember/object/tests/evented_test.js +++ b/packages/@ember/object/tests/evented_test.js @@ -1,21 +1,44 @@ import CoreObject from '@ember/object/core'; import EventedMixin from '@ember/object/evented'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'internal-test-helpers'; +import { DEPRECATIONS } from '../../-internals/deprecations'; moduleFor( 'Ember.Evented', class extends AbstractTestCase { - ['@test works properly on proxy-ish objects'](assert) { - let eventedProxyObj = class extends CoreObject.extend(EventedMixin) { - unknownProperty() { - return true; - } - }.create(); + [`${testUnless( + DEPRECATIONS.DEPRECATE_EVENTED.isRemoved + )} @test works properly on proxy-ish objects`](assert) { + let eventedProxyObj; + expectDeprecation( + () => { + eventedProxyObj = class extends CoreObject.extend(EventedMixin) { + unknownProperty() { + return true; + } + }.create(); + }, + /Evented is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); let noop = function () {}; - eventedProxyObj.on('foo', noop); - eventedProxyObj.off('foo', noop); + expectDeprecation( + () => { + eventedProxyObj.on('foo', noop); + }, + /Evented#on` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); + + expectDeprecation( + () => { + eventedProxyObj.off('foo', noop); + }, + /Evented#off` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); assert.ok(true, 'An assertion was triggered'); } diff --git a/packages/@ember/object/tests/events_test.js b/packages/@ember/object/tests/events_test.js index fee30bd5bdb..c3daddf6c3d 100644 --- a/packages/@ember/object/tests/events_test.js +++ b/packages/@ember/object/tests/events_test.js @@ -1,29 +1,59 @@ import EmberObject from '@ember/object'; import Evented from '@ember/object/evented'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, expectDeprecation, testUnless } from 'internal-test-helpers'; +import { DEPRECATIONS } from '../../-internals/deprecations'; moduleFor( 'Object events', class extends AbstractTestCase { - ['@test a listener can be added to an object'](assert) { + [`${testUnless( + DEPRECATIONS.DEPRECATE_EVENTED.isRemoved + )} @test a listener can be added to an object`](assert) { let count = 0; let F = function () { count++; }; - let obj = EmberObject.extend(Evented).create(); - - obj.on('event!', F); - obj.trigger('event!'); + let obj; + expectDeprecation( + () => { + obj = EmberObject.extend(Evented).create(); + }, + /Evented is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); + + expectDeprecation( + () => { + obj.on('event!', F); + }, + /Evented#on` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); + expectDeprecation( + () => { + obj.trigger('event!'); + }, + /Evented#trigger` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); assert.equal(count, 1, 'the event was triggered'); - obj.trigger('event!'); + expectDeprecation( + () => { + obj.trigger('event!'); + }, + /Evented#trigger` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); assert.equal(count, 2, 'the event was triggered'); } - ['@test a listener can be added and removed automatically the first time it is triggered']( + [`${testUnless( + DEPRECATIONS.DEPRECATE_EVENTED.isRemoved + )} @test a listener can be added and removed automatically the first time it is triggered`]( assert ) { let count = 0; @@ -31,77 +61,174 @@ moduleFor( count++; }; - let obj = EmberObject.extend(Evented).create(); - - obj.one('event!', F); - obj.trigger('event!'); + let obj; + expectDeprecation( + () => { + obj = EmberObject.extend(Evented).create(); + }, + /Evented is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); + + expectDeprecation( + () => { + obj.one('event!', F); + }, + /Evented#one` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); + + expectDeprecation( + () => { + obj.trigger('event!'); + }, + /Evented#trigger` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); assert.equal(count, 1, 'the event was triggered'); - obj.trigger('event!'); + expectDeprecation( + () => { + obj.trigger('event!'); + }, + /Evented#trigger` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); assert.equal(count, 1, 'the event was not triggered again'); } - ['@test triggering an event can have arguments'](assert) { + [`${testUnless( + DEPRECATIONS.DEPRECATE_EVENTED.isRemoved + )} @test triggering an event can have arguments`](assert) { let self, args; - let obj = EmberObject.extend(Evented).create(); - - obj.on('event!', function () { - args = [].slice.call(arguments); - self = this; - }); - - obj.trigger('event!', 'foo', 'bar'); + let obj; + expectDeprecation( + () => { + obj = EmberObject.extend(Evented).create(); + }, + /Evented is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); + + expectDeprecation( + () => { + obj.on('event!', function () { + args = [].slice.call(arguments); + self = this; + }); + }, + /Evented#on` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); + + expectDeprecation( + () => { + obj.trigger('event!', 'foo', 'bar'); + }, + /Evented#trigger` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); assert.deepEqual(args, ['foo', 'bar']); assert.equal(self, obj); } - ['@test a listener can be added and removed automatically and have arguments'](assert) { + [`${testUnless( + DEPRECATIONS.DEPRECATE_EVENTED.isRemoved + )} @test a listener can be added and removed automatically and have arguments`](assert) { let self, args; let count = 0; - let obj = EmberObject.extend(Evented).create(); - - obj.one('event!', function () { - args = [].slice.call(arguments); - self = this; - count++; - }); - - obj.trigger('event!', 'foo', 'bar'); + let obj; + expectDeprecation( + () => { + obj = EmberObject.extend(Evented).create(); + }, + /Evented is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); + + expectDeprecation( + () => { + obj.one('event!', function () { + args = [].slice.call(arguments); + self = this; + count++; + }); + }, + /Evented#one` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); + + expectDeprecation( + () => { + obj.trigger('event!', 'foo', 'bar'); + }, + /Evented#trigger` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); assert.deepEqual(args, ['foo', 'bar']); assert.equal(self, obj); assert.equal(count, 1, 'the event is triggered once'); - obj.trigger('event!', 'baz', 'bat'); + expectDeprecation( + () => { + obj.trigger('event!', 'baz', 'bat'); + }, + /Evented#trigger` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); assert.deepEqual(args, ['foo', 'bar']); assert.equal(count, 1, 'the event was not triggered again'); assert.equal(self, obj); } - ['@test binding an event can specify a different target'](assert) { + [`${testUnless( + DEPRECATIONS.DEPRECATE_EVENTED.isRemoved + )} @test binding an event can specify a different target`](assert) { let self, args; - let obj = EmberObject.extend(Evented).create(); + let obj; + expectDeprecation( + () => { + obj = EmberObject.extend(Evented).create(); + }, + /Evented is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); let target = {}; - obj.on('event!', target, function () { - args = [].slice.call(arguments); - self = this; - }); - - obj.trigger('event!', 'foo', 'bar'); + expectDeprecation( + () => { + obj.on('event!', target, function () { + args = [].slice.call(arguments); + self = this; + }); + }, + /Evented#on` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); + + expectDeprecation( + () => { + obj.trigger('event!', 'foo', 'bar'); + }, + /Evented#trigger` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); assert.deepEqual(args, ['foo', 'bar']); assert.equal(self, target); } - ['@test a listener registered with one can take method as string and can be added with different target']( + [`${testUnless( + DEPRECATIONS.DEPRECATE_EVENTED.isRemoved + )} @test a listener registered with one can take method as string and can be added with different target`]( assert ) { let count = 0; @@ -110,46 +237,152 @@ moduleFor( count++; }; - let obj = EmberObject.extend(Evented).create(); - - obj.one('event!', target, 'fn'); - obj.trigger('event!'); + let obj; + expectDeprecation( + () => { + obj = EmberObject.extend(Evented).create(); + }, + /Evented is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); + + expectDeprecation( + () => { + obj.one('event!', target, 'fn'); + }, + /Evented#one` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); + + expectDeprecation( + () => { + obj.trigger('event!'); + }, + /Evented#trigger` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); assert.equal(count, 1, 'the event was triggered'); - obj.trigger('event!'); + expectDeprecation( + () => { + obj.trigger('event!'); + }, + /Evented#trigger` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); assert.equal(count, 1, 'the event was not triggered again'); } - ['@test a listener registered with one can be removed with off'](assert) { - let obj = class extends EmberObject.extend(Evented) { - F() {} - }.create(); + [`${testUnless( + DEPRECATIONS.DEPRECATE_EVENTED.isRemoved + )} @test a listener registered with one can be removed with off`](assert) { + let obj; + expectDeprecation( + () => { + obj = class extends EmberObject.extend(Evented) { + F() {} + }.create(); + }, + /Evented is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); let F = function () {}; - obj.one('event!', F); - obj.one('event!', obj, 'F'); - - assert.equal(obj.has('event!'), true, 'has events'); - - obj.off('event!', F); - obj.off('event!', obj, 'F'); - - assert.equal(obj.has('event!'), false, 'has no more events'); + expectDeprecation( + () => { + obj.one('event!', F); + }, + /Evented#one` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); + + expectDeprecation( + () => { + obj.one('event!', obj, 'F'); + }, + /Evented#one` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); + + let objHas; + expectDeprecation( + () => { + objHas = obj.has('event!'); + }, + /Evented#has` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); + assert.equal(objHas, true, 'has events'); + + expectDeprecation( + () => { + obj.off('event!', F); + }, + /Evented#off` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); + + expectDeprecation( + () => { + obj.off('event!', obj, 'F'); + }, + /Evented#off` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); + + expectDeprecation( + () => { + objHas = obj.has('event!'); + }, + /Evented#has` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); + + assert.equal(objHas, false, 'has no more events'); } - ['@test adding and removing listeners should be chainable'](assert) { - let obj = EmberObject.extend(Evented).create(); + [`${testUnless( + DEPRECATIONS.DEPRECATE_EVENTED.isRemoved + )} @test adding and removing listeners should be chainable`](assert) { + let obj; + expectDeprecation( + () => { + obj = EmberObject.extend(Evented).create(); + }, + /Evented is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); let F = function () {}; - let ret = obj.on('event!', F); + let ret; + + expectDeprecation( + () => { + ret = obj.on('event!', F); + }, + /Evented#on` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); assert.equal(ret, obj, '#on returns self'); - ret = obj.off('event!', F); + expectDeprecation( + () => { + ret = obj.off('event!', F); + }, + /Evented#off` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); assert.equal(ret, obj, '#off returns self'); - ret = obj.one('event!', F); + expectDeprecation( + () => { + ret = obj.one('event!', F); + }, + /Evented#one` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); assert.equal(ret, obj, '#one returns self'); } } diff --git a/packages/@ember/routing/route.ts b/packages/@ember/routing/route.ts index 7b4a8f24eac..066eb847101 100644 --- a/packages/@ember/routing/route.ts +++ b/packages/@ember/routing/route.ts @@ -4,6 +4,7 @@ import { defineProperty, descriptorForProperty, flushAsyncObservers, + sendEvent, } from '@ember/-internals/metal'; import type Owner from '@ember/owner'; import { getOwner } from '@ember/-internals/owner'; @@ -36,6 +37,7 @@ import { prefixRouteNameArg, stashParamNames, } from './lib/utils'; +import { disableDeprecations } from '@ember/-internals/utils'; export interface ExtendedInternalRouteInfo extends InternalRouteInfo { _names?: unknown[]; @@ -252,7 +254,10 @@ interface Route extends IRoute, ActionHandler, Evented { error?(error: Error, transition: Transition): boolean | void; } -class Route extends EmberObject.extend(ActionHandler, Evented) implements IRoute { +class Route + extends disableDeprecations(() => EmberObject.extend(ActionHandler, Evented)) + implements IRoute +{ static isRouteFactory = true; // These properties will end up appearing in the public interface because we @@ -769,7 +774,7 @@ class Route extends EmberObject.extend(ActionHandler, Evented) */ exit(transition?: Transition) { this.deactivate(transition); - this.trigger('deactivate', transition); + sendEvent(this, 'deactivate', [transition]); this.teardownViews(); } @@ -795,7 +800,7 @@ class Route extends EmberObject.extend(ActionHandler, Evented) enter(transition: Transition) { this[RENDER_STATE] = undefined; this.activate(transition); - this.trigger('activate', transition); + sendEvent(this, 'activate', [transition]); } /** diff --git a/packages/@ember/routing/router-service.ts b/packages/@ember/routing/router-service.ts index 936e7539d1c..053c77af4bc 100644 --- a/packages/@ember/routing/router-service.ts +++ b/packages/@ember/routing/router-service.ts @@ -2,7 +2,6 @@ * @module @ember/routing/router-service */ import { getOwner } from '@ember/-internals/owner'; -import Evented from '@ember/object/evented'; import { assert } from '@ember/debug'; import { readOnly } from '@ember/object/computed'; import Service from '@ember/service'; @@ -13,6 +12,7 @@ import EmberRouter from '@ember/routing/router'; import type { RouteInfo, RouteInfoWithAttributes } from './lib/route-info'; import type { RouteArgs, RouteOptions } from './lib/utils'; import { extractRouteArgs, resemblesURL, shallowEqual } from './lib/utils'; +import { addListener, hasListeners, removeListener, sendEvent } from '@ember/-internals/metal'; export const ROUTER = Symbol('ROUTER'); @@ -24,6 +24,8 @@ function cleanURL(url: string, rootURL: string) { return url.substring(rootURL.length); } +type EventName = 'routeWillChange' | 'routeDidChange'; + /** The Router service is the public API that provides access to the router. @@ -55,14 +57,108 @@ function cleanURL(url: string, rootURL: string) { @extends Service @class RouterService */ -interface RouterService extends Evented { +class RouterService extends Service { + [ROUTER]?: EmberRouter; + + /** + Subscribes to a named event with given function. + + @method on + @param {String} name The name of the event + @param {Object} [target] The "this" binding for the callback + @param {Function|String} method A function or the name of a function to be called on `target` + @return this + */ + on(name: 'routeWillChange' | 'routeDidChange', callback: (transition: Transition) => void): this; + on( + name: EventName, + target: Target, + method: string | ((this: Target, ...args: any[]) => void) + ): this; + on(name: EventName, method: ((...args: any[]) => void) | string): this; on( - eventName: 'routeWillChange' | 'routeDidChange', - callback: (transition: Transition) => void + name: EventName, + target: object | ((...args: any[]) => void) | string, + method?: string | ((...args: any[]) => void) + ) { + // SAFETY: The types are not actually correct, but it's not worth the effort to fix them, since we'll be deprecating this API soon. + addListener(this, name, target, method as any); + return this; + } + + /** + Subscribes a function to a named event and then cancels the subscription + after the first time the event is triggered. + + @method one + @param {String} name The name of the event + @param {Object} [target] The "this" binding for the callback + @param {Function|String} method A function or the name of a function to be called on `target` + @return this + */ + one( + name: string, + target: Target, + method: string | ((this: Target, ...args: any[]) => void) ): this; -} -class RouterService extends Service.extend(Evented) { - [ROUTER]?: EmberRouter; + one(name: string, method: string | ((...args: any[]) => void)): this; + one( + name: string, + target: object | string | ((...args: any[]) => void), + method?: string | Function + ) { + // SAFETY: The types are not actually correct, but it's not worth the effort to fix them, since we'll be deprecating this API soon. + addListener(this, name, target, method as any, true); + return this; + } + + /** + Triggers a named event for the object. + + @method trigger + @param {String} name The name of the event + @param {Object...} args Optional arguments to pass on + @return {boolean} true if listeners were notified, false otherwise + */ + trigger(name: string, ...args: any[]): boolean { + return sendEvent(this, name, args); + } + + /** + Cancels subscription for given name, target, and method. + + @method off + @param {String} name The name of the event + @param {Object} target The target of the subscription + @param {Function|String} method The function or the name of a function of the subscription + @return this + */ + off( + name: string, + target: Target, + method: string | ((this: Target, ...args: any[]) => void) + ): this; + off(name: string, method: string | ((...args: any[]) => void)): this; + off( + name: string, + target: object | string | ((...args: any[]) => void), + method?: string | Function + ) { + // SAFETY: The types are not actually correct, but it's not worth the effort to fix them, since we'll be deprecating this API soon. + removeListener(this, name, target as any, method as any); + return this; + } + + /** + Checks to see if object has any subscriptions for named event. + + @method has + @param {String} name The name of the event + @return {Boolean} does the object have a subscription for event + */ + has(name: string) { + return hasListeners(this, name); + } get _router(): EmberRouter { let router = this[ROUTER]; diff --git a/packages/@ember/routing/router.ts b/packages/@ember/routing/router.ts index a52b28f0a32..735fe11187d 100644 --- a/packages/@ember/routing/router.ts +++ b/packages/@ember/routing/router.ts @@ -50,6 +50,8 @@ import type { QueryParams } from 'route-recognizer'; import type { AnyFn, MethodNamesOf, OmitFirst } from '@ember/-internals/utility-types'; import type { Template } from '@glimmer/interfaces'; import type ApplicationInstance from '@ember/application/instance'; +import { sendEvent } from '@ember/-internals/metal'; +import { disableDeprecations } from '@ember/-internals/utils'; /** @module @ember/routing/router @@ -135,7 +137,10 @@ const { slice } = Array.prototype; @uses Evented @public */ -class EmberRouter extends EmberObject.extend(Evented) implements Evented { +class EmberRouter + extends disableDeprecations(() => EmberObject.extend(Evented)) + implements Evented +{ /** Represents the URL of the root of the application, often '/'. This prefix is assumed on all routes defined on this router. @@ -415,7 +420,7 @@ class EmberRouter extends EmberObject.extend(Evented) implements Evented { } routeWillChange(transition: Transition) { - router.trigger('routeWillChange', transition); + sendEvent(router, 'routeWillChange', [transition]); if (DEBUG) { freezeRouteInfo(transition); @@ -433,7 +438,7 @@ class EmberRouter extends EmberObject.extend(Evented) implements Evented { routeDidChange(transition: Transition) { router.set('currentRoute', transition.to); once(() => { - router.trigger('routeDidChange', transition); + sendEvent(router, 'routeDidChange', [transition]); if (DEBUG) { freezeRouteInfo(transition); diff --git a/packages/ember/barrel.ts b/packages/ember/barrel.ts index 55b88a16432..afa6b18c8cf 100644 --- a/packages/ember/barrel.ts +++ b/packages/ember/barrel.ts @@ -520,17 +520,26 @@ namespace Ember { export declare let lookup: Record; /** - This should not be needed. - You may be able to use an event listener for the error event on window. - - `window.addEventListener('error', eventHandler);` - - For promise rejections, you'll want to use the unhandledrejection event. - `window.addEventListener('unhandledrejection', eventHandler);` - - If you really need the original behavior: - - `import { getOnerror, setOnerror } from '@ember/-internals/error-handling';` + A function may be assigned to `Ember.onerror` to be called when Ember + internals encounter an error. This is useful for specialized error handling + and reporting code. + + ```javascript + + Ember.onerror = function(error) { + const payload = { + stack: error.stack, + otherInformation: 'whatever app state you want to provide' + }; + + fetch('/report-error', { + method: 'POST', + body: JSON.stringify(payload) + }); + }; + ``` + + Internally, `Ember.onerror` is used as Backburner's error handler. @event onerror @for Ember diff --git a/packages/ember/tests/routing/decoupled_basic_test.js b/packages/ember/tests/routing/decoupled_basic_test.js index a71b245d919..832517ec48e 100644 --- a/packages/ember/tests/routing/decoupled_basic_test.js +++ b/packages/ember/tests/routing/decoupled_basic_test.js @@ -13,7 +13,10 @@ import { ModuleBasedTestResolver, runDestroy, runTask, + expectDeprecation, + testUnless, } from 'internal-test-helpers'; +import { DEPRECATIONS } from '@ember/-internals/deprecations'; import { run } from '@ember/runloop'; import { addObserver } from '@ember/-internals/metal'; import { service } from '@ember/service'; @@ -878,8 +881,10 @@ moduleFor( }); } - ['@test `activate` event fires on the route'](assert) { - assert.expect(4); + [`${testUnless(DEPRECATIONS.DEPRECATE_EVENTED.isRemoved)} @test \`activate\` event fires on the route`]( + assert + ) { + assert.expect(5); let eventFired = 0; @@ -893,10 +898,16 @@ moduleFor( init() { super.init(...arguments); - this.on('activate', function (transition) { - assert.equal(++eventFired, 1, 'activate event is fired once'); - assert.ok(transition, 'transition is passed to activate event'); - }); + expectDeprecation( + () => { + this.on('activate', function (transition) { + assert.equal(++eventFired, 1, 'activate event is fired once'); + assert.ok(transition, 'transition is passed to activate event'); + }); + }, + /Evented#on` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); } activate(transition) { @@ -909,8 +920,10 @@ moduleFor( return this.visit('/nork'); } - ['@test `deactivate` event fires on the route'](assert) { - assert.expect(4); + [`${testUnless(DEPRECATIONS.DEPRECATE_EVENTED.isRemoved)} @test \`deactivate\` event fires on the route`]( + assert + ) { + assert.expect(5); let eventFired = 0; @@ -925,10 +938,16 @@ moduleFor( init() { super.init(...arguments); - this.on('deactivate', function (transition) { - assert.equal(++eventFired, 1, 'deactivate event is fired once'); - assert.ok(transition, 'transition is passed'); - }); + expectDeprecation( + () => { + this.on('deactivate', function (transition) { + assert.equal(++eventFired, 1, 'deactivate event is fired once'); + assert.ok(transition, 'transition is passed'); + }); + }, + /Evented#on` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); } deactivate(transition) { diff --git a/type-tests/@ember/routing-test/router-service.ts b/type-tests/@ember/routing-test/router-service.ts index 2b6083b8a34..0944a281c4e 100644 --- a/type-tests/@ember/routing-test/router-service.ts +++ b/type-tests/@ember/routing-test/router-service.ts @@ -22,7 +22,7 @@ expectTypeOf(router.on('routeWillChange', routeWillChangeHandler)).toEqualTypeOf expectTypeOf(router.has('routeWillChange')).toEqualTypeOf(); expectTypeOf(router.off('routeWillChange', routeWillChangeHandler)).toEqualTypeOf(); expectTypeOf(router.one('routeWillChange', routeWillChangeHandler)).toEqualTypeOf(); -expectTypeOf(router.trigger('routeWillChange', 'boo')).toBeAny(); +expectTypeOf(router.trigger('routeWillChange', 'boo')).toEqualTypeOf(); const transition = router.transitionTo('someRoute');