X Tutup
Skip to content

Commit cee2318

Browse files
committed
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 angular#6779
1 parent cfef76f commit cee2318

File tree

10 files changed

+192
-62
lines changed

10 files changed

+192
-62
lines changed

modules/angular2/src/common/directives/ng_for.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
IterableDiffers,
77
ViewContainerRef,
88
TemplateRef,
9-
EmbeddedViewRef
9+
EmbeddedViewRef,
10+
TrackByFn
1011
} from 'angular2/core';
1112
import {isPresent, isBlank} from 'angular2/src/facade/lang';
1213

@@ -59,10 +60,11 @@ import {isPresent, isBlank} from 'angular2/src/facade/lang';
5960
* See a [live demo](http://plnkr.co/edit/KVuXxDp0qinGDyo307QW?p=preview) for a more detailed
6061
* example.
6162
*/
62-
@Directive({selector: '[ngFor][ngForOf]', inputs: ['ngForOf', 'ngForTemplate']})
63+
@Directive({selector: '[ngFor][ngForOf]', inputs: ['ngForTrackBy', 'ngForOf', 'ngForTemplate']})
6364
export class NgFor implements DoCheck {
6465
/** @internal */
6566
_ngForOf: any;
67+
_ngForTrackBy: TrackByFn;
6668
private _differ: IterableDiffer;
6769

6870
constructor(private _viewContainer: ViewContainerRef, private _templateRef: TemplateRef,
@@ -71,7 +73,7 @@ export class NgFor implements DoCheck {
7173
set ngForOf(value: any) {
7274
this._ngForOf = value;
7375
if (isBlank(this._differ) && isPresent(value)) {
74-
this._differ = this._iterableDiffers.find(value).create(this._cdr);
76+
this._differ = this._iterableDiffers.find(value).create(this._cdr, this._ngForTrackBy);
7577
}
7678
}
7779

@@ -81,6 +83,8 @@ export class NgFor implements DoCheck {
8183
}
8284
}
8385

86+
set ngForTrackBy(value: TrackByFn) { this._ngForTrackBy = value; }
87+
8488
ngDoCheck() {
8589
if (isPresent(this._differ)) {
8690
var changes = this._differ.diff(this._ngForOf);

modules/angular2/src/common/directives/observable_list_diff.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class ObservableListDiff extends DefaultIterableDiffer {
5757
class ObservableListDiffFactory implements IterableDifferFactory {
5858
const ObservableListDiffFactory();
5959
bool supports(obj) => obj is ObservableList;
60-
IterableDiffer create(ChangeDetectorRef cdRef) {
60+
IterableDiffer create(ChangeDetectorRef cdRef, [Function trackByFn]) {
6161
return new ObservableListDiff(cdRef);
6262
}
6363
}

modules/angular2/src/core/change_detection.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@ export {
2020
IterableDifferFactory,
2121
KeyValueDiffers,
2222
KeyValueDiffer,
23-
KeyValueDifferFactory
23+
KeyValueDifferFactory,
24+
TrackByFn
2425
} from './change_detection/change_detection';

modules/angular2/src/core/change_detection/change_detection.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {IterableDiffers, IterableDifferFactory} from './differs/iterable_differs';
1+
import {IterableDiffers, IterableDifferFactory, TrackByFn} from './differs/iterable_differs';
22
import {DefaultIterableDifferFactory} from './differs/default_iterable_differ';
33
import {KeyValueDiffers, KeyValueDifferFactory} from './differs/keyvalue_differs';
44
import {DefaultKeyValueDifferFactory} from './differs/default_keyvalue_differ';
@@ -37,7 +37,12 @@ export {BindingRecord, BindingTarget} from './binding_record';
3737
export {DirectiveIndex, DirectiveRecord} from './directive_record';
3838
export {DynamicChangeDetector} from './dynamic_change_detector';
3939
export {ChangeDetectorRef} from './change_detector_ref';
40-
export {IterableDiffers, IterableDiffer, IterableDifferFactory} from './differs/iterable_differs';
40+
export {
41+
IterableDiffers,
42+
IterableDiffer,
43+
IterableDifferFactory,
44+
TrackByFn
45+
} from './differs/iterable_differs';
4146
export {KeyValueDiffers, KeyValueDiffer, KeyValueDifferFactory} from './differs/keyvalue_differs';
4247
export {PipeTransform} from './pipe_transform';
4348
export {WrappedValue, SimpleChange} from './change_detection_util';

modules/angular2/src/core/change_detection/differs/default_iterable_differ.ts

Lines changed: 53 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
import {CONST} from 'angular2/src/facade/lang';
22
import {BaseException} from 'angular2/src/facade/exceptions';
3-
import {
4-
isListLikeIterable,
5-
iterateListLike,
6-
ListWrapper,
7-
MapWrapper
8-
} from 'angular2/src/facade/collection';
3+
import {isListLikeIterable, iterateListLike, ListWrapper} from 'angular2/src/facade/collection';
94

105
import {
116
isBlank,
@@ -17,17 +12,21 @@ import {
1712
} from 'angular2/src/facade/lang';
1813

1914
import {ChangeDetectorRef} from '../change_detector_ref';
20-
import {IterableDiffer, IterableDifferFactory} from '../differs/iterable_differs';
15+
import {IterableDiffer, IterableDifferFactory, TrackByFn} from '../differs/iterable_differs';
2116

2217
@CONST()
2318
export class DefaultIterableDifferFactory implements IterableDifferFactory {
2419
supports(obj: Object): boolean { return isListLikeIterable(obj); }
25-
create(cdRef: ChangeDetectorRef): DefaultIterableDiffer { return new DefaultIterableDiffer(); }
20+
create(cdRef: ChangeDetectorRef, trackByFn?: TrackByFn): DefaultIterableDiffer {
21+
return new DefaultIterableDiffer(trackByFn);
22+
}
2623
}
2724

25+
var trackByIdentity = (index: number, item: any) => item;
26+
2827
export class DefaultIterableDiffer implements IterableDiffer {
29-
private _collection = null;
3028
private _length: number = null;
29+
private _collection = null;
3130
// Keeps track of the used records at any point in time (during & across `_check()` calls)
3231
private _linkedRecords: _DuplicateMap = null;
3332
// Keeps track of the removed records at any point in time during `_check()` calls.
@@ -42,6 +41,10 @@ export class DefaultIterableDiffer implements IterableDiffer {
4241
private _removalsHead: CollectionChangeRecord = null;
4342
private _removalsTail: CollectionChangeRecord = null;
4443

44+
constructor(private _trackByFn?: TrackByFn) {
45+
this._trackByFn = isPresent(this._trackByFn) ? this._trackByFn : trackByIdentity;
46+
}
47+
4548
get collection() { return this._collection; }
4649

4750
get length(): number { return this._length; }
@@ -104,31 +107,37 @@ export class DefaultIterableDiffer implements IterableDiffer {
104107
var mayBeDirty: boolean = false;
105108
var index: number;
106109
var item;
107-
110+
var itemTrackBy;
108111
if (isArray(collection)) {
109112
var list = collection;
110113
this._length = collection.length;
111114

112115
for (index = 0; index < this._length; index++) {
113116
item = list[index];
114-
if (record === null || !looseIdentical(record.item, item)) {
115-
record = this._mismatch(record, item, index);
117+
itemTrackBy = this._trackByFn(index, item);
118+
if (record === null || !looseIdentical(record.trackById, itemTrackBy)) {
119+
record = this._mismatch(record, item, itemTrackBy, index);
116120
mayBeDirty = true;
117-
} else if (mayBeDirty) {
118-
// TODO(misko): can we limit this to duplicates only?
119-
record = this._verifyReinsertion(record, item, index);
121+
} else {
122+
if (mayBeDirty) {
123+
// TODO(misko): can we limit this to duplicates only?
124+
record = this._verifyReinsertion(record, item, itemTrackBy, index);
125+
}
126+
record.item = item;
120127
}
128+
121129
record = record._next;
122130
}
123131
} else {
124132
index = 0;
125133
iterateListLike(collection, (item) => {
126-
if (record === null || !looseIdentical(record.item, item)) {
127-
record = this._mismatch(record, item, index);
134+
itemTrackBy = this._trackByFn(index, item);
135+
if (record === null || !looseIdentical(record.trackById, itemTrackBy)) {
136+
record = this._mismatch(record, item, itemTrackBy, index);
128137
mayBeDirty = true;
129138
} else if (mayBeDirty) {
130139
// TODO(misko): can we limit this to duplicates only?
131-
record = this._verifyReinsertion(record, item, index);
140+
record = this._verifyReinsertion(record, item, itemTrackBy, index);
132141
}
133142
record = record._next;
134143
index++;
@@ -190,7 +199,8 @@ export class DefaultIterableDiffer implements IterableDiffer {
190199
*
191200
* @internal
192201
*/
193-
_mismatch(record: CollectionChangeRecord, item, index: number): CollectionChangeRecord {
202+
_mismatch(record: CollectionChangeRecord, item: any, itemTrackBy: any,
203+
index: number): CollectionChangeRecord {
194204
// The previous record after which we will append the current one.
195205
var previousRecord: CollectionChangeRecord;
196206

@@ -203,19 +213,20 @@ export class DefaultIterableDiffer implements IterableDiffer {
203213
}
204214

205215
// Attempt to see if we have seen the item before.
206-
record = this._linkedRecords === null ? null : this._linkedRecords.get(item, index);
216+
record = this._linkedRecords === null ? null : this._linkedRecords.get(itemTrackBy, index);
207217
if (record !== null) {
208218
// We have seen this before, we need to move it forward in the collection.
209219
this._moveAfter(record, previousRecord, index);
210220
} else {
211221
// Never seen it, check evicted list.
212-
record = this._unlinkedRecords === null ? null : this._unlinkedRecords.get(item);
222+
record = this._unlinkedRecords === null ? null : this._unlinkedRecords.get(itemTrackBy);
213223
if (record !== null) {
214224
// It is an item which we have evicted earlier: reinsert it back into the list.
215225
this._reinsertAfter(record, previousRecord, index);
216226
} else {
217227
// It is a new item: add it.
218-
record = this._addAfter(new CollectionChangeRecord(item), previousRecord, index);
228+
record =
229+
this._addAfter(new CollectionChangeRecord(item, itemTrackBy), previousRecord, index);
219230
}
220231
}
221232
return record;
@@ -248,15 +259,17 @@ export class DefaultIterableDiffer implements IterableDiffer {
248259
*
249260
* @internal
250261
*/
251-
_verifyReinsertion(record: CollectionChangeRecord, item, index: number): CollectionChangeRecord {
262+
_verifyReinsertion(record: CollectionChangeRecord, item: any, itemTrackBy: any,
263+
index: number): CollectionChangeRecord {
252264
var reinsertRecord: CollectionChangeRecord =
253-
this._unlinkedRecords === null ? null : this._unlinkedRecords.get(item);
265+
this._unlinkedRecords === null ? null : this._unlinkedRecords.get(itemTrackBy);
254266
if (reinsertRecord !== null) {
255267
record = this._reinsertAfter(reinsertRecord, record._prev, index);
256268
} else if (record.currentIndex != index) {
257269
record.currentIndex = index;
258270
this._addToMoves(record, index);
259271
}
272+
record.item = item;
260273
return record;
261274
}
262275

@@ -457,31 +470,20 @@ export class DefaultIterableDiffer implements IterableDiffer {
457470
}
458471

459472
toString(): string {
460-
var record: CollectionChangeRecord;
461-
462473
var list = [];
463-
for (record = this._itHead; record !== null; record = record._next) {
464-
list.push(record);
465-
}
474+
this.forEachItem((record) => list.push(record));
466475

467476
var previous = [];
468-
for (record = this._previousItHead; record !== null; record = record._nextPrevious) {
469-
previous.push(record);
470-
}
477+
this.forEachPreviousItem((record) => previous.push(record));
471478

472479
var additions = [];
473-
for (record = this._additionsHead; record !== null; record = record._nextAdded) {
474-
additions.push(record);
475-
}
480+
this.forEachAddedItem((record) => additions.push(record));
481+
476482
var moves = [];
477-
for (record = this._movesHead; record !== null; record = record._nextMoved) {
478-
moves.push(record);
479-
}
483+
this.forEachMovedItem((record) => moves.push(record));
480484

481485
var removals = [];
482-
for (record = this._removalsHead; record !== null; record = record._nextRemoved) {
483-
removals.push(record);
484-
}
486+
this.forEachRemovedItem((record) => removals.push(record));
485487

486488
return "collection: " + list.join(', ') + "\n" + "previous: " + previous.join(', ') + "\n" +
487489
"additions: " + additions.join(', ') + "\n" + "moves: " + moves.join(', ') + "\n" +
@@ -512,7 +514,7 @@ export class CollectionChangeRecord {
512514
/** @internal */
513515
_nextMoved: CollectionChangeRecord = null;
514516

515-
constructor(public item: any) {}
517+
constructor(public item: any, public trackById: any) {}
516518

517519
toString(): string {
518520
return this.previousIndex === this.currentIndex ?
@@ -550,13 +552,13 @@ class _DuplicateItemRecordList {
550552
}
551553
}
552554

553-
// Returns a CollectionChangeRecord having CollectionChangeRecord.item == item and
555+
// Returns a CollectionChangeRecord having CollectionChangeRecord.trackById == trackById and
554556
// CollectionChangeRecord.currentIndex >= afterIndex
555-
get(item: any, afterIndex: number): CollectionChangeRecord {
557+
get(trackById: any, afterIndex: number): CollectionChangeRecord {
556558
var record: CollectionChangeRecord;
557559
for (record = this._head; record !== null; record = record._nextDup) {
558560
if ((afterIndex === null || afterIndex < record.currentIndex) &&
559-
looseIdentical(record.item, item)) {
561+
looseIdentical(record.trackById, trackById)) {
560562
return record;
561563
}
562564
}
@@ -599,7 +601,7 @@ class _DuplicateMap {
599601

600602
put(record: CollectionChangeRecord) {
601603
// todo(vicb) handle corner cases
602-
var key = getMapKey(record.item);
604+
var key = getMapKey(record.trackById);
603605

604606
var duplicates = this.map.get(key);
605607
if (!isPresent(duplicates)) {
@@ -610,17 +612,17 @@ class _DuplicateMap {
610612
}
611613

612614
/**
613-
* Retrieve the `value` using key. Because the CollectionChangeRecord value maybe one which we
615+
* Retrieve the `value` using key. Because the CollectionChangeRecord value may be one which we
614616
* have already iterated over, we use the afterIndex to pretend it is not there.
615617
*
616618
* Use case: `[a, b, c, a, a]` if we are at index `3` which is the second `a` then asking if we
617619
* have any more `a`s needs to return the last `a` not the first or second.
618620
*/
619-
get(value: any, afterIndex: number = null): CollectionChangeRecord {
620-
var key = getMapKey(value);
621+
get(trackById: any, afterIndex: number = null): CollectionChangeRecord {
622+
var key = getMapKey(trackById);
621623

622624
var recordList = this.map.get(key);
623-
return isBlank(recordList) ? null : recordList.get(value, afterIndex);
625+
return isBlank(recordList) ? null : recordList.get(trackById, afterIndex);
624626
}
625627

626628
/**
@@ -629,7 +631,7 @@ class _DuplicateMap {
629631
* The list of duplicates also is removed from the map if it gets empty.
630632
*/
631633
remove(record: CollectionChangeRecord): CollectionChangeRecord {
632-
var key = getMapKey(record.item);
634+
var key = getMapKey(record.trackById);
633635
// todo(vicb)
634636
// assert(this.map.containsKey(key));
635637
var recordList: _DuplicateItemRecordList = this.map.get(key);

modules/angular2/src/core/change_detection/differs/iterable_differs.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,19 @@ export interface IterableDiffer {
1313
onDestroy();
1414
}
1515

16+
/**
17+
* An optional function passed into {@link NgFor} that defines how to track
18+
* items in an iterable (e.g. by index or id)
19+
*/
20+
export interface TrackByFn { (index: number, item: any): any; }
21+
22+
1623
/**
1724
* Provides a factory for {@link IterableDiffer}.
1825
*/
1926
export interface IterableDifferFactory {
2027
supports(objects: any): boolean;
21-
create(cdRef: ChangeDetectorRef): IterableDiffer;
28+
create(cdRef: ChangeDetectorRef, trackByFn?: TrackByFn): IterableDiffer;
2229
}
2330

2431
/**

0 commit comments

Comments
 (0)
X Tutup