diff --git a/addon/array/fragment.js b/addon/array/fragment.js index ee293ef4..cfa42e6b 100644 --- a/addon/array/fragment.js +++ b/addon/array/fragment.js @@ -28,14 +28,6 @@ const FragmentArray = StatefulArray.extend({ */ modelName: null, - objectAt(index) { - const recordData = this._super(index); - if (recordData === undefined) { - return; - } - return recordData._fragmentGetRecord(); - }, - _normalizeData(data, index) { assert( `You can only add '${this.modelName}' fragments or object literals to this property`, @@ -46,14 +38,28 @@ const FragmentArray = StatefulArray.extend({ if (isFragment(data)) { const recordData = recordDataFor(data); setFragmentOwner(data, this.recordData, this.key); - return recordData; + return recordData._fragmentGetRecord(); } - const existing = this.objectAt(index); + const existing = this.currentState[index]; if (existing) { existing.setProperties(data); - return recordDataFor(existing); + return existing; } - return this.recordData._newFragmentRecordDataForKey(this.key, data); + const recordData = this.recordData._newFragmentRecordDataForKey( + this.key, + data + ); + return recordData._fragmentGetRecord(); + }, + + _getFragmentState() { + const recordDatas = this._super(); + return recordDatas?.map((recordData) => recordData._fragmentGetRecord()); + }, + + _setFragmentState(fragments) { + const recordDatas = fragments.map((fragment) => recordDataFor(fragment)); + this._super(recordDatas); }, /** diff --git a/addon/array/stateful.js b/addon/array/stateful.js index 415ff06e..fc40fe39 100644 --- a/addon/array/stateful.js +++ b/addon/array/stateful.js @@ -106,11 +106,23 @@ const StatefulArray = EmberObject.extend(MutableArray, Copyable, { return data; }, + _getFragmentState() { + return this.recordData.getFragment(this.key); + }, + + _setFragmentState(array) { + this.recordData.setDirtyFragment(this.key, array); + }, + replace(start, deleteCount, items) { assert( 'The third argument to replace needs to be an array.', isArray(items) ); + assert( + 'Attempted to update the fragment array after it was destroyed', + !this.isDestroyed && !this.isDestroying + ); if (deleteCount === 0 && items.length === 0) { // array is unchanged return; @@ -124,7 +136,7 @@ const StatefulArray = EmberObject.extend(MutableArray, Copyable, { deleteCount, ...items.map((item, i) => this._normalizeData(item, start + i)) ); - this.recordData.setDirtyFragment(this.key, data); + this._setFragmentState(data); this.notify(); }, @@ -133,9 +145,9 @@ const StatefulArray = EmberObject.extend(MutableArray, Copyable, { if (this.isDestroyed || this.isDestroying || this._isUpdating) { return; } - const currentState = this.recordData.getFragment(this.key); + const currentState = this._getFragmentState(); if (currentState == null) { - // detached + // detached; the underlying fragment array was set to null after this StatefulArray was accessed return; } diff --git a/addon/record-data.js b/addon/record-data.js index b8736f29..9632c9eb 100644 --- a/addon/record-data.js +++ b/addon/record-data.js @@ -752,9 +752,9 @@ export default class FragmentRecordData extends RecordData { Object.assign(this._fragmentData, newCanonicalFragments); // update fragment arrays - changedFragmentKeys?.forEach((key) => - this._fragmentArrayCache[key]?.notify() - ); + changedFragmentKeys?.forEach((key) => { + this._fragmentArrayCache[key]?.notify(); + }); } const changedKeys = mergeArrays(changedAttributeKeys, changedFragmentKeys); @@ -830,9 +830,9 @@ export default class FragmentRecordData extends RecordData { const changedAttributeKeys = super.didCommit(data); // update fragment arrays - Object.keys(newCanonicalFragments).forEach((key) => - this._fragmentArrayCache[key]?.notify() - ); + changedFragmentKeys.forEach((key) => { + this._fragmentArrayCache[key]?.notify(); + }); const changedKeys = mergeArrays(changedAttributeKeys, changedFragmentKeys); if (gte('ember-data', '4.5.0') && changedKeys?.length > 0) { diff --git a/tests/integration/render_test.js b/tests/integration/render_test.js index 0beff41a..a0f7439a 100644 --- a/tests/integration/render_test.js +++ b/tests/integration/render_test.js @@ -4,18 +4,21 @@ import { render, settled } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setComponentTemplate } from '@ember/component'; import Component from '@glimmer/component'; +import Pretender from 'pretender'; module('Integration | Rendering', function (hooks) { setupRenderingTest(hooks); - let store; + let store, server; hooks.beforeEach(function () { store = this.owner.lookup('service:store'); + server = new Pretender(); }); hooks.afterEach(function () { store = null; + server.shutdown(); }); test('construct fragments without autotracking.mutation-after-consumption error', async function (assert) { @@ -194,4 +197,118 @@ module('Integration | Rendering', function (hooks) { assert.dom('[data-product]').exists({ count: 1 }); assert.dom('[data-product="0"]').hasText('The Strangler: 299.99'); }); + + test('fragment is destroyed', async function (assert) { + this.order = store.createRecord('order', { id: 1 }); + + store.push({ + data: [ + { + id: this.order.id, + type: 'order', + attributes: { + product: { + name: 'The Strangler', + price: '299.99', + }, + }, + relationships: {}, + }, + ], + }); + + await render(hbs` + {{#let this.order.product as |product|}} + {{product.name}}: {{product.price}} + {{/let}} + `); + + assert.dom('[data-product]').hasText('The Strangler: 299.99'); + + server.delete('/orders/1', () => [204]); + await this.order.destroyRecord(); + + assert.dom('[data-product]').hasText('The Strangler: 299.99'); + }); + + test('fragment array is destroyed', async function (assert) { + this.order = store.createRecord('order', { id: 1 }); + + store.push({ + data: [ + { + id: this.order.id, + type: 'order', + attributes: { + products: [ + { + name: 'The Strangler', + price: '299.99', + }, + { + name: 'Tears of Lys', + price: '499.99', + }, + ], + }, + relationships: {}, + }, + ], + }); + + await render(hbs` +