Skip to content

Commit 63f9f45

Browse files
Chris GarrettRobert Jackson
authored andcommitted
[BUGFIX release] Correctly links ArrayProxy tags to arrangedContent
Currently, if `arrangedContent` is overridden in an ArrayProxy with a computed property that depends on changes to another array/context, those changes will not propagate correctly. This is because we never link the tags of the ArrayProxy to the corresponding tags of the `arrangedContent`, instead relying on array observers to propagate changes. This works when the underlying array is being changed directly, but _doesn't_ work if the array is being replaced entirely (e.g. the computed property has invalidated and needs to recompute). This PR ensures that ArrayProxy tags are setup correctly, so that if `arrangedContent` ever changes, the proxy will also propagate those changes. This will affect anything that depends on the ArrayProxy directly, such as `{{#each}}` loops and other computed properties. One side effect of this is that ArrayProxy's no longer need to manually dirty themselves, and in fact attempting to do so can trigger the backtracking rerender assertion (specifically when the proxy first attempts to update/synchronize while rendering). Internally, a boolean flag has been added to the array change methods to allow it to opt-out of sending a notification.
1 parent 0aa4e5f commit 63f9f45

File tree

3 files changed

+48
-7
lines changed

3 files changed

+48
-7
lines changed

packages/@ember/-internals/glimmer/tests/integration/syntax/each-test.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { moduleFor, RenderingTestCase, applyMixins, strip, runTask } from 'internal-test-helpers';
22

3-
import { get, set, notifyPropertyChange } from '@ember/-internals/metal';
3+
import { get, set, notifyPropertyChange, computed } from '@ember/-internals/metal';
44
import { A as emberA, ArrayProxy, RSVP } from '@ember/-internals/runtime';
55
import { HAS_NATIVE_SYMBOL } from '@ember/-internals/utils';
66

@@ -1089,6 +1089,25 @@ moduleFor(
10891089
}
10901090
);
10911091

1092+
moduleFor(
1093+
'Syntax test: {{#each}} with array proxies, arrangedContent depends on external content',
1094+
class extends EachTest {
1095+
createList(items) {
1096+
let wrapped = emberA(items);
1097+
let proxy = ArrayProxy.extend({
1098+
arrangedContent: computed('wrappedItems.[]', function() {
1099+
// Slice the items to ensure that updates must be propogated
1100+
return this.wrappedItems.slice();
1101+
}),
1102+
}).create({
1103+
wrappedItems: wrapped,
1104+
});
1105+
1106+
return { list: proxy, delegate: wrapped };
1107+
}
1108+
}
1109+
);
1110+
10921111
moduleFor(
10931112
'Syntax test: {{#each as}} undefined path',
10941113
class extends RenderingTestCase {

packages/@ember/-internals/metal/lib/array_events.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ export function arrayContentDidChange<T extends { length: number }>(
3232
array: T,
3333
startIdx: number,
3434
removeAmt: number,
35-
addAmt: number
35+
addAmt: number,
36+
notify = true
3637
): T {
3738
// if no args are passed assume everything changes
3839
if (startIdx === undefined) {
@@ -50,11 +51,13 @@ export function arrayContentDidChange<T extends { length: number }>(
5051

5152
let meta = peekMeta(array);
5253

53-
if (addAmt < 0 || removeAmt < 0 || addAmt - removeAmt !== 0) {
54-
notifyPropertyChange(array, 'length', meta);
55-
}
54+
if (notify) {
55+
if (addAmt < 0 || removeAmt < 0 || addAmt - removeAmt !== 0) {
56+
notifyPropertyChange(array, 'length', meta);
57+
}
5658

57-
notifyPropertyChange(array, '[]', meta);
59+
notifyPropertyChange(array, '[]', meta);
60+
}
5861

5962
sendEvent(array, '@array:change', [array, startIdx, removeAmt, addAmt]);
6063

packages/@ember/-internals/runtime/lib/system/array_proxy.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ import {
1111
removeArrayObserver,
1212
replace,
1313
getChainTagsForKey,
14+
tagForProperty,
15+
arrayContentDidChange,
16+
arrayContentWillChange,
1417
} from '@ember/-internals/metal';
1518
import EmberObject from './object';
1619
import { isArray, MutableArray } from '../mixins/array';
1720
import { assert } from '@ember/debug';
18-
import { combine, validate, value } from '@glimmer/reference';
21+
import { combine, update, validate, value } from '@glimmer/reference';
1922

2023
const ARRAY_OBSERVER_MAPPING = {
2124
willChange: '_arrangedContentArrayWillChange',
@@ -109,6 +112,12 @@ export default class ArrayProxy extends EmberObject {
109112
this._arrangedContentRevision = value(this._arrangedContentTag);
110113

111114
this._addArrangedContentArrayObserver();
115+
116+
update(tagForProperty(this, '[]'), combine(getChainTagsForKey(this, 'arrangedContent.[]')));
117+
update(
118+
tagForProperty(this, 'length'),
119+
combine(getChainTagsForKey(this, 'arrangedContent.length'))
120+
);
112121
}
113122

114123
willDestroy() {
@@ -316,4 +325,14 @@ ArrayProxy.reopen(MutableArray, {
316325
@public
317326
*/
318327
arrangedContent: alias('content'),
328+
329+
// Array proxies don't need to notify when they change since their `[]` tag is
330+
// already dependent on the `[]` tag of `arrangedContent`
331+
arrayContentWillChange(startIdx, removeAmt, addAmt) {
332+
return arrayContentWillChange(this, startIdx, removeAmt, addAmt, false);
333+
},
334+
335+
arrayContentDidChange(startIdx, removeAmt, addAmt) {
336+
return arrayContentDidChange(this, startIdx, removeAmt, addAmt, false);
337+
},
319338
});

0 commit comments

Comments
 (0)