Skip to content

Commit

Permalink
[BUGFIX] Update iterable items only when changed
Browse files Browse the repository at this point in the history
Currently, iterable item references always update their value and dirty
their tag, even if the value didn't actually change. This is different
from the baseline behavior in Ember, and a regression.

This PR updates this behavior, and also updates affected tests. These
tests were written prior to the recent VM upgrade, and probably assumed
some differences in behaviors in the testing environment. Now that we're
trying to align the testing environment more closely with Ember, these
tests need to be updated. I added a `@tracked` decorator to the test
helpers, which should allow us to more closely match Ember's behavior.
  • Loading branch information
Chris Garrett committed May 15, 2020
1 parent fea493f commit 7b6266a
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 13 deletions.
56 changes: 51 additions & 5 deletions packages/@glimmer/integration-tests/lib/suites/each.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { RenderTest } from '../render-test';
import { test } from '../test-decorator';
import { tracked } from '../test-helpers/tracked';

export class EachSuite extends RenderTest {
static suiteName = '#each';
Expand Down Expand Up @@ -104,11 +105,6 @@ export class EachSuite extends RenderTest {
this.assertHTML('1-02-13-24-35-46-5');
this.assertStableNodes();

v1.val = 1000;
this.rerender({ list });
this.assertHTML('1000-02-13-24-35-46-5');
this.assertStableNodes();

list = [];
this.rerender({ list });
this.assertHTML('Empty');
Expand Down Expand Up @@ -165,6 +161,38 @@ export class EachSuite extends RenderTest {
'it renders all items with duplicate key values'() {
let list = [{ text: 'Hello' }, { text: 'Hello' }, { text: 'Hello' }];

this.render(`{{#each list key="text" as |item|}}{{item.text}}{{/each}}`, {
list,
});

this.assertHTML('HelloHelloHello');
this.assertStableRerender();

list.forEach(item => (item.text = 'Goodbye'));

this.rerender({ list });
this.assertHTML('GoodbyeGoodbyeGoodbye');
this.assertStableNodes();

list = [{ text: 'Hello' }, { text: 'Hello' }, { text: 'Hello' }];

this.rerender({ list });
this.assertHTML('HelloHelloHello');
this.assertStableNodes();
}

@test
'it updates items if their key has not changed, and the items are tracked'() {
class Item {
@tracked public text: string;

constructor(text: string) {
this.text = text;
}
}

let list = [new Item('Hello'), new Item('Hello'), new Item('Hello')];

this.render(`{{#each list key="@identity" as |item|}}{{item.text}}{{/each}}`, {
list,
});
Expand All @@ -185,6 +213,24 @@ export class EachSuite extends RenderTest {
this.assertStableNodes();
}

@test
'it does not update items if their key has not changed, and the items are not tracked'() {
let list = [{ text: 'Hello' }, { text: 'Hello' }, { text: 'Hello' }];

this.render(`{{#each list key="@identity" as |item|}}{{item.text}}{{/each}}`, {
list,
});

this.assertHTML('HelloHelloHello');
this.assertStableRerender();

list.forEach(item => (item.text = 'Goodbye'));

this.rerender({ list });
this.assertHTML('HelloHelloHello');
this.assertStableNodes();
}

@test
'scoped variable not available outside list'() {
let list = ['Wycats'];
Expand Down
21 changes: 15 additions & 6 deletions packages/@glimmer/integration-tests/lib/suites/in-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { equalsElement } from '../dom/assertions';
import { stripTight } from '../test-helpers/strings';
import { replaceHTML } from '../dom/simple-utils';
import { EmberishCurlyComponent } from '../components/emberish-curly';
import { tracked } from '../test-helpers/tracked';

export class InElementSuite extends RenderTest {
static suiteName = '#in-element';
Expand Down Expand Up @@ -388,20 +389,28 @@ export class InElementSuite extends RenderTest {

@test
'Inside a loop'() {
let { delegate } = this;

class Item {
element = delegate.createElement('div');

@tracked value: string;

constructor(value: string) {
this.value = value;
}
}

this.testType = 'Dynamic';
this.registerComponent('Basic', 'FooBar', '<p>{{@value}}</p>');

this.registerHelper('log', ([item]) => console.log(item));

let roots = [
{ id: 0, element: this.delegate.createElement('div'), value: 'foo' },
{ id: 1, element: this.delegate.createElement('div'), value: 'bar' },
{ id: 2, element: this.delegate.createElement('div'), value: 'baz' },
];
let roots = [new Item('foo'), new Item('bar'), new Item('baz')];

this.render(
stripTight`
{{~#each this.roots key="id" as |root|~}}
{{~#each this.roots as |root|~}}
{{~log root~}}
{{~#in-element root.element ~}}
<FooBar @value={{root.value}} />
Expand Down
15 changes: 15 additions & 0 deletions packages/@glimmer/integration-tests/lib/test-helpers/tracked.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { trackedData } from '@glimmer/validator';

export function tracked(target: object, key: string) {
let { getter, setter } = trackedData<any, any>(key);

Object.defineProperty(target, key, {
get() {
return getter(this);
},

set(value: unknown) {
setter(this, value);
},
});
}
8 changes: 6 additions & 2 deletions packages/@glimmer/reference/lib/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,12 @@ export class IterationItemReference<T = unknown> implements TemplatePathReferenc
}

update(value: T) {
dirtyTag(this.tag);
this.itemValue = value;
let { itemValue } = this;

if (value !== itemValue) {
dirtyTag(this.tag);
this.itemValue = value;
}
}

get(key: string): TemplatePathReference {
Expand Down

0 comments on commit 7b6266a

Please sign in to comment.