From 231ead48352dd15852d273147130b1e8bafed384 Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Fri, 20 Jun 2025 14:36:33 -0700 Subject: [PATCH 1/4] Add deprecation guide for Ember.Evented and @ember/object/events --- content/ember/v6/evented.md | 140 ++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 content/ember/v6/evented.md diff --git a/content/ember/v6/evented.md b/content/ember/v6/evented.md new file mode 100644 index 00000000..1310ba32 --- /dev/null +++ b/content/ember/v6/evented.md @@ -0,0 +1,140 @@ +--- +title: Ember.Evented and @ember/object/events +until: 7.0.0 +since: 6.6.0 +--- + +The `Ember.Evented` mixin, the underlying `@ember/object/events` module (`addListener`, `removeListener`, `sendEvent`), and the `on()` function from `@ember/object/evented` are all deprecated. + +These APIs provided a way for Ember objects to send and receive events. With modern JavaScript features and patterns, we recommend more explicit and standard approaches. For eventing, we recommend refactoring to use a library like [emittery](https://www.npmjs.com/package/emittery). + +Please note: The methods from `Evented` (`on`, `one`, `off`, `trigger`, `has`) were also available on `Ember.Component`, `Ember.Route`, and `Ember.Router`. While usage on these objects is deprecated, the methods will continue to be supported on the `Ember.RouterService`, since key parts of its functionality are difficult to reproduce without them. + +### Replacing `Evented` with `emittery` + +We recommend the [emittery](https://www.npmjs.com/package/emittery) library, which is a modern, promise-based event emitter. This is useful for services that need to broadcast state changes across an application, such as a session service that announces login and logout events. + +First, add `emittery` to your project: +```bash +npm install --save-dev emittery +# or +pnpm add --save-dev emittery +``` + +Here is an example of a session service that used `Evented`: + +Before: +```javascript +// app/services/session.js +import Service from '@ember/service'; +import Evented from '@ember/object/evented'; +import { tracked } from '@glimmer/tracking'; + +export default class SessionService extends Service.extend(Evented) { + @tracked user = null; + + login(userData) { + this.user = userData; + this.trigger('loggedIn', userData); + } + + logout() { + const oldUser = this.user; + this.user = null; + this.trigger('loggedOut', oldUser); + } +} +``` + +A consumer might use it like this: + +```javascript +// app/components/some-component.js +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; + +export default class SomeComponent extends Component { + @service session; + + constructor(owner, args) { + super(owner, args); + this.session.on('loggedIn', this, 'handleLogin'); + } + + willDestroy() { + super.willDestroy(); + this.session.off('loggedIn', this, 'handleLogin'); + } + + handleLogin(user) { + console.log(`User logged in: ${user.name}`); + // ... update component state + } +} +``` + +After refactoring to use `emittery`, the service manages its own event emitter and provides clear methods for subscribing. + +After: +```javascript +// app/services/session.js +import Service from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import Emittery from 'emittery'; + +export default class SessionService extends Service { + @tracked user = null; + + #emitter = new Emittery(); + + login(userData) { + this.user = userData; + this.#emitter.emit('loggedIn', userData); + } + + logout() { + const oldUser = this.user; + this.user = null; + this.#emitter.emit('loggedOut', oldUser); + } + + // Public subscription methods + onLoggedIn(callback) { + return this.#emitter.on('loggedIn', callback); + } + + onLoggedOut(callback) { + return this.#emitter.on('loggedOut', callback); + } +} +``` + +The listening object now receives a dedicated `unsubscribe` function, which simplifies teardown logic. + +```javascript +// app/components/some-component.js +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; + +export default class SomeComponent extends Component { + @service session; + unsubscribe; + + constructor(owner, args) { + super(owner, args); + this.unsubscribe = this.session.onLoggedIn((user) => { + this.handleLogin(user); + }); + } + + willDestroy() { + super.willDestroy(); + this.unsubscribe(); + } + + handleLogin(user) { + console.log(`User logged in: ${user.name}`); + // ... update component state + } +} +``` From 028b4e48c61c9eb36d237eec78eb9d83a46e54f2 Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Mon, 23 Jun 2025 06:56:07 -0700 Subject: [PATCH 2/4] Switch to destructors --- content/ember/v6/evented.md | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/content/ember/v6/evented.md b/content/ember/v6/evented.md index 1310ba32..e55e644f 100644 --- a/content/ember/v6/evented.md +++ b/content/ember/v6/evented.md @@ -52,6 +52,7 @@ A consumer might use it like this: // app/components/some-component.js import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; +import { registerDestructor } from '@ember/destroyable'; export default class SomeComponent extends Component { @service session; @@ -59,11 +60,10 @@ export default class SomeComponent extends Component { constructor(owner, args) { super(owner, args); this.session.on('loggedIn', this, 'handleLogin'); - } - willDestroy() { - super.willDestroy(); - this.session.off('loggedIn', this, 'handleLogin'); + registerDestructor(this, () => { + this.session.off('loggedIn', this, 'handleLogin'); + }); } handleLogin(user) { @@ -109,27 +109,25 @@ export default class SessionService extends Service { } ``` -The listening object now receives a dedicated `unsubscribe` function, which simplifies teardown logic. +The listening object can then use `registerDestructor` from `@ember/destroyable` to tie the subscription's lifetime to its own. This removes the need for a `willDestroy` hook and manual cleanup. ```javascript // app/components/some-component.js import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; +import { registerDestructor } from '@ember/destroyable'; export default class SomeComponent extends Component { @service session; - unsubscribe; constructor(owner, args) { super(owner, args); - this.unsubscribe = this.session.onLoggedIn((user) => { + + const unsubscribe = this.session.onLoggedIn((user) => { this.handleLogin(user); }); - } - willDestroy() { - super.willDestroy(); - this.unsubscribe(); + registerDestructor(this, unsubscribe); } handleLogin(user) { From 4c20d4ae734056eded88a40ccda9aa060b547b30 Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Mon, 23 Jun 2025 08:25:10 -0700 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> --- content/ember/v6/evented.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/content/ember/v6/evented.md b/content/ember/v6/evented.md index e55e644f..9bb20599 100644 --- a/content/ember/v6/evented.md +++ b/content/ember/v6/evented.md @@ -8,7 +8,7 @@ The `Ember.Evented` mixin, the underlying `@ember/object/events` module (`addLis These APIs provided a way for Ember objects to send and receive events. With modern JavaScript features and patterns, we recommend more explicit and standard approaches. For eventing, we recommend refactoring to use a library like [emittery](https://www.npmjs.com/package/emittery). -Please note: The methods from `Evented` (`on`, `one`, `off`, `trigger`, `has`) were also available on `Ember.Component`, `Ember.Route`, and `Ember.Router`. While usage on these objects is deprecated, the methods will continue to be supported on the `Ember.RouterService`, since key parts of its functionality are difficult to reproduce without them. +Please note: The methods from `Evented` (`on`, `one`, `off`, `trigger`, `has`) were also available on `Ember.Component`, `Ember.Route`, and `Ember.Router`. While usage on these objects is deprecated, the methods will continue to be supported and not deprecated on the `RouterService`, since key parts of its functionality are difficult to reproduce without them. ### Replacing `Evented` with `emittery` @@ -23,7 +23,7 @@ pnpm add --save-dev emittery Here is an example of a session service that used `Evented`: -Before: +#### Before ```javascript // app/services/session.js import Service from '@ember/service'; @@ -75,7 +75,7 @@ export default class SomeComponent extends Component { After refactoring to use `emittery`, the service manages its own event emitter and provides clear methods for subscribing. -After: +#### After ```javascript // app/services/session.js import Service from '@ember/service'; From 8f816221b4c2c0c7fe78ec7b466ee3048b23f13b Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Tue, 12 Aug 2025 11:36:44 -0700 Subject: [PATCH 4/4] Add note about async and sync listeners --- content/ember/v6/evented.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/content/ember/v6/evented.md b/content/ember/v6/evented.md index 9bb20599..a1a648ca 100644 --- a/content/ember/v6/evented.md +++ b/content/ember/v6/evented.md @@ -6,14 +6,14 @@ since: 6.6.0 The `Ember.Evented` mixin, the underlying `@ember/object/events` module (`addListener`, `removeListener`, `sendEvent`), and the `on()` function from `@ember/object/evented` are all deprecated. -These APIs provided a way for Ember objects to send and receive events. With modern JavaScript features and patterns, we recommend more explicit and standard approaches. For eventing, we recommend refactoring to use a library like [emittery](https://www.npmjs.com/package/emittery). +These APIs provided a way for Ember objects to send and receive events. With modern JavaScript features and patterns, we recommend more explicit and standard approaches. For eventing, we recommend refactoring to use a modern asynchronous library like [emittery](https://www.npmjs.com/package/emittery) or (if you need to preserve synchronous semantics) a library such as [`nanoevents`](https://www.npmjs.com/package/nanoevents) or [`mitt`](https://www.npmjs.com/package/mitt). + +> ⚠️ Important: `Ember.Evented` emits events *synchronously*. Changing to a library with asynchronous behavior, while recommended, may lead to subtle changes in your application's behavior. Please note: The methods from `Evented` (`on`, `one`, `off`, `trigger`, `has`) were also available on `Ember.Component`, `Ember.Route`, and `Ember.Router`. While usage on these objects is deprecated, the methods will continue to be supported and not deprecated on the `RouterService`, since key parts of its functionality are difficult to reproduce without them. ### Replacing `Evented` with `emittery` -We recommend the [emittery](https://www.npmjs.com/package/emittery) library, which is a modern, promise-based event emitter. This is useful for services that need to broadcast state changes across an application, such as a session service that announces login and logout events. - First, add `emittery` to your project: ```bash npm install --save-dev emittery