diff --git a/package.json b/package.json index dbe510a6724..52ed8c88fb0 100644 --- a/package.json +++ b/package.json @@ -222,6 +222,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 9625d864bc5..0b0c42429ce 100644 --- a/packages/@ember/-internals/deprecations/index.ts +++ b/packages/@ember/-internals/deprecations/index.ts @@ -126,6 +126,13 @@ export const DEPRECATIONS = { until: '8.0.0', url: 'https://deprecations.emberjs.com/id/deprecate-target-action-support', }), + 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..fd5a5030aa4 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,11 @@ -import { moduleFor, RenderingTestCase, strip, runTask } from 'internal-test-helpers'; +import { + moduleFor, + RenderingTestCase, + strip, + runTask, + expectDeprecation, +} from 'internal-test-helpers'; +import { DEPRECATIONS } from '../../../../deprecations'; import { set } from '@ember/object'; import { setComponentTemplate } from '@glimmer/manager'; @@ -57,7 +64,15 @@ class AbstractAppendTest extends RenderingTestCase { } componentsByName[name] = this; pushHook('init'); - this.on('init', () => pushHook('on(init)')); + if (!DEPRECATIONS.DEPRECATE_EVENTED.isRemoved) { + expectDeprecation( + () => { + this.on('init', () => pushHook('on(init)')); + }, + /Evented#on` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); + } } didReceiveAttrs() { @@ -145,26 +160,28 @@ class AbstractAppendTest extends RenderingTestCase { { parentValue: 1, childValue: 1, foo: 'zomg', show: true } ); - assert.deepEqual( - hooks, - [ - ['x-parent', 'init'], - ['x-parent', 'on(init)'], - ['x-parent', 'didReceiveAttrs'], - ['x-parent', 'willRender'], - ['x-parent', 'willInsertElement'], - ['x-child', 'init'], - ['x-child', 'on(init)'], - ['x-child', 'didReceiveAttrs'], - ['x-child', 'willRender'], - ['x-child', 'willInsertElement'], - ['x-child', 'didInsertElement'], - ['x-child', 'didRender'], - ['x-parent', 'didInsertElement'], - ['x-parent', 'didRender'], - ], - 'creation of x-parent' - ); + let expectedCreationHooks = [ + ['x-parent', 'init'], + ['x-parent', 'on(init)'], + ['x-parent', 'didReceiveAttrs'], + ['x-parent', 'willRender'], + ['x-parent', 'willInsertElement'], + ['x-child', 'init'], + ['x-child', 'on(init)'], + ['x-child', 'didReceiveAttrs'], + ['x-child', 'willRender'], + ['x-child', 'willInsertElement'], + ['x-child', 'didInsertElement'], + ['x-child', 'didRender'], + ['x-parent', 'didInsertElement'], + ['x-parent', 'didRender'], + ]; + + if (DEPRECATIONS.DEPRECATE_EVENTED.isRemoved) { + expectedCreationHooks = expectedCreationHooks.filter(([, hook]) => hook !== 'on(init)'); + } + + assert.deepEqual(hooks, expectedCreationHooks, 'creation of x-parent'); hooks.length = 0; runTask(() => this.rerender()); @@ -298,7 +315,15 @@ class AbstractAppendTest extends RenderingTestCase { } componentsByName[name] = this; pushHook('init'); - this.on('init', () => pushHook('on(init)')); + if (!DEPRECATIONS.DEPRECATE_EVENTED.isRemoved) { + expectDeprecation( + () => { + this.on('init', () => pushHook('on(init)')); + }, + /Evented#on` is deprecated/, + DEPRECATIONS.DEPRECATE_EVENTED.isEnabled + ); + } } didReceiveAttrs() { @@ -381,15 +406,18 @@ class AbstractAppendTest extends RenderingTestCase { XParent = this.owner.factoryFor('component:x-parent'); this.component = XParent.create({ foo: 'zomg' }); - - assert.deepEqual( - hooks, - [ - ['x-parent', 'init'], - ['x-parent', 'on(init)'], - ], - 'creation of x-parent' - ); + if (DEPRECATIONS.DEPRECATE_EVENTED.isRemoved) { + assert.deepEqual(hooks, [['x-parent', 'init']], 'creation of x-parent'); + } else { + assert.deepEqual( + hooks, + [ + ['x-parent', 'init'], + ['x-parent', 'on(init)'], + ], + 'creation of x-parent' + ); + } hooks.length = 0; @@ -401,7 +429,7 @@ class AbstractAppendTest extends RenderingTestCase { ['x-parent', 'willInsertElement'], ['x-child', 'init'], - ['x-child', 'on(init)'], + !DEPRECATIONS.DEPRECATE_EVENTED.isRemoved && ['x-child', 'on(init)'], ['x-child', 'didReceiveAttrs'], ['x-child', 'willRender'], ['x-child', 'willInsertElement'], @@ -411,7 +439,7 @@ class AbstractAppendTest extends RenderingTestCase { ['x-parent', 'didInsertElement'], ['x-parent', 'didRender'], - ], + ].filter(Boolean), 'appending of x-parent' ); 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..ac02dd5df23 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/syntax/each-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/syntax/each-test.js @@ -1,6 +1,6 @@ import { moduleFor, RenderingTestCase, applyMixins, strip, runTask } from 'internal-test-helpers'; -import { notifyPropertyChange, on } from '@ember/-internals/metal'; +import { notifyPropertyChange } from '@ember/-internals/metal'; import { get, set, computed } from '@ember/object'; import { A as emberA } from '@ember/array'; import ArrayProxy from '@ember/array/proxy'; @@ -1115,9 +1115,11 @@ moduleFor( createList(items) { let wrapped = emberA(items); let proxy = ArrayProxy.extend({ - setup: on('init', function () { + init: function () { + this._super(...arguments); + this.set('content', emberA(wrapped)); - }), + }, }).create(); return { list: proxy, delegate: wrapped }; diff --git a/packages/@ember/-internals/metal/lib/evented-methods.ts b/packages/@ember/-internals/metal/lib/evented-methods.ts new file mode 100644 index 00000000000..59643a98a58 --- /dev/null +++ b/packages/@ember/-internals/metal/lib/evented-methods.ts @@ -0,0 +1,64 @@ +import { addListener, removeListener, hasListeners, sendEvent } from './events'; +import { DEPRECATIONS, deprecateUntil } from '@ember/-internals/deprecations'; + +/* + Standalone implementations of the deprecated `Evented` methods. They are + shared between the `Evented` mixin and the framework classes (`Component`, + `Route`, `EmberRouter`) that historically included it, so that the framework + itself no longer applies the deprecated mixin. +*/ + +export function eventedOn( + obj: object, + name: string, + target: object | Function | null, + method?: Function | PropertyKey +): void { + deprecateUntil( + '`Evented#on` is deprecated. Use native JavaScript events or a dedicated event library instead.', + DEPRECATIONS.DEPRECATE_EVENTED + ); + addListener(obj, name, target, method); +} + +export function eventedOne( + obj: object, + name: string, + target: object | Function | null, + method?: Function | PropertyKey +): void { + deprecateUntil( + '`Evented#one` is deprecated. Use native JavaScript events or a dedicated event library instead.', + DEPRECATIONS.DEPRECATE_EVENTED + ); + addListener(obj, name, target, method, true); +} + +export function eventedTrigger(obj: object, name: string, args: any[]): void { + deprecateUntil( + '`Evented#trigger` is deprecated. Use native JavaScript events or a dedicated event library instead.', + DEPRECATIONS.DEPRECATE_EVENTED + ); + sendEvent(obj, name, args); +} + +export function eventedOff( + obj: object, + name: string, + target: object | Function | null, + method?: string | Function +): void { + deprecateUntil( + '`Evented#off` is deprecated. Use native JavaScript events or a dedicated event library instead.', + DEPRECATIONS.DEPRECATE_EVENTED + ); + removeListener(obj, name, target, method); +} + +export function eventedHas(obj: object, name: string): boolean { + deprecateUntil( + '`Evented#has` is deprecated. Use native JavaScript events or a dedicated event library instead.', + DEPRECATIONS.DEPRECATE_EVENTED + ); + return hasListeners(obj, name); +} diff --git a/packages/@ember/-internals/metal/lib/events.ts b/packages/@ember/-internals/metal/lib/events.ts index 56668b9bc9b..106cfbea16f 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 @@ -199,6 +200,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 +208,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/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/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..2f0e757da06 100644 --- a/packages/@ember/-internals/views/lib/views/core_view.ts +++ b/packages/@ember/-internals/views/lib/views/core_view.ts @@ -1,5 +1,13 @@ import type { Renderer, View } from '@ember/-internals/glimmer/lib/renderer'; +import { meta as metaFor } from '@ember/-internals/meta/lib/meta'; import inject from '@ember/-internals/metal/lib/injected_property'; +import { + eventedOn, + eventedOne, + eventedTrigger, + eventedOff, + eventedHas, +} from '@ember/-internals/metal/lib/evented-methods'; import ActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler'; import Evented from '@ember/object/evented'; import { FrameworkObject } from '@ember/object/-internals'; @@ -23,8 +31,14 @@ import states from './states'; @private */ -interface CoreView extends Evented, ActionHandler, View {} -class CoreView extends FrameworkObject.extend(Evented, ActionHandler) { +interface CoreView extends ActionHandler, View {} +class CoreView extends FrameworkObject.extend(ActionHandler) { + static { + // The deprecated Evented mixin is no longer applied, but instances still + // provide its methods, so `Evented.detect` must keep returning true. + metaFor(this.prototype).addMixin(Evented); + } + isView = true; declare _states: typeof states; @@ -32,9 +46,6 @@ class CoreView extends FrameworkObject.extend(Evented, ActionHandler) { declare _state: keyof typeof states; declare _currentState: ViewState; - _superTrigger?: Evented['trigger']; - _superHas?: Evented['has']; - /** If the view is currently inserted into the DOM of a parent view, this property will point to the parent of the view. @@ -49,15 +60,6 @@ class CoreView extends FrameworkObject.extend(Evented, ActionHandler) { init(properties: object | undefined) { super.init(properties); - // Handle methods from Evented - // The native class inheritance will not work for mixins. To work around this, - // we copy the existing trigger and has methods provided by the mixin and swap in the - // new ones from our class. - this._superTrigger = this.trigger; - this.trigger = this._trigger; - this._superHas = this.has; - this.has = this._has; - this.parentView ??= null; this._state = 'preRender'; @@ -74,6 +76,28 @@ class CoreView extends FrameworkObject.extend(Evented, ActionHandler) { return hash; } + on( + name: string, + target: Target, + method: string | ((this: Target, ...args: any[]) => void) + ): this; + on(name: string, method: ((...args: any[]) => void) | string): this; + on(name: string, target: any, method?: any) { + eventedOn(this, name, target, method); + return this; + } + + one( + name: string, + target: Target, + method: string | ((this: Target, ...args: any[]) => void) + ): this; + one(name: string, method: string | ((...args: any[]) => void)): this; + one(name: string, target: any, method?: any) { + eventedOne(this, name, target, method); + return this; + } + /** Override the default event firing from `Evented` to also call methods with the given name. @@ -82,18 +106,27 @@ class CoreView extends FrameworkObject.extend(Evented, ActionHandler) { @param name {String} @private */ - // Changed to `trigger` on init - _trigger(name: string, ...args: any[]) { - this._superTrigger!(name, ...args); + trigger(name: string, ...args: any[]) { + eventedTrigger(this, name, args); let method = (this as any)[name]; if (typeof method === 'function') { return method.apply(this, args); } } - // Changed to `has` on init - _has(name: string) { - return typeof (this as any)[name] === 'function' || this._superHas!(name); + 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: any, method?: any) { + eventedOff(this, name, target, method); + return this; + } + + has(name: string) { + return typeof (this as any)[name] === 'function' || eventedHas(this, name); } static isViewFactory = true; 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..a52280a8fd0 100644 --- a/packages/@ember/object/evented.ts +++ b/packages/@ember/object/evented.ts @@ -1,9 +1,10 @@ import { - addListener, - removeListener, - hasListeners, - sendEvent, -} from '@ember/-internals/metal/lib/events'; + eventedOn, + eventedOne, + eventedTrigger, + eventedOff, + eventedHas, +} from '@ember/-internals/metal/lib/evented-methods'; import Mixin from '@ember/object/mixin'; export { on } from '@ember/-internals/metal/lib/events'; @@ -51,6 +52,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 +70,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 +93,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 +122,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 +132,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 +149,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,26 +158,26 @@ interface Evented { } const Evented = Mixin.create({ on(name: string, target: object, method?: string | Function) { - addListener(this, name, target, method); + eventedOn(this, name, target, method); return this; }, one(name: string, target: object, method?: string | Function) { - addListener(this, name, target, method, true); + eventedOne(this, name, target, method); return this; }, trigger(name: string, ...args: any[]) { - sendEvent(this, name, args); + eventedTrigger(this, name, args); }, off(name: string, target: object, method?: string | Function) { - removeListener(this, name, target, method); + eventedOff(this, name, target, method); return this; }, has(name: string) { - return hasListeners(this, name); + return eventedHas(this, name); }, }); diff --git a/packages/@ember/object/events.ts b/packages/@ember/object/events.ts index 66224dac13f..9189f61882b 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 object | Function | null, + method, + 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 object | Function | null, + method as string | Function + ); +} + +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..6509f6c0b6a 100644 --- a/packages/@ember/object/mixin.ts +++ b/packages/@ember/object/mixin.ts @@ -27,7 +27,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; 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..6e2831d95da 100644 --- a/packages/@ember/object/tests/evented_test.js +++ b/packages/@ember/object/tests/evented_test.js @@ -1,12 +1,37 @@ import CoreObject from '@ember/object/core'; +import EmberObject from '@ember/object'; import EventedMixin from '@ember/object/evented'; -import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; +import Component from '@ember/component'; +import Route from '@ember/routing/route'; +import EmberRouter from '@ember/routing/router'; +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) { + [`${testUnless( + DEPRECATIONS.DEPRECATE_EVENTED.isRemoved + )} @test Evented.detect returns true for framework classes that provide the Evented methods`]( + assert + ) { + assert.true(EventedMixin.detect(Component.prototype), 'Component'); + assert.true(EventedMixin.detect(Route.prototype), 'Route'); + assert.true(EventedMixin.detect(EmberRouter.prototype), 'EmberRouter'); + assert.true(EventedMixin.detect(class extends Route {}.prototype), 'Route subclass'); + + let route = Route.create(); + assert.true(EventedMixin.detect(route), 'Route instance'); + route.destroy(); + + assert.false(EventedMixin.detect(EmberObject.prototype), 'EmberObject'); + } + + [`${testUnless( + DEPRECATIONS.DEPRECATE_EVENTED.isRemoved + )} @test works properly on proxy-ish objects`](assert) { + let eventedProxyObj; + eventedProxyObj = class extends CoreObject.extend(EventedMixin) { unknownProperty() { return true; } @@ -14,8 +39,21 @@ moduleFor( 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..c9c600060c6 100644 --- a/packages/@ember/object/tests/events_test.js +++ b/packages/@ember/object/tests/events_test.js @@ -1,29 +1,53 @@ 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; + obj = EmberObject.extend(Evented).create(); + + 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 +55,150 @@ moduleFor( count++; }; - let obj = EmberObject.extend(Evented).create(); - - obj.one('event!', F); - obj.trigger('event!'); + let obj; + obj = EmberObject.extend(Evented).create(); + + 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; + obj = EmberObject.extend(Evented).create(); + + 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; + obj = EmberObject.extend(Evented).create(); + + 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; + obj = EmberObject.extend(Evented).create(); 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 +207,134 @@ moduleFor( count++; }; - let obj = EmberObject.extend(Evented).create(); - - obj.one('event!', target, 'fn'); - obj.trigger('event!'); + let obj; + obj = EmberObject.extend(Evented).create(); + + 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) { + [`${testUnless( + DEPRECATIONS.DEPRECATE_EVENTED.isRemoved + )} @test a listener registered with one can be removed with off`](assert) { + let obj; + obj = class extends EmberObject.extend(Evented) { F() {} }.create(); 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; + obj = EmberObject.extend(Evented).create(); 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 4e05176c34e..dd553d3a16e 100644 --- a/packages/@ember/routing/route.ts +++ b/packages/@ember/routing/route.ts @@ -2,6 +2,14 @@ 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 { + eventedOn, + eventedOne, + eventedTrigger, + eventedOff, + eventedHas, +} from '@ember/-internals/metal/lib/evented-methods'; import type Owner from '@ember/owner'; import { getOwner } from '@ember/-internals/owner'; import type { default as BucketCache } from './lib/cache'; @@ -12,6 +20,7 @@ import getProperties from '@ember/-internals/metal/lib/get_properties'; import setProperties from '@ember/-internals/metal/lib/set_properties'; import EmberObject from '@ember/object'; import Evented from '@ember/object/evented'; +import { meta as metaFor } from '@ember/-internals/meta/lib/meta'; import { A as emberA } from '@ember/array'; import ActionHandler from '@ember/-internals/runtime/lib/mixins/action_handler'; import typeOf from '@ember/utils/lib/type-of'; @@ -82,7 +91,7 @@ const RENDER_STATE = Symbol('render-state'); @since 1.0.0 @public */ -interface Route extends IRoute, ActionHandler, Evented { +interface Route extends IRoute, ActionHandler { /** The `willTransition` action is fired at the beginning of any attempted transition with a `Transition` object as the sole @@ -256,7 +265,13 @@ 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 EmberObject.extend(ActionHandler) implements IRoute { + static { + // The deprecated Evented mixin is no longer applied, but instances still + // provide its methods, so `Evented.detect` must keep returning true. + metaFor(this.prototype).addMixin(Evented); + } + static isRouteFactory = true; // These properties will end up appearing in the public interface because we @@ -773,10 +788,51 @@ class Route extends EmberObject.extend(ActionHandler, Evented) */ exit(transition?: Transition) { this.deactivate(transition); - this.trigger('deactivate', transition); + sendEvent(this, 'deactivate', [transition]); this.teardownViews(); } + on( + name: string, + target: Target, + method: string | ((this: Target, ...args: any[]) => void) + ): this; + on(name: string, method: ((...args: any[]) => void) | string): this; + on(name: string, target: any, method?: any) { + eventedOn(this, name, target, method); + return this; + } + + one( + name: string, + target: Target, + method: string | ((this: Target, ...args: any[]) => void) + ): this; + one(name: string, method: string | ((...args: any[]) => void)): this; + one(name: string, target: any, method?: any) { + eventedOne(this, name, target, method); + return this; + } + + trigger(name: string, ...args: any[]): void { + eventedTrigger(this, name, args); + } + + 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: any, method?: any) { + eventedOff(this, name, target, method); + return this; + } + + has(name: string): boolean { + return eventedHas(this, name); + } + /** @private @@ -799,7 +855,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 d95bbc2ea4c..81645762229 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 as object | Function, 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 as object | Function, 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..a057ae2e5ea 100644 --- a/packages/@ember/routing/router.ts +++ b/packages/@ember/routing/router.ts @@ -27,9 +27,9 @@ import type { } from '@ember/routing/location'; import type RouterService from '@ember/routing/router-service'; import EmberObject from '@ember/object'; +import Evented from '@ember/object/evented'; import { A as emberA } from '@ember/array'; import typeOf from '@ember/utils/lib/type-of'; -import Evented from '@ember/object/evented'; import { assert, info } from '@ember/debug'; import { cancel, once, run, scheduleOnce } from '@ember/runloop'; import { DEBUG } from '@glimmer/env'; @@ -57,6 +57,15 @@ 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 { meta as metaFor } from '@ember/-internals/meta/lib/meta'; +import { + eventedOn, + eventedOne, + eventedTrigger, + eventedOff, + eventedHas, +} from '@ember/-internals/metal/lib/evented-methods'; /** @module @ember/routing/router @@ -142,7 +151,13 @@ const { slice } = Array.prototype; @uses Evented @public */ -class EmberRouter extends EmberObject.extend(Evented) implements Evented { +class EmberRouter extends EmberObject { + static { + // The deprecated Evented mixin is no longer applied, but instances still + // provide its methods, so `Evented.detect` must keep returning true. + metaFor(this.prototype).addMixin(Evented); + } + /** Represents the URL of the root of the application, often '/'. This prefix is assumed on all routes defined on this router. @@ -202,11 +217,46 @@ class EmberRouter extends EmberObject.extend(Evented) implements Evented { private namespace: any; // Begin Evented - declare on: (name: string, method: ((...args: any[]) => void) | string) => this; - declare one: (name: string, method: string | ((...args: any[]) => void)) => this; - declare trigger: (name: string, ...args: any[]) => unknown; - declare off: (name: string, method: string | ((...args: any[]) => void)) => this; - declare has: (name: string) => boolean; + on( + name: string, + target: Target, + method: string | ((this: Target, ...args: any[]) => void) + ): this; + on(name: string, method: ((...args: any[]) => void) | string): this; + on(name: string, target: any, method?: any) { + eventedOn(this, name, target, method); + return this; + } + + one( + name: string, + target: Target, + method: string | ((this: Target, ...args: any[]) => void) + ): this; + one(name: string, method: string | ((...args: any[]) => void)): this; + one(name: string, target: any, method?: any) { + eventedOne(this, name, target, method); + return this; + } + + trigger(name: string, ...args: any[]): void { + eventedTrigger(this, name, args); + } + + 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: any, method?: any) { + eventedOff(this, name, target, method); + return this; + } + + has(name: string): boolean { + return eventedHas(this, name); + } // End Evented // Set with reopenClass @@ -422,7 +472,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 +490,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..4b53915cb78 100644 --- a/packages/ember/tests/routing/decoupled_basic_test.js +++ b/packages/ember/tests/routing/decoupled_basic_test.js @@ -13,7 +13,9 @@ import { ModuleBasedTestResolver, runDestroy, runTask, + expectDeprecation, } 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 +890,12 @@ moduleFor( }); } - ['@test `activate` event fires on the route'](assert) { - assert.expect(4); + [`@test \`activate\` event fires on the route`](assert) { + if (DEPRECATIONS.DEPRECATE_EVENTED.isRemoved) { + assert.expect(2); + } else { + assert.expect(5); + } let eventFired = 0; @@ -903,10 +909,18 @@ 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'); - }); + if (!DEPRECATIONS.DEPRECATE_EVENTED.isRemoved) { + 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 +933,12 @@ moduleFor( return this.visit('/nork'); } - ['@test `deactivate` event fires on the route'](assert) { - assert.expect(4); + [`@test \`deactivate\` event fires on the route`](assert) { + if (DEPRECATIONS.DEPRECATE_EVENTED.isRemoved) { + assert.expect(2); + } else { + assert.expect(5); + } let eventFired = 0; @@ -935,10 +953,18 @@ 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'); - }); + if (!DEPRECATIONS.DEPRECATE_EVENTED.isRemoved) { + 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');