diff --git a/package.json b/package.json index dca8e27a2e8..9448adf3226 100644 --- a/package.json +++ b/package.json @@ -213,6 +213,7 @@ "@ember/-internals/runtime/lib/mixins/action_handler.js": "ember-source/@ember/-internals/runtime/lib/mixins/action_handler.js", "@ember/-internals/runtime/lib/mixins/comparable.js": "ember-source/@ember/-internals/runtime/lib/mixins/comparable.js", "@ember/-internals/runtime/lib/mixins/container_proxy.js": "ember-source/@ember/-internals/runtime/lib/mixins/container_proxy.js", + "@ember/-internals/runtime/lib/mixins/evented.js": "ember-source/@ember/-internals/runtime/lib/mixins/evented.js", "@ember/-internals/runtime/lib/mixins/registry_proxy.js": "ember-source/@ember/-internals/runtime/lib/mixins/registry_proxy.js", "@ember/-internals/runtime/lib/mixins/target_action_support.js": "ember-source/@ember/-internals/runtime/lib/mixins/target_action_support.js", "@ember/-internals/string/index.js": "ember-source/@ember/-internals/string/index.js", @@ -226,6 +227,7 @@ "@ember/-internals/views/lib/system/action_manager.js": "ember-source/@ember/-internals/views/lib/system/action_manager.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/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 a1a177fd924..91b9d85f117 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 { @@ -893,7 +893,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..cea41705a8a 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,10 @@ -import { moduleFor, RenderingTestCase, strip, runTask } from 'internal-test-helpers'; +import { + moduleFor, + RenderingTestCase, + strip, + runTask, + expectDeprecation, +} from 'internal-test-helpers'; import { set } from '@ember/object'; @@ -56,7 +62,9 @@ class AbstractAppendTest extends RenderingTestCase { } componentsByName[name] = this; pushHook('init'); - this.on('init', () => pushHook('on(init)')); + expectDeprecation(() => { + this.on('init', () => pushHook('on(init)')); + }, /`on` is deprecated/); } didReceiveAttrs() { @@ -299,7 +307,9 @@ class AbstractAppendTest extends RenderingTestCase { } componentsByName[name] = this; pushHook('init'); - this.on('init', () => pushHook('on(init)')); + expectDeprecation(() => { + this.on('init', () => pushHook('on(init)')); + }, /`on` is deprecated/); } 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..8ba4f8bb396 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,6 +7,7 @@ import { equalsElement, runTask, runLoopSettled, + expectDeprecation, } from 'internal-test-helpers'; import { action } from '@ember/object'; @@ -3162,45 +3163,49 @@ moduleFor( ['@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' - ); - }, + expectDeprecation(() => { + this.registerComponent('evented-component', { + ComponentClass: Component.extend({ + someTruthyProperty: true, + + init() { + this._super(...arguments); + expectDeprecation(() => { + this.trigger('someMethod', ...payload); + this.trigger('someTruthyProperty', ...payload); + }, /`trigger` is deprecated/); + }, - listenerForSomeMethod: on('someMethod', function (...data) { - assert.deepEqual( - data, - payload, - 'the listener `listenerForSomeMethod` should be called, when `someMethod` is triggered' - ); - }), + someMethod(...data) { + assert.deepEqual( + data, + payload, + 'the method `someMethod` 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` is deprecated/); 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..8bfeec4cab6 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 @@ -1,4 +1,11 @@ -import { classes, moduleFor, RenderingTestCase, runTask, strip } from 'internal-test-helpers'; +import { + classes, + expectDeprecation, + moduleFor, + RenderingTestCase, + runTask, + strip, +} from 'internal-test-helpers'; import { schedule } from '@ember/runloop'; import { set, setProperties } from '@ember/object'; @@ -6,6 +13,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 +182,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..43bbcb27b41 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,4 @@ -import { moduleFor, RenderingTestCase, runTask } from 'internal-test-helpers'; +import { expectDeprecation, moduleFor, RenderingTestCase, runTask } from 'internal-test-helpers'; import { Component } from '../utils/helpers'; import { _getCurrentRunLoop } from '@ember/runloop'; @@ -159,7 +159,9 @@ 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)); + }, /`on` is deprecated/); }); } }, 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..1cd3e989d3d 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,11 @@ -import { moduleFor, RenderingTestCase, applyMixins, strip, runTask } from 'internal-test-helpers'; +import { + moduleFor, + RenderingTestCase, + applyMixins, + strip, + runTask, + expectDeprecation, +} from 'internal-test-helpers'; import { notifyPropertyChange, on } from '@ember/-internals/metal'; import { get, set, computed } from '@ember/object'; @@ -1121,11 +1128,14 @@ moduleFor( class extends EachTest { createList(items) { let wrapped = emberA(items); - let proxy = ArrayProxy.extend({ - setup: on('init', function () { - this.set('content', emberA(wrapped)); - }), - }).create(); + let proxy; + expectDeprecation(() => { + proxy = ArrayProxy.extend({ + setup: on('init', function () { + this.set('content', emberA(wrapped)); + }), + }).create(); + }, /`on` is deprecated/); return { list: proxy, delegate: wrapped }; } 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..cec8469f0f0 100644 --- a/packages/@ember/-internals/metal/lib/events.ts +++ b/packages/@ember/-internals/metal/lib/events.ts @@ -5,7 +5,7 @@ import type { Meta } from '@ember/-internals/meta'; 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 { assert, deprecate } from '@ember/debug'; /* The event system uses a series of nested hashes to store listeners on an @@ -38,11 +38,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 +65,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 +92,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', @@ -199,6 +226,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 +234,17 @@ export function hasListeners(obj: object, eventName: string): boolean { @public */ export function on(...args: [...eventNames: string[], func: T]): T { + deprecate( + '`on` is deprecated. Use native JavaScript events or a dedicated event library instead.', + false, + { + for: 'ember-source', + id: 'ember-evented', + since: { available: '6.8.0' }, + until: '7.0.0', + } + ); + let func = args.pop(); let events = args as string[]; diff --git a/packages/@ember/-internals/metal/tests/events_test.js b/packages/@ember/-internals/metal/tests/events_test.js index 109792c39b7..257e1b66a50 100644 --- a/packages/@ember/-internals/metal/tests/events_test.js +++ b/packages/@ember/-internals/metal/tests/events_test.js @@ -1,6 +1,6 @@ import { on, addListener, removeListener, sendEvent, hasListeners } from '..'; import Mixin from '@ember/object/mixin'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, expectDeprecation } from 'internal-test-helpers'; moduleFor( 'system/props/events_test', @@ -141,15 +141,18 @@ moduleFor( ['@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` is deprecated/); let obj = {}; MyMixin.apply(obj); @@ -160,30 +163,41 @@ moduleFor( [`@test 'on' asserts for invalid arguments`]() { expectAssertion(() => { - Mixin.create({ - foo1: on('bar'), - }); + expectDeprecation(() => { + Mixin.create({ + foo1: on('bar'), + }); + }, /`on` is deprecated/); }, 'on expects function as last argument'); expectAssertion(() => { - Mixin.create({ - foo1: on(function () {}), - }); + expectDeprecation(() => { + Mixin.create({ + foo1: on(function () {}), + }); + }, /`on` is deprecated/); }, 'on called without valid event names'); } ['@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` is deprecated/); + + let SecondMixin; + expectDeprecation(() => { + SecondMixin = Mixin.create({ + foo: on('baz', function () { + triggered++; + }), + }); + }, /`on` is deprecated/); let obj = {}; FirstMixin.apply(obj); diff --git a/packages/@ember/-internals/runtime/index.ts b/packages/@ember/-internals/runtime/index.ts index ec028a7a2da..947400434bb 100644 --- a/packages/@ember/-internals/runtime/index.ts +++ b/packages/@ember/-internals/runtime/index.ts @@ -5,5 +5,6 @@ export { default as ActionHandler } from './lib/mixins/action_handler'; export { default as _ProxyMixin, contentFor as _contentFor } from './lib/mixins/-proxy'; export { default as MutableEnumerable } from '@ember/enumerable/mutable'; export { default as TargetActionSupport } from './lib/mixins/target_action_support'; +export { default as Evented, on } from './lib/mixins/evented'; export { default as RSVP, onerrorDefault } from './lib/ext/rsvp'; // just for side effect of extending Ember.RSVP diff --git a/packages/@ember/-internals/runtime/lib/mixins/evented.ts b/packages/@ember/-internals/runtime/lib/mixins/evented.ts new file mode 100644 index 00000000000..701364837e1 --- /dev/null +++ b/packages/@ember/-internals/runtime/lib/mixins/evented.ts @@ -0,0 +1,127 @@ +import { addListener, removeListener, hasListeners, sendEvent } from '@ember/-internals/metal'; +import { deprecate } from '@ember/debug'; +import Mixin from '@ember/object/mixin'; + +export { on } from '@ember/-internals/metal'; + +/** + @module @ember/-internals/runtime +*/ + +/** + Internal implementation of the Evented mixin. + This mixin allows for Ember objects to subscribe to and emit events. + + @private + @deprecated + */ +interface Evented { + /** @deprecated */ + on( + name: string, + target: Target, + method: string | ((this: Target, ...args: any[]) => void) + ): this; + on(name: string, method: ((...args: any[]) => void) | string): this; + + /** @deprecated */ + one( + name: string, + target: Target, + method: string | ((this: Target, ...args: any[]) => void) + ): this; + one(name: string, method: string | ((...args: any[]) => void)): this; + + /** @deprecated */ + trigger(name: string, ...args: any[]): any; + + /** @deprecated */ + off( + name: string, + target: Target, + method: string | ((this: Target, ...args: any[]) => void) + ): this; + off(name: string, method: string | ((...args: any[]) => void)): this; + + /** @deprecated */ + has(name: string): boolean; +} + +const Evented = Mixin.create({ + on(name: string, target: object, method?: string | Function) { + deprecate( + '`on` is deprecated. Use native JavaScript events or a dedicated event library instead.', + false, + { + for: 'ember-source', + id: 'ember-evented', + since: { available: '6.8.0' }, + until: '7.0.0', + } + ); + // SAFETY: The types are not actaully 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; + }, + + one(name: string, target: object, method?: string | Function) { + deprecate( + '`one` is deprecated. Use native JavaScript events or a dedicated event library instead.', + false, + { + for: 'ember-source', + id: 'ember-evented', + since: { available: '6.8.0' }, + until: '7.0.0', + } + ); + // SAFETY: The types are not actaully 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; + }, + + trigger(name: string, ...args: any[]) { + deprecate( + '`trigger` is deprecated. Use native JavaScript events or a dedicated event library instead.', + false, + { + for: 'ember-source', + id: 'ember-evented', + since: { available: '6.8.0' }, + until: '7.0.0', + } + ); + sendEvent(this, name, args); + }, + + off(name: string, target: object, method?: string | Function) { + deprecate( + '`off` is deprecated. Use native JavaScript events or a dedicated event library instead.', + false, + { + for: 'ember-source', + id: 'ember-evented', + since: { available: '6.8.0' }, + until: '7.0.0', + } + ); + removeListener(this, name, target, method); + return this; + }, + + has(name: string) { + deprecate( + '`has` is deprecated. Use native JavaScript events or a dedicated event library instead.', + false, + { + for: 'ember-source', + id: 'ember-evented', + since: { available: '6.8.0' }, + until: '7.0.0', + } + ); + return hasListeners(this, name); + }, +}); + +export default Evented; 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..adf0f084c46 --- /dev/null +++ b/packages/@ember/-internals/utils/lib/mixin-deprecation.ts @@ -0,0 +1,42 @@ +import type { DeprecationOptions } from '@ember/debug'; +import type Mixin from '@ember/object/mixin'; + +/** @internal */ +export const DEPRECATION = Symbol('DEPRECATION'); + +/** @internal */ +export function setDeprecation( + mixin: Mixin, + value: { message: string; options: DeprecationOptions } | null +) { + mixin[DEPRECATION] = value; +} + +let deprecationsEnabled = true; + +export function findDeprecation( + mixin: Mixin +): { message: string; options: DeprecationOptions } | null { + if (!deprecationsEnabled) { + return null; + } + if (mixin[DEPRECATION]) { + return mixin[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 71a23252628..9aa11faa59f 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 ActionManager } from './lib/system/action_manager'; 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 c6a7529cf44..16469aa6ecd 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/lib/renderer'; 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/lib/mixin-deprecation'; /** `Ember.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..adf94249edc 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 { setDeprecation } from '@ember/-internals/utils/lib/mixin-deprecation'; +import { deprecate } from '@ember/debug'; 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,88 @@ interface Evented { } const Evented = Mixin.create({ on(name: string, target: object, method?: string | Function) { + deprecate( + '`on` is deprecated. Use native JavaScript events or a dedicated event library instead.', + false, + { + for: 'ember-source', + id: 'ember-evented', + since: { available: '6.8.0' }, + until: '7.0.0', + } + ); addListener(this, name, target, method); return this; }, one(name: string, target: object, method?: string | Function) { + deprecate( + '`one` is deprecated. Use native JavaScript events or a dedicated event library instead.', + false, + { + for: 'ember-source', + id: 'ember-evented', + since: { available: '6.8.0' }, + until: '7.0.0', + } + ); addListener(this, name, target, method, true); return this; }, trigger(name: string, ...args: any[]) { + deprecate( + '`trigger` is deprecated. Use native JavaScript events or a dedicated event library instead.', + false, + { + for: 'ember-source', + id: 'ember-evented', + since: { available: '6.8.0' }, + until: '7.0.0', + } + ); sendEvent(this, name, args); }, off(name: string, target: object, method?: string | Function) { + deprecate( + '`off` is deprecated. Use native JavaScript events or a dedicated event library instead.', + false, + { + for: 'ember-source', + id: 'ember-evented', + since: { available: '6.8.0' }, + until: '7.0.0', + } + ); removeListener(this, name, target, method); return this; }, has(name: string) { + deprecate( + '`has` is deprecated. Use native JavaScript events or a dedicated event library instead.', + false, + { + for: 'ember-source', + id: 'ember-evented', + since: { available: '6.8.0' }, + until: '7.0.0', + } + ); return hasListeners(this, name); }, }); +setDeprecation(Evented, { + message: + 'Evented mixin is deprecated. Use native JavaScript events or a dedicated event library instead.', + options: { + for: 'ember-source', + id: 'ember-evented', + since: { available: '6.8.0' }, + until: '7.0.0', + }, +}); + export default Evented; diff --git a/packages/@ember/object/events.ts b/packages/@ember/object/events.ts index 3f4559266fc..28c8429d419 100644 --- a/packages/@ember/object/events.ts +++ b/packages/@ember/object/events.ts @@ -1 +1,39 @@ -export { addListener, removeListener, sendEvent } from '@ember/-internals/metal'; +import { + addListener as originalAddListener, + removeListener as originalRemoveListener, + sendEvent as originalSendEvent, +} from '@ember/-internals/metal'; +import { deprecateFunc } from '@ember/debug'; + +export const addListener = deprecateFunc( + '`addListener is deprecated', + { + for: 'ember-source', + id: 'ember-object-add-listener', + since: { available: '6.8.0' }, + until: '7.0.0', + }, + originalAddListener +); + +export const removeListener = deprecateFunc( + '`removeListener is deprecated', + { + for: 'ember-source', + id: 'ember-object-remove-listener', + since: { available: '6.8.0' }, + until: '7.0.0', + }, + originalRemoveListener +); + +export const sendEvent = deprecateFunc( + '`sendEvent is deprecated', + { + for: 'ember-source', + id: 'ember-object-send-event', + since: { available: '6.8.0' }, + until: '7.0.0', + }, + originalSendEvent +); diff --git a/packages/@ember/object/mixin.ts b/packages/@ember/object/mixin.ts index 32b550c2cf9..f169beab847 100644 --- a/packages/@ember/object/mixin.ts +++ b/packages/@ember/object/mixin.ts @@ -5,7 +5,7 @@ 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 { assert } from '@ember/debug'; +import { assert, deprecate, DeprecationOptions } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; import { type ComputedDecorator, @@ -14,6 +14,8 @@ import { type ComputedPropertySetter, type ComputedDescriptor, isClassicDecorator, + addListener, + removeListener, } from '@ember/-internals/metal'; import { ComputedProperty, @@ -27,7 +29,7 @@ import { defineDecorator, defineValue, } from '@ember/-internals/metal'; -import { addListener, removeListener } from '@ember/object/events'; +import { DEPRECATION, findDeprecation } from '@ember/-internals/utils/lib/mixin-deprecation'; const a_concat = Array.prototype.concat; const { isArray } = Array; @@ -543,6 +545,9 @@ export default class Mixin { /** @internal */ properties: { [key: string]: any } | undefined; + /*** @internal */ + [DEPRECATION]: { message: string; options: DeprecationOptions } | null = null; + /** @internal */ ownerConstructor: any; @@ -621,6 +626,13 @@ export default class Mixin { return this; } + if (DEBUG) { + for (let mixin of args) { + let deprecation = mixin instanceof Mixin ? findDeprecation(mixin) : null; + deprecate(deprecation?.message ?? 'Huh???', !deprecation, deprecation?.options); + } + } + 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..b4f9bb140d6 100644 --- a/packages/@ember/object/tests/es-compatibility-test.js +++ b/packages/@ember/object/tests/es-compatibility-test.js @@ -9,7 +9,12 @@ 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, +} from 'internal-test-helpers'; moduleFor( 'EmberObject ES Compatibility', @@ -283,30 +288,35 @@ moduleFor( 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` is deprecated/); 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..5da69ae1001 100644 --- a/packages/@ember/object/tests/evented_test.js +++ b/packages/@ember/object/tests/evented_test.js @@ -1,21 +1,29 @@ import CoreObject from '@ember/object/core'; import EventedMixin from '@ember/object/evented'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { moduleFor, AbstractTestCase, expectDeprecation } from 'internal-test-helpers'; 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(); + let eventedProxyObj; + expectDeprecation(() => { + eventedProxyObj = class extends CoreObject.extend(EventedMixin) { + unknownProperty() { + return true; + } + }.create(); + }, /Evented mixin is deprecated/); let noop = function () {}; - eventedProxyObj.on('foo', noop); - eventedProxyObj.off('foo', noop); + expectDeprecation(() => { + eventedProxyObj.on('foo', noop); + }, /`on` is deprecated/); + + expectDeprecation(() => { + eventedProxyObj.off('foo', noop); + }, /`off` is deprecated/); 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..5745c9a5f26 100644 --- a/packages/@ember/object/tests/events_test.js +++ b/packages/@ember/object/tests/events_test.js @@ -1,6 +1,6 @@ import EmberObject from '@ember/object'; -import Evented from '@ember/object/evented'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import { Evented } from '@ember/-internals/runtime'; +import { moduleFor, AbstractTestCase, expectDeprecation } from 'internal-test-helpers'; moduleFor( 'Object events', @@ -13,12 +13,18 @@ moduleFor( let obj = EmberObject.extend(Evented).create(); - obj.on('event!', F); - obj.trigger('event!'); + expectDeprecation(() => { + obj.on('event!', F); + }, /`on` is deprecated/); + expectDeprecation(() => { + obj.trigger('event!'); + }, /`trigger` is deprecated/); assert.equal(count, 1, 'the event was triggered'); - obj.trigger('event!'); + expectDeprecation(() => { + obj.trigger('event!'); + }, /`trigger` is deprecated/); assert.equal(count, 2, 'the event was triggered'); } @@ -33,12 +39,19 @@ moduleFor( let obj = EmberObject.extend(Evented).create(); - obj.one('event!', F); - obj.trigger('event!'); + expectDeprecation(() => { + obj.one('event!', F); + }, /`one` is deprecated/); + + expectDeprecation(() => { + obj.trigger('event!'); + }, /`trigger` is deprecated/); assert.equal(count, 1, 'the event was triggered'); - obj.trigger('event!'); + expectDeprecation(() => { + obj.trigger('event!'); + }, /`trigger` is deprecated/); assert.equal(count, 1, 'the event was not triggered again'); } @@ -48,12 +61,16 @@ moduleFor( let obj = EmberObject.extend(Evented).create(); - obj.on('event!', function () { - args = [].slice.call(arguments); - self = this; - }); + expectDeprecation(() => { + obj.on('event!', function () { + args = [].slice.call(arguments); + self = this; + }); + }, /`on` is deprecated/); - obj.trigger('event!', 'foo', 'bar'); + expectDeprecation(() => { + obj.trigger('event!', 'foo', 'bar'); + }, /`trigger` is deprecated/); assert.deepEqual(args, ['foo', 'bar']); assert.equal(self, obj); @@ -65,19 +82,25 @@ moduleFor( let obj = EmberObject.extend(Evented).create(); - obj.one('event!', function () { - args = [].slice.call(arguments); - self = this; - count++; - }); + expectDeprecation(() => { + obj.one('event!', function () { + args = [].slice.call(arguments); + self = this; + count++; + }); + }, /`one` is deprecated/); - obj.trigger('event!', 'foo', 'bar'); + expectDeprecation(() => { + obj.trigger('event!', 'foo', 'bar'); + }, /`trigger` is deprecated/); 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'); + }, /`trigger` is deprecated/); assert.deepEqual(args, ['foo', 'bar']); assert.equal(count, 1, 'the event was not triggered again'); @@ -90,12 +113,16 @@ moduleFor( let obj = EmberObject.extend(Evented).create(); let target = {}; - obj.on('event!', target, function () { - args = [].slice.call(arguments); - self = this; - }); + expectDeprecation(() => { + obj.on('event!', target, function () { + args = [].slice.call(arguments); + self = this; + }); + }, /`on` is deprecated/); - obj.trigger('event!', 'foo', 'bar'); + expectDeprecation(() => { + obj.trigger('event!', 'foo', 'bar'); + }, /`trigger` is deprecated/); assert.deepEqual(args, ['foo', 'bar']); assert.equal(self, target); @@ -112,12 +139,19 @@ moduleFor( let obj = EmberObject.extend(Evented).create(); - obj.one('event!', target, 'fn'); - obj.trigger('event!'); + expectDeprecation(() => { + obj.one('event!', target, 'fn'); + }, /`one` is deprecated/); + + expectDeprecation(() => { + obj.trigger('event!'); + }, /`trigger` is deprecated/); assert.equal(count, 1, 'the event was triggered'); - obj.trigger('event!'); + expectDeprecation(() => { + obj.trigger('event!'); + }, /`trigger` is deprecated/); assert.equal(count, 1, 'the event was not triggered again'); } @@ -128,28 +162,54 @@ moduleFor( }.create(); let F = function () {}; - obj.one('event!', F); - obj.one('event!', obj, 'F'); + expectDeprecation(() => { + obj.one('event!', F); + }, /`one` is deprecated/); + + expectDeprecation(() => { + obj.one('event!', obj, 'F'); + }, /`one` is deprecated/); - assert.equal(obj.has('event!'), true, 'has events'); + let objHas; + expectDeprecation(() => { + objHas = obj.has('event!'); + }, /`has` is deprecated/); + assert.equal(objHas, true, 'has events'); - obj.off('event!', F); - obj.off('event!', obj, 'F'); + expectDeprecation(() => { + obj.off('event!', F); + }, /`off` is deprecated/); - assert.equal(obj.has('event!'), false, 'has no more events'); + expectDeprecation(() => { + obj.off('event!', obj, 'F'); + }, /`off` is deprecated/); + + expectDeprecation(() => { + objHas = obj.has('event!'); + }, /`has` is deprecated/); + + assert.equal(objHas, false, 'has no more events'); } ['@test adding and removing listeners should be chainable'](assert) { let obj = EmberObject.extend(Evented).create(); let F = function () {}; - let ret = obj.on('event!', F); + let ret; + + expectDeprecation(() => { + ret = obj.on('event!', F); + }, /`on` is deprecated/); assert.equal(ret, obj, '#on returns self'); - ret = obj.off('event!', F); + expectDeprecation(() => { + ret = obj.off('event!', F); + }, /`off` is deprecated/); assert.equal(ret, obj, '#off returns self'); - ret = obj.one('event!', F); + expectDeprecation(() => { + ret = obj.one('event!', F); + }, /`one` is deprecated/); assert.equal(ret, obj, '#one returns self'); } } diff --git a/packages/@ember/routing/route.ts b/packages/@ember/routing/route.ts index 7b4a8f24eac..df6dcf8e7a9 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/lib/mixin-deprecation'; 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..20457727e76 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,106 @@ 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: 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 actaully 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 actaully 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 + */ + trigger(name: string, ...args: any[]) { + 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 actaully 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..fb8a0302a2f 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/lib/mixin-deprecation'; /** @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/tests/routing/decoupled_basic_test.js b/packages/ember/tests/routing/decoupled_basic_test.js index a71b245d919..ae4ca4c4daf 100644 --- a/packages/ember/tests/routing/decoupled_basic_test.js +++ b/packages/ember/tests/routing/decoupled_basic_test.js @@ -13,6 +13,7 @@ import { ModuleBasedTestResolver, runDestroy, runTask, + expectDeprecation, } from 'internal-test-helpers'; import { run } from '@ember/runloop'; import { addObserver } from '@ember/-internals/metal'; @@ -879,7 +880,7 @@ moduleFor( } ['@test `activate` event fires on the route'](assert) { - assert.expect(4); + assert.expect(5); let eventFired = 0; @@ -893,10 +894,12 @@ 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'); + }); + }, /`on` is deprecated/); } activate(transition) { @@ -910,7 +913,7 @@ moduleFor( } ['@test `deactivate` event fires on the route'](assert) { - assert.expect(4); + assert.expect(5); let eventFired = 0; @@ -925,10 +928,12 @@ 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'); + }); + }, /`on` is deprecated/); } deactivate(transition) {