diff --git a/package.json b/package.json index 86c1d3d37db..c3455ab2707 100644 --- a/package.json +++ b/package.json @@ -204,6 +204,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 9b1c7e93827..ec164970692 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; @@ -119,6 +119,13 @@ export const DEPRECATIONS = { until: '7.5.0', url: 'https://deprecations.emberjs.com/id/deprecate-comparable-mixin', }), + DEPRECATE_EVENTED: deprecation({ + id: 'deprecate-evented', + for: 'ember-source', + since: { available: '7.3.0' }, + until: '8.0.0', + url: 'https://deprecations.emberjs.com/id/deprecate-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 a79ef3392af..c7e1027ca35 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts @@ -10,6 +10,7 @@ import { setElementView, setViewElement, } from '@ember/-internals/views/lib/system/utils'; +import { sendCoreViewEvent } from '@ember/-internals/views/lib/views/core-view-utils'; import type { Nullable } from '@ember/-internals/utility-types'; import { assert, debugFreeze } from '@ember/debug'; import { _instrumentStart } from '@ember/instrumentation'; @@ -318,20 +319,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'); } } @@ -355,7 +356,7 @@ export default class CurlyComponentManager } if (isInteractive && hasWrappedElement) { - component.trigger('willRender'); + sendCoreViewEvent(component, 'willRender'); } endUntrackFrame(); @@ -420,7 +421,7 @@ export default class CurlyComponentManager if (isInteractive) { beginUntrackFrame(); - component.trigger('willInsertElement'); + sendCoreViewEvent(component, 'willInsertElement'); endUntrackFrame(); } } @@ -433,8 +434,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'); } } @@ -456,13 +457,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(); @@ -477,8 +478,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 2ca09c5e158..abcb5383a05 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/root.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/root.ts @@ -21,6 +21,7 @@ import CurlyComponentManager, { initialRenderInstrumentDetails, processComponentInitializationAssertions, } from './curly'; +import { sendCoreViewEvent } from '@ember/-internals/views/lib/views/core-view-utils'; class RootComponentManager extends CurlyComponentManager { component: Component; @@ -48,13 +49,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 e533c4042ba..c404404ecd9 100644 --- a/packages/@ember/-internals/glimmer/lib/renderer.ts +++ b/packages/@ember/-internals/glimmer/lib/renderer.ts @@ -3,6 +3,7 @@ import type { InternalOwner } from '@ember/-internals/owner'; import { getOwner } from '@ember/-internals/owner'; import { guidFor } from '@ember/-internals/utils/lib/guid'; import { getViewElement, getViewId } from '@ember/-internals/views/lib/system/utils'; +import { sendCoreViewEvent } from '@ember/-internals/views/lib/views/core-view-utils'; import { assert } from '@ember/debug'; import { associateDestroyableChild, @@ -309,7 +310,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 49907136c01..1780214877f 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 @@ -3,6 +3,7 @@ import { clearViewElement, getViewElement, } from '@ember/-internals/views/lib/system/utils'; +import { sendCoreViewEvent } from '@ember/-internals/views/lib/views/core-view-utils'; import { registerDestructor } from '@glimmer/destroyable'; import type { CapturedNamedArguments } from '@glimmer/interfaces'; import type { Reference } from '@glimmer/reference/lib/reference'; @@ -52,8 +53,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 8c00b604cc9..ecf50e8062d 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'; import { setComponentTemplate } from '@glimmer/manager'; @@ -39,7 +47,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 = {}; @@ -57,7 +67,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() { @@ -279,7 +295,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 componentsByName = {}; @@ -298,7 +316,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 5a1b7771b20..41420b751bf 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'; @@ -3440,48 +3443,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.owner.register( - 'component:evented-component', - Component.extend({ - someTruthyProperty: true, - - init() { - this._super(...arguments); - this.trigger('someMethod', ...payload); - this.trigger('someTruthyProperty', ...payload); - }, + expectDeprecation( + () => { + this.owner.register( + 'component:evented-component', + 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' - ); - }, + 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' - ); - }), - }) + 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 911f26a44e4..b989a47b24a 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 @@ -8,6 +8,7 @@ import { precompileTemplate } from '@ember/template-compilation'; import { setComponentTemplate } from '@glimmer/manager'; import { Component } from '../../utils/helpers'; +import { addListener } from '@ember/-internals/metal'; class LifeCycleHooksTest extends RenderingTestCase { constructor() { @@ -176,7 +177,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 9b496a0ef36..bfb6816e210 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; @@ -160,7 +169,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 c1017feeaa1..1dd20bef531 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'; @@ -1109,21 +1117,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 781566d20bd..77d703641c2 100644 --- a/packages/@ember/-internals/meta/lib/meta.ts +++ b/packages/@ember/-internals/meta/lib/meta.ts @@ -356,7 +356,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 56668b9bc9b..62f20d3f462 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/lib/meta'; import { setListeners } from '@ember/-internals/utils/lib/super'; 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 08c5148cd9c..5127f2cde2b 100644 --- a/packages/@ember/-internals/metal/lib/observer.ts +++ b/packages/@ember/-internals/metal/lib/observer.ts @@ -44,7 +44,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); @@ -78,7 +85,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/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 d68ea2baff8..a217d978b06 100644 --- a/packages/@ember/-internals/views/index.ts +++ b/packages/@ember/-internals/views/index.ts @@ -17,6 +17,7 @@ export { } from './lib/system/utils'; export { default as EventDispatcher } from './lib/system/event_dispatcher'; 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..dfc273e84bd --- /dev/null +++ b/packages/@ember/-internals/views/lib/views/core-view-utils.ts @@ -0,0 +1,14 @@ +import { hasListeners, sendEvent } from '@ember/-internals/metal/lib/events'; +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 ceb6c0cfa8f..3f08d7cfba1 100644 --- a/packages/@ember/-internals/views/lib/views/core_view.ts +++ b/packages/@ember/-internals/views/lib/views/core_view.ts @@ -5,6 +5,7 @@ import Evented from '@ember/object/evented'; import { FrameworkObject } from '@ember/object/-internals'; import type { ViewState } from './states'; import states from './states'; +import { disableDeprecations } from '@ember/-internals/utils/lib/mixin-deprecation'; /** `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 aba69ba106b..3dead388b0f 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 097d274d4bf..fcb3a179654 100644 --- a/packages/@ember/object/evented.ts +++ b/packages/@ember/object/evented.ts @@ -4,6 +4,8 @@ import { hasListeners, sendEvent, } from '@ember/-internals/metal/lib/events'; +import { DEPRECATIONS, deprecateUntil } from '@ember/-internals/deprecations'; +import { setDeprecation } from '@ember/-internals/utils/lib/mixin-deprecation'; import Mixin from '@ember/object/mixin'; export { on } from '@ember/-internals/metal/lib/events'; @@ -51,6 +53,7 @@ export { on } from '@ember/-internals/metal/lib/events'; @class Evented @public + @deprecated Use native JavaScript events or a dedicated event library instead. */ interface Evented { /** @@ -68,6 +71,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` @@ -90,6 +94,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` @@ -118,6 +123,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 @@ -127,6 +133,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 @@ -143,6 +150,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 @@ -151,27 +159,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 66224dac13f..7f542db3c31 100644 --- a/packages/@ember/object/events.ts +++ b/packages/@ember/object/events.ts @@ -1 +1,78 @@ -export { addListener, removeListener, sendEvent } from '@ember/-internals/metal/lib/events'; +import { + addListener as originalAddListener, + removeListener as originalRemoveListener, + sendEvent as originalSendEvent, +} from '@ember/-internals/metal/lib/events'; +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 c76b57f7dbe..6e3951e9a63 100644 --- a/packages/@ember/object/mixin.ts +++ b/packages/@ember/object/mixin.ts @@ -5,7 +5,9 @@ import { INIT_FACTORY } from '@ember/-internals/container/lib/container'; import type { Meta } from '@ember/-internals/meta/lib/meta'; import { meta as metaFor, peekMeta } from '@ember/-internals/meta/lib/meta'; import { observerListenerMetaFor, ROOT, wrap } from '@ember/-internals/utils/lib/super'; +import { DEPRECATION, findDeprecation } from '@ember/-internals/utils/lib/mixin-deprecation'; import { assert } from '@ember/debug'; +import { deprecateUntil, type DeprecationObject } from '@ember/-internals/deprecations'; import { DEBUG } from '@glimmer/env'; import type { ComputedDecorator, @@ -27,7 +29,7 @@ import { revalidateObservers, } from '@ember/-internals/metal/lib/observer'; import { defineDecorator, defineValue } from '@ember/-internals/metal/lib/properties'; -import { addListener, removeListener } from '@ember/object/events'; +import { addListener, removeListener } from '@ember/-internals/metal/lib/events'; const a_concat = Array.prototype.concat; const { isArray } = Array; @@ -385,10 +387,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 +547,9 @@ export default class Mixin { /** @internal */ properties: { [key: string]: any } | undefined; + /** @internal */ + [DEPRECATION]: { message: string; deprecation: DeprecationObject } | null = null; + /** @internal */ ownerConstructor: any; @@ -613,6 +620,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 28831d60328..1bf7cc7f96d 100644 --- a/packages/@ember/routing/route.ts +++ b/packages/@ember/routing/route.ts @@ -2,6 +2,7 @@ import { privatize as P } from '@ember/-internals/container/lib/registry'; import { addObserver, flushAsyncObservers } from '@ember/-internals/metal/lib/observer'; import { defineProperty } from '@ember/-internals/metal/lib/properties'; import { descriptorForProperty } from '@ember/-internals/metal/lib/decorator'; +import { sendEvent } from '@ember/-internals/metal/lib/events'; import type Owner from '@ember/owner'; import { getOwner } from '@ember/-internals/owner'; import type { default as BucketCache } from './lib/cache'; @@ -40,6 +41,7 @@ import { prefixRouteNameArg, stashParamNames, } from './lib/utils'; +import { disableDeprecations } from '@ember/-internals/utils/lib/mixin-deprecation'; export interface ExtendedInternalRouteInfo extends InternalRouteInfo { _names?: unknown[]; @@ -256,7 +258,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 @@ -773,7 +778,7 @@ class Route extends EmberObject.extend(ActionHandler, Evented) */ exit(transition?: Transition) { this.deactivate(transition); - this.trigger('deactivate', transition); + sendEvent(this, 'deactivate', [transition]); this.teardownViews(); } @@ -799,7 +804,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 9fc5e3b4df1..24ed95c46ed 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'; @@ -14,6 +13,12 @@ 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/lib/events'; export const ROUTER = Symbol('ROUTER'); @@ -25,6 +30,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. @@ -56,14 +63,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 7f6d760dd9d..283fce08297 100644 --- a/packages/@ember/routing/router.ts +++ b/packages/@ember/routing/router.ts @@ -57,6 +57,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/lib/events'; +import { disableDeprecations } from '@ember/-internals/utils/lib/mixin-deprecation'; /** @module @ember/routing/router @@ -142,7 +144,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. @@ -422,7 +427,7 @@ class EmberRouter extends EmberObject.extend(Evented) implements Evented { } routeWillChange(transition: Transition) { - router.trigger('routeWillChange', transition); + sendEvent(router, 'routeWillChange', [transition]); if (DEBUG) { freezeRouteInfo(transition); @@ -440,7 +445,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/tests/routing/decoupled_basic_test.js b/packages/ember/tests/routing/decoupled_basic_test.js index 0b81cb807ed..92461f5796b 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'; @@ -888,8 +891,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; @@ -903,10 +908,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) { @@ -919,8 +930,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; @@ -935,10 +948,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');