feat(ngFor): add custom trackBy function support

Make it possible to track items in iterables in custom ways (e.g. by ID or index), rather than simply by identity.

Closes #6779
This commit is contained in:
Kara Erickson
2016-02-01 18:31:26 -08:00
committed by Kara
parent cfef76f683
commit cee2318110
10 changed files with 192 additions and 62 deletions
@@ -16,7 +16,7 @@ import {
import {ListWrapper} from 'angular2/src/facade/collection';
import {Component, View, TemplateRef, ContentChild} from 'angular2/core';
import {NgFor} from 'angular2/src/common/directives/ng_for';
import {By} from 'angular2/platform/common_dom';
export function main() {
describe('ngFor', () => {
@@ -360,6 +360,31 @@ export function main() {
async.done();
});
}));
it('should use custom track by if function is provided',
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
var template =
`<template ngFor #item [ngForOf]="items" [ngForTrackBy]="customTrackBy" #i="index">
<p>{{items[i]}}</p>
</template>`;
tcb.overrideTemplate(TestComponent, template)
.createAsync(TestComponent)
.then((fixture) => {
var buildItemList =
() => {
fixture.debugElement.componentInstance.items = [{'id': 'a'}];
fixture.detectChanges();
return fixture.debugElement.queryAll(By.css('p'))[0];
}
var firstP = buildItemList();
var finalP = buildItemList();
expect(finalP.nativeElement).toBe(firstP.nativeElement);
async.done();
});
}));
});
}
@@ -373,6 +398,7 @@ class TestComponent {
@ContentChild(TemplateRef) contentTpl: TemplateRef;
items: any;
constructor() { this.items = [1, 2]; }
customTrackBy(index: number, item: any): string { return item['id']; }
}
@Component({selector: 'outer-cmp'})
@@ -14,11 +14,17 @@ import {
} from 'angular2/src/core/change_detection/differs/default_iterable_differ';
import {NumberWrapper} from 'angular2/src/facade/lang';
import {ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
import {ListWrapper} from 'angular2/src/facade/collection';
import {TestIterable} from '../../../core/change_detection/iterable';
import {iterableChangesAsString} from '../../../core/change_detection/util';
class ItemWithId {
constructor(private id: string) {}
toString() { return `{id: ${this.id}}` }
}
// todo(vicb): UnmodifiableListView / frozen object when implemented
export function main() {
describe('iterable differ', function() {
@@ -254,6 +260,7 @@ export function main() {
}));
});
it('should support duplicates', () => {
let l = ['a', 'a', 'a', 'b', 'b'];
differ.check(l);
@@ -324,5 +331,80 @@ export function main() {
});
});
});
describe('trackBy function', function() {
var differ;
var trackByItemId = (index: number, item: any): any => item.id;
var buildItemList =
(list: string[]) => { return list.map((val) => {return new ItemWithId(val)}) };
beforeEach(() => { differ = new DefaultIterableDiffer(trackByItemId); });
it('should not treat maps as new with track by function', () => {
let l = buildItemList(['a', 'b', 'c']);
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({
collection: [`{id: a}[null->0]`, `{id: b}[null->1]`, `{id: c}[null->2]`],
additions: [`{id: a}[null->0]`, `{id: b}[null->1]`, `{id: c}[null->2]`]
}));
l = buildItemList(['a', 'b', 'c']);
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({
collection: [`{id: a}`, `{id: b}`, `{id: c}`],
previous: [`{id: a}`, `{id: b}`, `{id: c}`]
}));
});
it('should track moves normally with track by function', () => {
let l = buildItemList(['a', 'b', 'c']);
differ.check(l);
l = buildItemList(['b', 'a', 'c']);
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({
collection: ['{id: b}[1->0]', '{id: a}[0->1]', '{id: c}'],
previous: ['{id: a}[0->1]', '{id: b}[1->0]', '{id: c}'],
moves: ['{id: b}[1->0]', '{id: a}[0->1]']
}));
});
it('should track duplicate reinsertion normally with track by function', () => {
let l = buildItemList(['a', 'a']);
differ.check(l);
l = buildItemList(['b', 'a', 'a']);
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({
collection: ['{id: b}[null->0]', '{id: a}[0->1]', '{id: a}[1->2]'],
previous: ['{id: a}[0->1]', '{id: a}[1->2]'],
moves: ['{id: a}[0->1]', '{id: a}[1->2]'],
additions: ['{id: b}[null->0]']
}));
});
it('should track removals normally with track by function', () => {
let l = buildItemList(['a', 'b', 'c']);
differ.check(l);
ListWrapper.removeAt(l, 2);
differ.check(l);
expect(differ.toString())
.toEqual(iterableChangesAsString({
collection: ['{id: a}', '{id: b}'],
previous: ['{id: a}', '{id: b}', '{id: c}[2->null]'],
removals: ['{id: c}[2->null]']
}));
});
});
});
}
+1
View File
@@ -290,6 +290,7 @@ var NG_COMMON = [
'NgFor.ngDoCheck()',
'NgFor.ngForOf=',
'NgFor.ngForTemplate=',
'NgFor.ngForTrackBy=',
'NgForm',
'NgForm.addControl()',
'NgForm.addControlGroup()',