Skip to content

Commit

Permalink
[BUG]: Consume array access to autotrack hasMany
Browse files Browse the repository at this point in the history
  • Loading branch information
snewcomer committed Sep 24, 2020
1 parent 30d5f54 commit 6aa7887
Show file tree
Hide file tree
Showing 3 changed files with 274 additions and 79 deletions.
184 changes: 183 additions & 1 deletion 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';
Expand Down Expand Up @@ -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`
<button id="createChild" {{on "click" this.createChild}}>Add child</button>
<h2>{{this.sortedChildren.length}}</h2>
<ul>
{{#each this.sortedChildren as |child|}}
<li>{{child.name}}</li>
{{/each}}
</ul>
`;
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`<ChildrenList @model={{this.model}} />`);

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`
<button id="createChild" {{on "click" this.createChild}}>Add child</button>
<h2>{{this.sortedChildren.length}}</h2>
<ul>
{{#each this.sortedChildren as |child|}}
<li>{{child.name}}</li>
{{/each}}
</ul>
`;
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`<ChildrenList @person={{this.person}} />`);

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`
<button id="createChild" {{on "click" this.createChild}}>Add child</button>
<h2>{{this.sortedChildren.length}}</h2>
<ul>
{{#each this.sortedChildren as |child|}}
<li>{{child.name}}</li>
{{/each}}
</ul>
`;
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`<ChildrenList @person={{this.person}} />`);

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');
});
});
150 changes: 73 additions & 77 deletions packages/-ember-data/tests/acceptance/tracking-model-id-test.js
Expand Up @@ -3,100 +3,96 @@ 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';
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`
<ul>
{{#each this.sortedWidgets as |widget index|}}
<li class="widget{{index}}">
<div class="id">ID: {{widget.id}}</div>
<div class="numeric-id">Numeric ID: {{widget.numericId}}</div>
<div class="name">Name: {{widget.name}}</div>
<br/>
</li>
{{/each}}
</ul>
`;

class TestAdapter extends JSONAPIAdapter {
createRecord() {
return resolve({
data: {
id: '4',
type: 'widget',
attributes: {
name: 'Contraption',
},
let layout = hbs`
<ul>
{{#each this.sortedWidgets as |widget index|}}
<li class="widget{{index}}">
<div class="id">ID: {{widget.id}}</div>
<div class="numeric-id">Numeric ID: {{widget.numericId}}</div>
<div class="name">Name: {{widget.name}}</div>
<br/>
</li>
{{/each}}
</ul>
`;

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`
<WidgetList @widgets={{this.widgets}} />
`);
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`
<WidgetList @widgets={{this.widgets}} />
`);
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');
});
}
});
19 changes: 18 additions & 1 deletion packages/model/addon/-private/system/many-array.js
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 6aa7887

Please sign in to comment.