Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions content/ember/v7/deprecate-action-handler-mixin.md
Original file line number Diff line number Diff line change
@@ -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).
192 changes: 192 additions & 0 deletions content/ember/v7/deprecate-array-proxy.md
Original file line number Diff line number Diff line change
@@ -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).
28 changes: 28 additions & 0 deletions content/ember/v7/deprecate-container-proxy-mixin.md
Original file line number Diff line number Diff line change
@@ -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).
35 changes: 35 additions & 0 deletions content/ember/v7/deprecate-controller-mixin.md
Original file line number Diff line number Diff line change
@@ -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).
86 changes: 86 additions & 0 deletions content/ember/v7/deprecate-ember-array-mixin.md
Original file line number Diff line number Diff line change
@@ -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).
Loading
Loading