diff --git a/content/ember/v7/deprecate-action-handler-mixin.md b/content/ember/v7/deprecate-action-handler-mixin.md new file mode 100644 index 00000000..48da8bba --- /dev/null +++ b/content/ember/v7/deprecate-action-handler-mixin.md @@ -0,0 +1,51 @@ +--- +title: 'ActionHandler mixin' +until: 8.0.0 +since: 7.4.0 +--- + +The `ActionHandler` mixin from `@ember/-internals/runtime` is deprecated. This mixin was private, but importable. + +`ActionHandler` gave an object an `actions` hash and a `send` method. Both are already deprecated on their own (refer to the [`send` deprecation](/id/deprecate-target-action-support)). The replacement is a plain method, decorated with `@action` when it is passed around as a callback. + +### Before + +```javascript +import EmberObject from '@ember/object'; +import { ActionHandler } from '@ember/-internals/runtime'; + +export default class Uploader extends EmberObject.extend(ActionHandler) { + actions = { + start(file) { + /* ... */ + }, + }; + + upload(file) { + this.send('start', file); + } +} +``` + +### After + +```javascript +import { action } from '@ember/object'; + +export default class Uploader { + @action + start(file) { + /* ... */ + } + + upload(file) { + this.start(file); + } +} +``` + +`@action` is only needed when the method is handed to something else (an `{{on}}` modifier, a child component argument, an event listener) and must keep its `this`. A method that is only called as `this.start()` does not need it. + +If the mixin was used for bubbling through `target`, pass the function down as an argument instead of naming it and bubbling by string. + +For more background, read [RFC 1116](https://github.com/emberjs/rfcs/pull/1116). diff --git a/content/ember/v7/deprecate-array-proxy.md b/content/ember/v7/deprecate-array-proxy.md new file mode 100644 index 00000000..0a64e0bc --- /dev/null +++ b/content/ember/v7/deprecate-array-proxy.md @@ -0,0 +1,192 @@ +--- +title: 'ArrayProxy' +until: 8.0.0 +since: 7.4.0 +--- + +`ArrayProxy` from `@ember/array/proxy` is deprecated. Use a tracked array, or a plain array behind a `@tracked` property, instead. + +### Swapping the underlying array + +Before: + +```javascript +import ArrayProxy from '@ember/array/proxy'; +import { A } from '@ember/array'; + +let pets = ArrayProxy.create({ content: A(['dog', 'cat']) }); + +pets.get('firstObject'); // 'dog' + +pets.set('content', A(['amoeba'])); +pets.get('firstObject'); // 'amoeba' +``` + +After: + +```javascript +import { tracked } from '@glimmer/tracking'; + +class PetStore { + @tracked pets = ['dog', 'cat']; +} + +let store = new PetStore(); + +store.pets[0]; // 'dog' + +store.pets = ['amoeba']; +store.pets[0]; // 'amoeba' +``` + +### Mutating in place + +Before: + +```javascript +import ArrayProxy from '@ember/array/proxy'; +import { A } from '@ember/array'; + +let pets = ArrayProxy.create({ content: A(['dog', 'cat']) }); + +pets.pushObject('fish'); +pets.get('length'); // 3 +``` + +After: + +```javascript +import { trackedArray } from '@ember/reactive/collections'; + +let pets = trackedArray(['dog', 'cat']); + +pets.push('fish'); +pets.length; // 3 +``` + +`trackedArray` from [`@ember/reactive/collections`](https://api.emberjs.com/ember/release/modules/@ember%2Freactive%2Fcollections/) is a native array whose mutations are tracked. + +### Sorted or filtered views (`arrangedContent`) + +Before: + +```javascript +import ArrayProxy from '@ember/array/proxy'; +import { computed } from '@ember/object'; +import { A } from '@ember/array'; + +class SortedPeople extends ArrayProxy { + @computed('content.[]') + get arrangedContent() { + return this.content.sortBy('name'); + } +} + +let people = SortedPeople.create({ content: A([{ name: 'Yehuda' }, { name: 'Tom' }]) }); + +people.get('firstObject').name; // 'Tom' +``` + +After: + +```javascript +import { trackedArray } from '@ember/reactive/collections'; +import { cached } from '@glimmer/tracking'; + +class People { + all = trackedArray([{ name: 'Yehuda' }, { name: 'Tom' }]); + + @cached + get sorted() { + return this.all.toSorted((a, b) => a.name.localeCompare(b.name)); + } +} + +let people = new People(); + +people.sorted[0].name; // 'Tom' + +people.all.push({ name: 'Chris' }); +people.sorted[0].name; // 'Chris' +``` + +`@cached` is optional. Without it the getter re-sorts on every read, which is fine for small lists. + +### Transforming items (`objectAtContent`) + +Before: + +```javascript +import ArrayProxy from '@ember/array/proxy'; +import { A } from '@ember/array'; + +class ShoutingPets extends ArrayProxy { + objectAtContent(index) { + return this.content.objectAt(index).toUpperCase(); + } +} + +let pets = ShoutingPets.create({ content: A(['dog', 'cat']) }); + +pets.objectAt(0); // 'DOG' +``` + +After: + +```javascript +import { trackedArray } from '@ember/reactive/collections'; + +class Pets { + all = trackedArray(['dog', 'cat']); +} + +let pets = new Pets(); + +pets.all[0].toUpperCase(); // 'DOG' +pets.all.at(-1).toUpperCase(); // 'CAT' +``` + +### When you must keep a stable array-shaped object + +If a third-party consumer holds on to the object and expects it to act like an array while its contents are swapped, a native [`Proxy`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) over a tracked holder reproduces that: + +```javascript +import { tracked } from '@glimmer/tracking'; + +class Holder { + @tracked content; + + constructor(content) { + this.content = content; + } +} + +export function swappableArray(initial) { + let holder = new Holder(initial); + + return new Proxy(holder, { + get(target, key, receiver) { + if (key === 'content') return target.content; + return Reflect.get(target.content, key, receiver); + }, + set(target, key, value, receiver) { + if (key === 'content') { + target.content = value; + return true; + } + return Reflect.set(target.content, key, value, receiver); + }, + has: (target, key) => key in target.content, + ownKeys: (target) => Reflect.ownKeys(target.content), + getOwnPropertyDescriptor: (target, key) => Reflect.getOwnPropertyDescriptor(target.content, key), + }); +} +``` + +Treat this as a last resort. It is harder to debug than a tracked property and getter, and `Array.isArray` returns `false` for it. + +### Computed properties that depended on the proxy + +Computed properties with dependent keys such as `pets.[]` or `pets.@each.name` relied on `ArrayProxy` firing array change notifications. After migrating, convert those computed properties to native getters. Tracked arrays and tracked properties auto-track, so the dependent keys are no longer needed. + +For more background, read [RFC 1112](https://github.com/emberjs/rfcs/pull/1112). diff --git a/content/ember/v7/deprecate-container-proxy-mixin.md b/content/ember/v7/deprecate-container-proxy-mixin.md new file mode 100644 index 00000000..cbf97715 --- /dev/null +++ b/content/ember/v7/deprecate-container-proxy-mixin.md @@ -0,0 +1,28 @@ +--- +title: 'ContainerProxyMixin' +until: 8.0.0 +since: 7.4.0 +--- + +`ContainerProxyMixin` from `@ember/-internals/runtime` is deprecated. This mixin was private, but was importable. + +There is no migration for applying the mixin yourself. Remove it. If you built a custom object that forwarded to a container, look up what you need through the owner instead: + +```javascript +import { getOwner } from '@ember/owner'; + +class ThemeLoader { + constructor(owner) { + this.owner = owner; + } + + load(name) { + return this.owner.lookup(`theme:${name}`); + } +} + +// from anywhere with an owner +let loader = new ThemeLoader(getOwner(this)); +``` + +For more background, read [RFC 1116](https://github.com/emberjs/rfcs/pull/1116). diff --git a/content/ember/v7/deprecate-controller-mixin.md b/content/ember/v7/deprecate-controller-mixin.md new file mode 100644 index 00000000..fdbbe4a3 --- /dev/null +++ b/content/ember/v7/deprecate-controller-mixin.md @@ -0,0 +1,35 @@ +--- +title: 'ControllerMixin' +until: 8.0.0 +since: 7.4.0 +--- + +`ControllerMixin` from `@ember/controller` is deprecated. Extend `Controller` from the same module instead. `ControllerMixin` was private but was importable. + +### Before + +```javascript +// app/controllers/settings.js +import EmberObject from '@ember/object'; +import { ControllerMixin } from '@ember/controller'; + +export default class SettingsController extends EmberObject.extend(ControllerMixin) { + queryParams = ['tab']; + tab = 'general'; +} +``` + +### After + +```javascript +// app/controllers/settings.js +import Controller from '@ember/controller'; +import { tracked } from '@glimmer/tracking'; + +export default class SettingsController extends Controller { + queryParams = ['tab']; + @tracked tab = 'general'; +} +``` + +For more background, read [RFC 1116](https://github.com/emberjs/rfcs/pull/1116). diff --git a/content/ember/v7/deprecate-ember-array-mixin.md b/content/ember/v7/deprecate-ember-array-mixin.md new file mode 100644 index 00000000..206444b8 --- /dev/null +++ b/content/ember/v7/deprecate-ember-array-mixin.md @@ -0,0 +1,86 @@ +--- +title: 'EmberArray mixin' +until: 8.0.0 +since: 7.4.0 +--- + +The `EmberArray` mixin, the default export of `@ember/array`, is deprecated. Use native arrays and native array methods instead. + +`EmberArray` provided a read-only array API (`firstObject`, `objectAt`, `mapBy`, `filterBy`, `findBy`, `sortBy`, `uniq`, `compact`, `without`, and so on) for any object that exposed `length` and `objectAt`. Every one of those has a native equivalent. + +### Before: a custom array-like class + +```javascript +import EmberObject from '@ember/object'; +import EmberArray from '@ember/array'; + +export default class Pages extends EmberObject.extend(EmberArray) { + pages = []; + + get length() { + return this.pages.length; + } + + objectAt(index) { + return this.pages[index]; + } +} + +let pages = Pages.create({ pages: [{ title: 'Intro' }, { title: 'Setup' }] }); + +pages.get('firstObject').title; // 'Intro' +pages.mapBy('title'); // ['Intro', 'Setup'] +``` + +### After: a native array + +Most of the time the class only existed to give an array the Ember API. Drop the wrapper and use the array: + +```javascript +let pages = [{ title: 'Intro' }, { title: 'Setup' }]; + +pages[0].title; // 'Intro' +pages.map((page) => page.title); // ['Intro', 'Setup'] +``` + +If the collection drives UI updates, wrap it with `trackedArray` from [`@ember/reactive/collections`](https://api.emberjs.com/ember/release/modules/@ember%2Freactive%2Fcollections/) so mutations re-render: + +```javascript +import { trackedArray } from '@ember/reactive/collections'; + +let pages = trackedArray([{ title: 'Intro' }, { title: 'Setup' }]); +``` + +If the class has its own API and only needs to be iterable, implement the [iterable protocol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) instead: + +```javascript +export default class Pages { + #pages = []; + + *[Symbol.iterator]() { + yield* this.#pages; + } +} +``` + +### Replacing each method + +| EmberArray | Native | +| --- | --- | +| `firstObject`, `lastObject` | `arr[0]`, `arr.at(-1)` | +| `objectAt(i)`, `objectsAt([i, j])` | `arr[i]`, `[i, j].map((i) => arr[i])` | +| `mapBy('k')`, `getEach('k')` | `arr.map((x) => x.k)` | +| `filterBy('k', v)`, `rejectBy('k', v)` | `arr.filter((x) => x.k === v)`, `arr.filter((x) => x.k !== v)` | +| `findBy('k', v)` | `arr.find((x) => x.k === v)` | +| `isAny('k', v)`, `isEvery('k', v)` | `arr.some((x) => x.k === v)`, `arr.every((x) => x.k === v)` | +| `sortBy('k')` | `arr.toSorted((a, b) => compare(a.k, b.k))` with `compare` from `@ember/utils` | +| `uniq()`, `uniqBy('k')` | `Array.from(new Set(arr))`, `uniqBy` from `@ember/array` or a small helper | +| `compact()` | `arr.filter((x) => x != null)` | +| `without(x)` | `arr.filter((y) => y !== x)` | +| `invoke('m', ...args)` | `arr.map((x) => x.m(...args))` | +| `toArray()` | `Array.from(arr)` | +| `any(fn)`, `every(fn)`, `find(fn)`, `includes(x)` | same names on `Array.prototype` | + +Reactivity note: `sortBy`, `filterBy`, and friends were often used inside computed properties with `[]` or `@each` dependent keys. With tracked arrays, a plain getter that calls the native method re-computes on its own. + +For more background, read [RFC 1116](https://github.com/emberjs/rfcs/pull/1116). diff --git a/content/ember/v7/deprecate-enumerable.md b/content/ember/v7/deprecate-enumerable-mixin.md similarity index 100% rename from content/ember/v7/deprecate-enumerable.md rename to content/ember/v7/deprecate-enumerable-mixin.md diff --git a/content/ember/v7/deprecate-mutable-array-mixin.md b/content/ember/v7/deprecate-mutable-array-mixin.md new file mode 100644 index 00000000..f20cc661 --- /dev/null +++ b/content/ember/v7/deprecate-mutable-array-mixin.md @@ -0,0 +1,71 @@ +--- +title: 'MutableArray mixin' +until: 8.0.0 +since: 7.4.0 +--- + +The `MutableArray` mixin, exported from `@ember/array` and `@ember/array/mutable`, is deprecated. Use native arrays and native array methods instead. + +`MutableArray` built on [`EmberArray`](/id/deprecate-ember-array-mixin) and added the mutation API: `pushObject`, `removeObject`, `insertAt`, `replace`, `clear`, and so on. These methods existed so the classic observer system could see array changes. With tracked arrays, native mutation is already observed. + +### Before: a custom mutable collection + +```javascript +import EmberObject from '@ember/object'; +import MutableArray from '@ember/array/mutable'; + +export default class Selection extends EmberObject.extend(MutableArray) { + items = []; + + get length() { + return this.items.length; + } + + objectAt(index) { + return this.items[index]; + } + + replace(start, deleteCount, added = []) { + this.items.splice(start, deleteCount, ...added); + this.arrayContentDidChange(start, deleteCount, added.length); + } +} + +let selection = Selection.create(); + +selection.pushObject('a'); +selection.addObject('a'); // no-op, already present +selection.removeObject('a'); +``` + +### After: a tracked array + +```javascript +import { trackedArray } from '@ember/reactive/collections'; + +let selection = trackedArray(); + +selection.push('a'); +if (!selection.includes('a')) selection.push('a'); +selection.splice(selection.indexOf('a'), 1); +``` + +Any template or getter that reads `selection` updates when it changes. If the values need to stay unique, `trackedSet` from the same module is a better fit than emulating `addObject`. + +### Replacing each method + +| MutableArray | Native | +| --- | --- | +| `pushObject(x)`, `pushObjects(arr)` | `arr.push(x)`, `arr.push(...items)` | +| `popObject()`, `shiftObject()` | `arr.pop()`, `arr.shift()` | +| `unshiftObject(x)`, `unshiftObjects(arr)` | `arr.unshift(x)`, `arr.unshift(...items)` | +| `insertAt(i, x)` | `arr.splice(i, 0, x)` | +| `removeAt(i, n)` | `arr.splice(i, n)` | +| `removeObject(x)`, `removeObjects(arr)` | `arr.splice(arr.indexOf(x), 1)`, or `removeAt` from `@ember/array` | +| `addObject(x)`, `addObjects(arr)` | `if (!arr.includes(x)) arr.push(x)`, or use a `trackedSet` | +| `replace(i, n, items)` | `arr.splice(i, n, ...items)` | +| `setObjects(items)` | `arr.splice(0, arr.length, ...items)` or assign a new array to a tracked property | +| `clear()` | `arr.length = 0` or `arr.splice(0)` | +| `reverseObjects()` | `arr.reverse()` | + +For more background, read [RFC 1116](https://github.com/emberjs/rfcs/pull/1116). diff --git a/content/ember/v7/deprecate-mutable-enumerable-mixin.md b/content/ember/v7/deprecate-mutable-enumerable-mixin.md new file mode 100644 index 00000000..d6aaa491 --- /dev/null +++ b/content/ember/v7/deprecate-mutable-enumerable-mixin.md @@ -0,0 +1,33 @@ +--- +title: 'MutableEnumerable mixin' +until: 8.0.0 +since: 7.4.0 +--- + +`MutableEnumerable` from `@ember/enumerable/mutable` is deprecated. + +Like `Enumerable`, this mixin has been empty for a long time and was kept only so `.detect()` checks kept working. The migration is the same as for [`Enumerable`](/id/deprecate-enumerable-mixin): replace `.detect()` checks with `Array.isArray` or an iterable check, and replace custom collection classes with native arrays or `trackedArray`. + +### Before + +```javascript +import MutableEnumerable from '@ember/enumerable/mutable'; + +function clearAll(maybeList) { + if (MutableEnumerable.detect(maybeList)) { + maybeList.clear(); + } +} +``` + +### After + +```javascript +function clearAll(maybeList) { + if (Array.isArray(maybeList)) { + maybeList.length = 0; + } +} +``` + +For more background, read [RFC 1116](https://github.com/emberjs/rfcs/pull/1116). diff --git a/content/ember/v7/deprecate-native-array-mixin.md b/content/ember/v7/deprecate-native-array-mixin.md new file mode 100644 index 00000000..8aaba97d --- /dev/null +++ b/content/ember/v7/deprecate-native-array-mixin.md @@ -0,0 +1,41 @@ +--- +title: 'NativeArray mixin' +until: 8.0.0 +since: 7.4.0 +--- + +The `NativeArray` mixin from `@ember/array` is deprecated. + +`NativeArray` is the set of [`EmberArray`](/id/deprecate-ember-array-mixin) and [`MutableArray`](/id/deprecate-mutable-array-mixin) methods that `A()` adds onto a plain JavaScript array. Using the mixin yourself now triggers this deprecation. Native arrays already have everything you need. Use them as-is, or wrap them with `trackedArray` when changes need to re-render. + +### Before + +```javascript +import { A, NativeArray } from '@ember/array'; + +let tags = A(['ember', 'glimmer']); + +tags.pushObject('vite'); +tags.get('lastObject'); // 'vite' + +NativeArray.detect(tags); // true +``` + +### After + +```javascript +import { trackedArray } from '@ember/reactive/collections'; + +let tags = trackedArray(['ember', 'glimmer']); + +tags.push('vite'); +tags.at(-1); // 'vite' + +Array.isArray(tags); // true +``` + +If the array never changes after creation, or only changes by reassignment of a `@tracked` property, a plain array literal is enough and `trackedArray` is not needed. + +Refer to the method tables in the [`EmberArray`](/id/deprecate-ember-array-mixin) and [`MutableArray`](/id/deprecate-mutable-array-mixin) guides for a native equivalent of each method. + +For more background, read [RFC 1116](https://github.com/emberjs/rfcs/pull/1116). diff --git a/content/ember/v7/deprecate-object-proxy.md b/content/ember/v7/deprecate-object-proxy.md new file mode 100644 index 00000000..da12382d --- /dev/null +++ b/content/ember/v7/deprecate-object-proxy.md @@ -0,0 +1,183 @@ +--- +title: 'ObjectProxy' +until: 8.0.0 +since: 7.4.0 +--- + +`ObjectProxy` from `@ember/object/proxy` is deprecated. Use tracked properties and direct property access, or a native `Proxy` for the rare cases that need interception. + +`ObjectProxy` forwarded reads and writes of unknown properties to a `content` object. It was mostly used to swap that object out from under consumers, or to add computed properties on top of it. Tracked state covers both. + +Pick the section below that matches how you used it. + +### Swapping the wrapped object + +Before: + +```javascript +import ObjectProxy from '@ember/object/proxy'; + +let person = ObjectProxy.create({ content: { name: 'Tom' } }); + +person.get('name'); // 'Tom' + +person.set('content', { name: 'Thomas' }); +person.get('name'); // 'Thomas' +``` + +After: + +```javascript +import { tracked } from '@glimmer/tracking'; + +class CurrentUser { + @tracked person = { name: 'Tom' }; +} + +let current = new CurrentUser(); + +current.person.name; // 'Tom' + +current.person = { name: 'Thomas' }; +current.person.name; // 'Thomas' +``` + +Templates and getters that read `current.person.name` update when `person` is reassigned. + +### Adding properties on top of the wrapped object + +Before: + +```javascript +import ObjectProxy from '@ember/object/proxy'; +import { computed } from '@ember/object'; + +class PersonPresenter extends ObjectProxy { + @computed('firstName', 'lastName') + get fullName() { + return `${this.get('firstName')} ${this.get('lastName')}`; + } +} + +let presenter = PersonPresenter.create({ + content: { firstName: 'Tom', lastName: 'Dale' }, +}); + +presenter.get('fullName'); // 'Tom Dale' +presenter.get('firstName'); // 'Tom' +``` + +After, when you own the object, make it a class with the getter on it: + +```javascript +import { tracked } from '@glimmer/tracking'; + +class Person { + @tracked firstName; + @tracked lastName; + + constructor({ firstName, lastName }) { + this.firstName = firstName; + this.lastName = lastName; + } + + get fullName() { + return `${this.firstName} ${this.lastName}`; + } +} + +let person = new Person({ firstName: 'Tom', lastName: 'Dale' }); + +person.fullName; // 'Tom Dale' +person.firstName; // 'Tom' +``` + +After, when you do not own the object, wrap it in a class that exposes what you need: + +```javascript +import { tracked } from '@glimmer/tracking'; + +class PersonPresenter { + @tracked person; + + constructor(person) { + this.person = person; + } + + get fullName() { + return `${this.person.firstName} ${this.person.lastName}`; + } +} + +let presenter = new PersonPresenter({ firstName: 'Tom', lastName: 'Dale' }); + +presenter.fullName; // 'Tom Dale' +presenter.person.firstName; // 'Tom' +``` + +Reading `presenter.person.firstName` instead of `presenter.firstName` is the intended change. Explicit access is easier to type-check and to follow than forwarding. + +### Forwarding an open-ended set of properties + +If consumers read arbitrary keys through the wrapper and you cannot change them, a native [`Proxy`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) reproduces the forwarding: + +```javascript +let person = { firstName: 'Tom', lastName: 'Dale' }; + +let presenter = new Proxy(person, { + get(target, key, receiver) { + if (key === 'fullName') { + return `${target.firstName} ${target.lastName}`; + } + return Reflect.get(target, key, receiver); + }, +}); + +presenter.fullName; // 'Tom Dale' +presenter.firstName; // 'Tom' +``` + +To also swap the target later, keep the target in a `@tracked` property on a holder and forward through `target.content`, as shown in the [`ArrayProxy` guide](/id/deprecate-array-proxy). + +### `unknownProperty` and `setUnknownProperty` + +The `get` and `set` traps of a native `Proxy` are the direct replacement. + +Before: + +```javascript +import ObjectProxy from '@ember/object/proxy'; + +class Defaults extends ObjectProxy { + unknownProperty(key) { + return this.content[key] ?? `missing:${key}`; + } +} + +let settings = Defaults.create({ content: { theme: 'dark' } }); + +settings.get('theme'); // 'dark' +settings.get('locale'); // 'missing:locale' +``` + +After: + +```javascript +let settings = new Proxy( + { theme: 'dark' }, + { + get(target, key, receiver) { + return key in target ? Reflect.get(target, key, receiver) : `missing:${String(key)}`; + }, + } +); + +settings.theme; // 'dark' +settings.locale; // 'missing:locale' +``` + +### Computed properties that depended on the proxy + +Computed properties with dependent keys such as `content.firstName` relied on `ObjectProxy` notifying changes through `content`. After migrating, convert those computed properties to native getters. Tracked properties auto-track, so the dependent keys are no longer needed. + +For more background, read [RFC 1112](https://github.com/emberjs/rfcs/pull/1112). diff --git a/content/ember/v7/deprecate-observable-mixin.md b/content/ember/v7/deprecate-observable-mixin.md new file mode 100644 index 00000000..8a72d1eb --- /dev/null +++ b/content/ember/v7/deprecate-observable-mixin.md @@ -0,0 +1,77 @@ +--- +title: 'Observable mixin' +until: 8.0.0 +since: 7.4.0 +--- + +The `Observable` mixin from `@ember/object/observable` is deprecated. + +`EmberObject` already includes this behavior, so the deprecation only fires when your code applies the mixin itself. That usually means one of two things: + +- a class that extends `EmberObject.extend(Observable)` (redundant, since `EmberObject` already has it) +- a class that applies `Observable` to get `get`, `set`, `notifyPropertyChange`, `addObserver`, `incrementProperty`, `toggleProperty`, and friends + +In both cases, the replacement is a native class with tracked properties and native property access. + +### Before + +```javascript +import EmberObject from '@ember/object'; +import Observable from '@ember/object/observable'; + +export default class Counter extends EmberObject.extend(Observable) { + count = 0; + + increment() { + this.incrementProperty('count'); + } + + reset() { + this.setProperties({ count: 0 }); + } +} + +let counter = Counter.create(); + +counter.get('count'); // 0 +counter.addObserver('count', () => console.log('changed')); +counter.increment(); +``` + +### After + +```javascript +import { tracked } from '@glimmer/tracking'; + +export default class Counter { + @tracked count = 0; + + increment() { + this.count++; + } + + reset() { + this.count = 0; + } +} + +let counter = new Counter(); + +counter.count; // 0 +counter.increment(); +``` + +Templates and getters that read `count` update automatically because the property is tracked. Nothing has to observe it. + +### Replacing each method + +| Observable method | Replacement | +| --- | --- | +| `get('foo')`, `getProperties(...)` | `this.foo` (refer to the [`get` and `set` deprecation](/id/deprecate-observable)) | +| `set('foo', v)`, `setProperties({...})` | `this.foo = v` on a `@tracked` property | +| `incrementProperty`, `decrementProperty`, `toggleProperty` | `this.foo++`, `this.foo--`, `this.foo = !this.foo` | +| `notifyPropertyChange` | Not needed. Assigning a tracked property invalidates dependents. | +| `addObserver`, `removeObserver` | Derive state with a getter instead of reacting to changes. Where a side effect is required, run it from the method that makes the change. | +| `cacheFor` | Not needed. Use `@cached` from `@glimmer/tracking` on a getter when you need memoization. | + +For more background, read [RFC 1116](https://github.com/emberjs/rfcs/pull/1116). diff --git a/content/ember/v7/deprecate-proxy-mixin.md b/content/ember/v7/deprecate-proxy-mixin.md new file mode 100644 index 00000000..22f86105 --- /dev/null +++ b/content/ember/v7/deprecate-proxy-mixin.md @@ -0,0 +1,55 @@ +--- +title: 'ProxyMixin' +until: 8.0.0 +since: 7.4.0 +--- + +`ProxyMixin`, exported as `_ProxyMixin` from `@ember/-internals/runtime`, is deprecated. This mixin was private, but was importable. + +`ProxyMixin` is what gives [`ObjectProxy`](/id/deprecate-object-proxy) its behavior: every property not defined on the proxy is forwarded to `content`. Applying the mixin directly to another `EmberObject` subclass is deprecated along with `ObjectProxy` itself. + +### Before + +```javascript +import EmberObject from '@ember/object'; +import { _ProxyMixin } from '@ember/-internals/runtime'; + +export default class Draft extends EmberObject.extend(_ProxyMixin) { + isDraft = true; +} + +let draft = Draft.create({ content: { title: 'Hello' } }); + +draft.get('title'); // 'Hello' +draft.isDraft; // true +``` + +### After + +Most uses only need the wrapped object plus a few extra fields. Hold the object in a tracked property and read through it: + +```javascript +import { tracked } from '@glimmer/tracking'; + +export default class Draft { + @tracked content; + isDraft = true; + + constructor(content) { + this.content = content; + } + + get title() { + return this.content.title; + } +} + +let draft = new Draft({ title: 'Hello' }); + +draft.title; // 'Hello' +draft.isDraft; // true +``` + +When the forwarded property set is open-ended, a native `Proxy` covers the same ground. Refer to the [`ObjectProxy` deprecation](/id/deprecate-object-proxy) for that pattern and for `unknownProperty` replacements. + +For more background, read [RFC 1116](https://github.com/emberjs/rfcs/pull/1116). diff --git a/content/ember/v7/deprecate-registry-proxy-mixin.md b/content/ember/v7/deprecate-registry-proxy-mixin.md new file mode 100644 index 00000000..debdce55 --- /dev/null +++ b/content/ember/v7/deprecate-registry-proxy-mixin.md @@ -0,0 +1,13 @@ +--- +title: 'RegistryProxyMixin' +until: 8.0.0 +since: 7.4.0 +--- + +`RegistryProxyMixin` from `@ember/-internals/runtime` is deprecated. This mixin was private, but importable. + +The mixin gave `Application`, `Engine`, and their instances the registry methods: `register`, `unregister`, `resolveRegistration`, `hasRegistration`, `registerOption`, and friends. Ember still provides these methods on the owner. They now come from an internal copy of the mixin, so application code and initializers that call `application.register(...)` keep working without changes. + +There is no migration for applying the mixin yourself. Remove it. + +For more background, read [RFC 1116](https://github.com/emberjs/rfcs/pull/1116).