From 6aa78879a15d865b87bbb8f4f3564cc099587d35 Mon Sep 17 00:00:00 2001 From: Scott Newcomer Date: Sat, 19 Sep 2020 22:33:30 -0700 Subject: [PATCH] [BUG]: Consume array access to autotrack hasMany --- .../acceptance/relationships/has-many-test.js | 184 +++++++++++++++++- .../acceptance/tracking-model-id-test.js | 150 +++++++------- .../model/addon/-private/system/many-array.js | 19 +- 3 files changed, 274 insertions(+), 79 deletions(-) diff --git a/packages/-ember-data/tests/acceptance/relationships/has-many-test.js b/packages/-ember-data/tests/acceptance/relationships/has-many-test.js index b22912336a7..37ab9245c75 100644 --- a/packages/-ember-data/tests/acceptance/relationships/has-many-test.js +++ b/packages/-ember-data/tests/acceptance/relationships/has-many-test.js @@ -1,4 +1,8 @@ -import { render } from '@ember/test-helpers'; +import { action } from '@ember/object'; +import { sort } from '@ember/object/computed'; +import { inject as service } from '@ember/service'; +import { click, render } from '@ember/test-helpers'; +import Component from '@glimmer/component'; import Ember from 'ember'; import hbs from 'htmlbars-inline-precompile'; @@ -546,3 +550,181 @@ module('async has-many rendering tests', function(hooks) { }); }); }); + +module('autotracking has-many', function(hooks) { + setupRenderingTest(hooks); + + let store; + + hooks.beforeEach(function() { + let { owner } = this; + owner.register('model:person', Person); + owner.register('adapter:application', TestAdapter); + owner.register('serializer:application', JSONAPISerializer); + owner.register('service:store', Store); + store = owner.lookup('service:store'); + }); + + test('We can re-render a pojo', async function(assert) { + class ChildrenList extends Component { + @service store; + + get children() { + return this.args.model.children; + } + + get sortedChildren() { + return this.children.sortBy('name'); + } + + @action + createChild() { + const parent = this.args.model.person; + const name = 'RGB'; + this.store.createRecord('person', { name, parent }); + } + } + + let layout = hbs` + + +

{{this.sortedChildren.length}}

+ + `; + this.owner.register('component:children-list', ChildrenList); + this.owner.register('template:components/children-list', layout); + + store.createRecord('person', { id: '1', name: 'Doodad' }); + let person = store.peekRecord('person', '1'); + let children = await person.children; + this.model = { person, children }; + + await render(hbs``); + + let items = this.element.querySelectorAll('li'); + let names = domListToArray(items).map(e => e.textContent); + + assert.deepEqual(names, [], 'rendered no children'); + + await click('#createChild'); + + items = this.element.querySelectorAll('li'); + names = domListToArray(items).map(e => e.textContent); + assert.deepEqual(names, ['RGB'], 'rendered 1 child'); + + await click('#createChild'); + + items = this.element.querySelectorAll('li'); + names = domListToArray(items).map(e => e.textContent); + assert.deepEqual(names, ['RGB', 'RGB'], 'rendered 2 children'); + }); + + test('We can re-render hasMany', async function(assert) { + class ChildrenList extends Component { + @service store; + + get sortedChildren() { + return this.args.person.children.sortBy('name'); + } + + @action + createChild() { + const parent = this.args.person; + const name = 'RGB'; + this.store.createRecord('person', { name, parent }); + } + } + + let layout = hbs` + + +

{{this.sortedChildren.length}}

+ + `; + this.owner.register('component:children-list', ChildrenList); + this.owner.register('template:components/children-list', layout); + + store.createRecord('person', { id: '1', name: 'Doodad' }); + let person = store.peekRecord('person', '1'); + this.person = person; + + await render(hbs``); + + let items = this.element.querySelectorAll('li'); + let names = domListToArray(items).map(e => e.textContent); + + assert.deepEqual(names, [], 'rendered no children'); + + await click('#createChild'); + + items = this.element.querySelectorAll('li'); + names = domListToArray(items).map(e => e.textContent); + assert.deepEqual(names, ['RGB'], 'rendered 1 child'); + + await click('#createChild'); + + items = this.element.querySelectorAll('li'); + names = domListToArray(items).map(e => e.textContent); + assert.deepEqual(names, ['RGB', 'RGB'], 'rendered 2 children'); + }); + + test('We can re-render hasMany with sort computed macro', async function(assert) { + class ChildrenList extends Component { + @service store; + + sortProperties = ['name']; + @sort('args.person.children', 'sortProperties') sortedChildren; + + @action + createChild() { + const parent = this.args.person; + const name = 'RGB'; + this.store.createRecord('person', { name, parent }); + } + } + + let layout = hbs` + + +

{{this.sortedChildren.length}}

+
    + {{#each this.sortedChildren as |child|}} +
  • {{child.name}}
  • + {{/each}} +
+ `; + this.owner.register('component:children-list', ChildrenList); + this.owner.register('template:components/children-list', layout); + + store.createRecord('person', { id: '1', name: 'Doodad' }); + let person = store.peekRecord('person', '1'); + this.person = person; + + await render(hbs``); + + let items = this.element.querySelectorAll('li'); + let names = domListToArray(items).map(e => e.textContent); + + assert.deepEqual(names, [], 'rendered no children'); + + await click('#createChild'); + + items = this.element.querySelectorAll('li'); + names = domListToArray(items).map(e => e.textContent); + assert.deepEqual(names, ['RGB'], 'rendered 1 child'); + + await click('#createChild'); + + items = this.element.querySelectorAll('li'); + names = domListToArray(items).map(e => e.textContent); + assert.deepEqual(names, ['RGB', 'RGB'], 'rendered 2 children'); + }); +}); diff --git a/packages/-ember-data/tests/acceptance/tracking-model-id-test.js b/packages/-ember-data/tests/acceptance/tracking-model-id-test.js index 64c52b62f28..043d76251ae 100644 --- a/packages/-ember-data/tests/acceptance/tracking-model-id-test.js +++ b/packages/-ember-data/tests/acceptance/tracking-model-id-test.js @@ -3,10 +3,8 @@ import Component from '@glimmer/component'; import hbs from 'htmlbars-inline-precompile'; import { module, test } from 'qunit'; -import { has } from 'require'; import { resolve } from 'rsvp'; -import { gte } from 'ember-compatibility-helpers'; import { setupRenderingTest } from 'ember-qunit'; import JSONAPIAdapter from '@ember-data/adapter/json-api'; @@ -14,89 +12,87 @@ import Model, { attr } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import Store from '@ember-data/store'; -if (gte('3.13.0') && has('@glimmer/component')) { - class Widget extends Model { - @attr() name; +class Widget extends Model { + @attr() name; - get numericId() { - return Number(this.id); - } + get numericId() { + return Number(this.id); } +} - class WidgetList extends Component { - get sortedWidgets() { - let { widgets } = this.args; +class WidgetList extends Component { + get sortedWidgets() { + let { widgets } = this.args; - return widgets.slice().sort((a, b) => b.numericId - a.numericId); - } + return widgets.slice().sort((a, b) => b.numericId - a.numericId); } +} - let layout = hbs` -
    - {{#each this.sortedWidgets as |widget index|}} -
  • -
    ID: {{widget.id}}
    -
    Numeric ID: {{widget.numericId}}
    -
    Name: {{widget.name}}
    -
    -
  • - {{/each}} -
- `; - - class TestAdapter extends JSONAPIAdapter { - createRecord() { - return resolve({ - data: { - id: '4', - type: 'widget', - attributes: { - name: 'Contraption', - }, +let layout = hbs` +
    + {{#each this.sortedWidgets as |widget index|}} +
  • +
    ID: {{widget.id}}
    +
    Numeric ID: {{widget.numericId}}
    +
    Name: {{widget.name}}
    +
    +
  • + {{/each}} +
+`; + +class TestAdapter extends JSONAPIAdapter { + createRecord() { + return resolve({ + data: { + id: '4', + type: 'widget', + attributes: { + name: 'Contraption', }, - }); - } + }, + }); } +} - module('acceptance/tracking-model-id - tracking model id', function(hooks) { - setupRenderingTest(hooks); - - hooks.beforeEach(function() { - let { owner } = this; - owner.register('service:store', Store); - owner.register('model:widget', Widget); - owner.register('component:widget-list', WidgetList); - owner.register('template:components/widget-list', layout); - owner.register('adapter:application', TestAdapter); - owner.register('serializer:application', JSONAPISerializer); - }); +module('acceptance/tracking-model-id - tracking model id', function(hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function() { + let { owner } = this; + owner.register('service:store', Store); + owner.register('model:widget', Widget); + owner.register('component:widget-list', WidgetList); + owner.register('template:components/widget-list', layout); + owner.register('adapter:application', TestAdapter); + owner.register('serializer:application', JSONAPISerializer); + }); - test("can track model id's without using get", async function(assert) { - let store = this.owner.lookup('service:store'); - store.createRecord('widget', { id: '1', name: 'Doodad' }); - store.createRecord('widget', { id: '3', name: 'Gizmo' }); - store.createRecord('widget', { id: '2', name: 'Gadget' }); - this.widgets = store.peekAll('widget'); - - await render(hbs` - - `); - await settled(); - - assert.dom('ul>li+li+li').exists(); - assert.dom('ul>li.widget0>div.name').containsText('Gizmo'); - assert.dom('ul>li.widget1>div.name').containsText('Gadget'); - assert.dom('ul>li.widget2>div.name').containsText('Doodad'); - - let contraption = store.createRecord('widget', { name: 'Contraption' }); - await contraption.save(); - await settled(); - - assert.dom('ul>li+li+li+li').exists(); - assert.dom('ul>li.widget0>div.name').containsText('Contraption'); - assert.dom('ul>li.widget1>div.name').containsText('Gizmo'); - assert.dom('ul>li.widget2>div.name').containsText('Gadget'); - assert.dom('ul>li.widget3>div.name').containsText('Doodad'); - }); + test("can track model id's without using get", async function(assert) { + let store = this.owner.lookup('service:store'); + store.createRecord('widget', { id: '1', name: 'Doodad' }); + store.createRecord('widget', { id: '3', name: 'Gizmo' }); + store.createRecord('widget', { id: '2', name: 'Gadget' }); + this.widgets = store.peekAll('widget'); + + await render(hbs` + + `); + await settled(); + + assert.dom('ul>li+li+li').exists(); + assert.dom('ul>li.widget0>div.name').containsText('Gizmo'); + assert.dom('ul>li.widget1>div.name').containsText('Gadget'); + assert.dom('ul>li.widget2>div.name').containsText('Doodad'); + + let contraption = store.createRecord('widget', { name: 'Contraption' }); + await contraption.save(); + await settled(); + + assert.dom('ul>li+li+li+li').exists(); + assert.dom('ul>li.widget0>div.name').containsText('Contraption'); + assert.dom('ul>li.widget1>div.name').containsText('Gizmo'); + assert.dom('ul>li.widget2>div.name').containsText('Gadget'); + assert.dom('ul>li.widget3>div.name').containsText('Doodad'); }); -} +}); diff --git a/packages/model/addon/-private/system/many-array.js b/packages/model/addon/-private/system/many-array.js index 3c3ebb0bfe5..ede693e192e 100644 --- a/packages/model/addon/-private/system/many-array.js +++ b/packages/model/addon/-private/system/many-array.js @@ -67,7 +67,6 @@ export default EmberObject.extend(MutableArray, DeprecatedEvented, { @property {Boolean} isLoaded */ this.isLoaded = this.isLoaded || false; - this.length = 0; /** Used for async `hasMany` arrays @@ -166,7 +165,25 @@ export default EmberObject.extend(MutableArray, DeprecatedEvented, { return false; }, + get length() { + // By using `get()`, the tracking system knows to pay attention to changes that occur. + get(this, '[]'); + + if (typeof this._length === 'number') { + return this._length; + } + + return (this._length = 0); + }, + + set length(value) { + return (this._length = value); + }, + objectAt(index) { + // By using `get()`, the tracking system knows to pay attention to changes that occur. + get(this, '[]'); + // TODO we likely need to force flush here /* if (this.relationship._willUpdateManyArray) {