Skip to content

Commit 2133e22

Browse files
authored
Merge pull request #18835 from emberjs/bugfix-lts/fix-array-proxy-content-update
[BUGFIX] Make ArrayProxy Lazy
2 parents b8fd7c9 + 7bf1347 commit 2133e22

File tree

7 files changed

+98
-30
lines changed

7 files changed

+98
-30
lines changed

packages/@ember/-internals/glimmer/tests/integration/components/tracked-test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,31 @@ moduleFor(
6969
this.assertText('123');
7070
}
7171

72+
'@test creating an array proxy inside a tracking context and immediately updating its content before usage does not trigger backtracking assertion'() {
73+
class LoaderComponent extends GlimmerishComponent {
74+
get data() {
75+
if (!this._data) {
76+
this._data = ArrayProxy.create({
77+
content: A(),
78+
});
79+
80+
this._data.content.pushObjects([1, 2, 3]);
81+
}
82+
83+
return this._data;
84+
}
85+
}
86+
87+
this.registerComponent('loader', {
88+
ComponentClass: LoaderComponent,
89+
template: '{{#each this.data as |item|}}{{item}}{{/each}}',
90+
});
91+
92+
this.render('<Loader/>');
93+
94+
this.assertText('123');
95+
}
96+
7297
'@test tracked properties that are uninitialized do not throw an error'() {
7398
let CountComponent = Component.extend({
7499
count: tracked(),

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

Lines changed: 17 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, computed } from '@ember/-internals/metal';
3+
import { get, set, notifyPropertyChange, computed, on } 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

@@ -1108,6 +1108,22 @@ moduleFor(
11081108
}
11091109
);
11101110

1111+
moduleFor(
1112+
'Syntax test: {{#each}} with array proxies, content is updated after init',
1113+
class extends EachTest {
1114+
createList(items) {
1115+
let wrapped = emberA(items);
1116+
let proxy = ArrayProxy.extend({
1117+
setup: on('init', function() {
1118+
this.set('content', emberA(wrapped));
1119+
}),
1120+
}).create();
1121+
1122+
return { list: proxy, delegate: wrapped };
1123+
}
1124+
}
1125+
);
1126+
11111127
moduleFor(
11121128
'Syntax test: {{#each as}} undefined path',
11131129
class extends RenderingTestCase {

packages/@ember/-internals/runtime/lib/mixins/array.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ const ArrayMixin = Mixin.create(Enumerable, {
569569
configurable: true,
570570
enumerable: false,
571571
get() {
572-
hasListeners(this, '@array:change') || hasListeners(this, '@array:before');
572+
return hasListeners(this, '@array:change') || hasListeners(this, '@array:before');
573573
},
574574
}),
575575

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

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ import {
1111
removeArrayObserver,
1212
replace,
1313
getChainTagsForKey,
14-
tagForProperty,
14+
CUSTOM_TAG_FOR,
1515
arrayContentDidChange,
16-
arrayContentWillChange,
16+
createTagForProperty,
1717
} from '@ember/-internals/metal';
1818
import EmberObject from './object';
1919
import { isArray, MutableArray } from '../mixins/array';
2020
import { assert } from '@ember/debug';
21-
import { combine, update, validate, value } from '@glimmer/reference';
21+
import { combine, validate, value } from '@glimmer/reference';
2222

2323
const ARRAY_OBSERVER_MAPPING = {
2424
willChange: '_arrangedContentArrayWillChange',
@@ -106,18 +106,24 @@ export default class ArrayProxy extends EmberObject {
106106
this._length = 0;
107107

108108
this._arrangedContent = null;
109-
110109
this._arrangedContentIsUpdating = false;
111-
this._arrangedContentTag = combine(getChainTagsForKey(this, 'arrangedContent'));
112-
this._arrangedContentRevision = value(this._arrangedContentTag);
110+
this._arrangedContentTag = null;
111+
this._arrangedContentRevision = null;
112+
}
113113

114-
this._addArrangedContentArrayObserver();
114+
[PROPERTY_DID_CHANGE]() {
115+
this._revalidate();
116+
}
115117

116-
update(tagForProperty(this, '[]'), combine(getChainTagsForKey(this, 'arrangedContent.[]')));
117-
update(
118-
tagForProperty(this, 'length'),
119-
combine(getChainTagsForKey(this, 'arrangedContent.length'))
120-
);
118+
[CUSTOM_TAG_FOR](key) {
119+
if (key === '[]' || key === 'length') {
120+
// revalidate eagerly if we're being tracked, since we no longer will
121+
// be able to later on due to backtracking re-render assertion
122+
this._revalidate();
123+
return combine(getChainTagsForKey(this, `arrangedContent.${key}`));
124+
}
125+
126+
return createTagForProperty(this, key);
121127
}
122128

123129
willDestroy() {
@@ -236,10 +242,6 @@ export default class ArrayProxy extends EmberObject {
236242
}
237243
}
238244

239-
[PROPERTY_DID_CHANGE]() {
240-
this._revalidate();
241-
}
242-
243245
_updateArrangedContentArray() {
244246
let oldLength = this._objects === null ? 0 : this._objects.length;
245247
let arrangedContent = get(this, 'arrangedContent');
@@ -301,13 +303,21 @@ export default class ArrayProxy extends EmberObject {
301303
}
302304

303305
_revalidate() {
306+
if (this._arrangedContentIsUpdating === true) return;
307+
304308
if (
305-
!this._arrangedContentIsUpdating &&
309+
this._arrangedContentTag === null ||
306310
!validate(this._arrangedContentTag, this._arrangedContentRevision)
307311
) {
308-
this._arrangedContentIsUpdating = true;
309-
this._updateArrangedContentArray();
310-
this._arrangedContentIsUpdating = false;
312+
if (this._arrangedContentTag === null) {
313+
// This is the first time the proxy has been setup, only add the observer
314+
// don't trigger any events
315+
this._addArrangedContentArrayObserver();
316+
} else {
317+
this._arrangedContentIsUpdating = true;
318+
this._updateArrangedContentArray();
319+
this._arrangedContentIsUpdating = false;
320+
}
311321

312322
this._arrangedContentTag = combine(getChainTagsForKey(this, 'arrangedContent'));
313323
this._arrangedContentRevision = value(this._arrangedContentTag);
@@ -328,10 +338,6 @@ ArrayProxy.reopen(MutableArray, {
328338

329339
// Array proxies don't need to notify when they change since their `[]` tag is
330340
// already dependent on the `[]` tag of `arrangedContent`
331-
arrayContentWillChange(startIdx, removeAmt, addAmt) {
332-
return arrayContentWillChange(this, startIdx, removeAmt, addAmt, false);
333-
},
334-
335341
arrayContentDidChange(startIdx, removeAmt, addAmt) {
336342
return arrayContentDidChange(this, startIdx, removeAmt, addAmt, false);
337343
},

packages/@ember/-internals/runtime/tests/helpers/array.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ export function runArrayTests(name, Tests, ...types) {
314314
moduleFor(`EmberArray: ${name}`, Tests, EmberArrayHelpers);
315315
break;
316316
case 'MutableArray':
317-
moduleFor(`MutableArray: ${name}`, Tests, EmberArrayHelpers);
317+
moduleFor(`MutableArray: ${name}`, Tests, MutableArrayHelpers);
318318
break;
319319
case 'CopyableArray':
320320
moduleFor(`CopyableArray: ${name}`, Tests, CopyableArray);
@@ -323,7 +323,7 @@ export function runArrayTests(name, Tests, ...types) {
323323
moduleFor(`CopyableNativeArray: ${name}`, Tests, CopyableNativeArray);
324324
break;
325325
case 'NativeArray':
326-
moduleFor(`NativeArray: ${name}`, Tests, EmberArrayHelpers);
326+
moduleFor(`NativeArray: ${name}`, Tests, NativeArrayHelpers);
327327
break;
328328
}
329329
});

packages/@ember/-internals/runtime/tests/mixins/array_test.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ import {
1111
arrayContentWillChange,
1212
} from '@ember/-internals/metal';
1313
import EmberObject from '../../lib/system/object';
14-
import EmberArray from '../../lib/mixins/array';
15-
import { A as emberA } from '../../lib/mixins/array';
14+
import EmberArray, { A as emberA } from '../../lib/mixins/array';
1615
import { moduleFor, AbstractTestCase, runLoopSettled } from 'internal-test-helpers';
1716

1817
/*
@@ -254,6 +253,22 @@ moduleFor(
254253
arrayContentDidChange(obj);
255254
assert.deepEqual(observer._after, null);
256255
}
256+
257+
['@test hasArrayObservers should work'](assert) {
258+
assert.equal(
259+
obj.hasArrayObservers,
260+
true,
261+
'correctly shows it has an array observer when one exists'
262+
);
263+
264+
removeArrayObserver(obj, observer);
265+
266+
assert.equal(
267+
obj.hasArrayObservers,
268+
false,
269+
'correctly shows it has an array observer when one exists'
270+
);
271+
}
257272
}
258273
);
259274

packages/@ember/-internals/runtime/tests/system/array_proxy/watching_and_listening_test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ moduleFor(
3838
let content2 = A();
3939
let proxy = ArrayProxy.create({ content: content1 });
4040

41+
assert.deepEqual(sortedListenersFor(content1, '@array:before'), []);
42+
assert.deepEqual(sortedListenersFor(content1, '@array:change'), []);
43+
44+
// setup proxy
45+
proxy.length;
46+
4147
assert.deepEqual(sortedListenersFor(content1, '@array:before'), [
4248
'_arrangedContentArrayWillChange',
4349
]);

0 commit comments

Comments
 (0)