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:
@ -1,4 +1,4 @@
|
||||
import {IterableDiffers, IterableDifferFactory} from './differs/iterable_differs';
|
||||
import {IterableDiffers, IterableDifferFactory, TrackByFn} from './differs/iterable_differs';
|
||||
import {DefaultIterableDifferFactory} from './differs/default_iterable_differ';
|
||||
import {KeyValueDiffers, KeyValueDifferFactory} from './differs/keyvalue_differs';
|
||||
import {DefaultKeyValueDifferFactory} from './differs/default_keyvalue_differ';
|
||||
@ -37,7 +37,12 @@ export {BindingRecord, BindingTarget} from './binding_record';
|
||||
export {DirectiveIndex, DirectiveRecord} from './directive_record';
|
||||
export {DynamicChangeDetector} from './dynamic_change_detector';
|
||||
export {ChangeDetectorRef} from './change_detector_ref';
|
||||
export {IterableDiffers, IterableDiffer, IterableDifferFactory} from './differs/iterable_differs';
|
||||
export {
|
||||
IterableDiffers,
|
||||
IterableDiffer,
|
||||
IterableDifferFactory,
|
||||
TrackByFn
|
||||
} from './differs/iterable_differs';
|
||||
export {KeyValueDiffers, KeyValueDiffer, KeyValueDifferFactory} from './differs/keyvalue_differs';
|
||||
export {PipeTransform} from './pipe_transform';
|
||||
export {WrappedValue, SimpleChange} from './change_detection_util';
|
||||
|
@ -1,11 +1,6 @@
|
||||
import {CONST} from 'angular2/src/facade/lang';
|
||||
import {BaseException} from 'angular2/src/facade/exceptions';
|
||||
import {
|
||||
isListLikeIterable,
|
||||
iterateListLike,
|
||||
ListWrapper,
|
||||
MapWrapper
|
||||
} from 'angular2/src/facade/collection';
|
||||
import {isListLikeIterable, iterateListLike, ListWrapper} from 'angular2/src/facade/collection';
|
||||
|
||||
import {
|
||||
isBlank,
|
||||
@ -17,17 +12,21 @@ import {
|
||||
} from 'angular2/src/facade/lang';
|
||||
|
||||
import {ChangeDetectorRef} from '../change_detector_ref';
|
||||
import {IterableDiffer, IterableDifferFactory} from '../differs/iterable_differs';
|
||||
import {IterableDiffer, IterableDifferFactory, TrackByFn} from '../differs/iterable_differs';
|
||||
|
||||
@CONST()
|
||||
export class DefaultIterableDifferFactory implements IterableDifferFactory {
|
||||
supports(obj: Object): boolean { return isListLikeIterable(obj); }
|
||||
create(cdRef: ChangeDetectorRef): DefaultIterableDiffer { return new DefaultIterableDiffer(); }
|
||||
create(cdRef: ChangeDetectorRef, trackByFn?: TrackByFn): DefaultIterableDiffer {
|
||||
return new DefaultIterableDiffer(trackByFn);
|
||||
}
|
||||
}
|
||||
|
||||
var trackByIdentity = (index: number, item: any) => item;
|
||||
|
||||
export class DefaultIterableDiffer implements IterableDiffer {
|
||||
private _collection = null;
|
||||
private _length: number = null;
|
||||
private _collection = null;
|
||||
// Keeps track of the used records at any point in time (during & across `_check()` calls)
|
||||
private _linkedRecords: _DuplicateMap = null;
|
||||
// Keeps track of the removed records at any point in time during `_check()` calls.
|
||||
@ -42,6 +41,10 @@ export class DefaultIterableDiffer implements IterableDiffer {
|
||||
private _removalsHead: CollectionChangeRecord = null;
|
||||
private _removalsTail: CollectionChangeRecord = null;
|
||||
|
||||
constructor(private _trackByFn?: TrackByFn) {
|
||||
this._trackByFn = isPresent(this._trackByFn) ? this._trackByFn : trackByIdentity;
|
||||
}
|
||||
|
||||
get collection() { return this._collection; }
|
||||
|
||||
get length(): number { return this._length; }
|
||||
@ -104,31 +107,37 @@ export class DefaultIterableDiffer implements IterableDiffer {
|
||||
var mayBeDirty: boolean = false;
|
||||
var index: number;
|
||||
var item;
|
||||
|
||||
var itemTrackBy;
|
||||
if (isArray(collection)) {
|
||||
var list = collection;
|
||||
this._length = collection.length;
|
||||
|
||||
for (index = 0; index < this._length; index++) {
|
||||
item = list[index];
|
||||
if (record === null || !looseIdentical(record.item, item)) {
|
||||
record = this._mismatch(record, item, index);
|
||||
itemTrackBy = this._trackByFn(index, item);
|
||||
if (record === null || !looseIdentical(record.trackById, itemTrackBy)) {
|
||||
record = this._mismatch(record, item, itemTrackBy, index);
|
||||
mayBeDirty = true;
|
||||
} else if (mayBeDirty) {
|
||||
// TODO(misko): can we limit this to duplicates only?
|
||||
record = this._verifyReinsertion(record, item, index);
|
||||
} else {
|
||||
if (mayBeDirty) {
|
||||
// TODO(misko): can we limit this to duplicates only?
|
||||
record = this._verifyReinsertion(record, item, itemTrackBy, index);
|
||||
}
|
||||
record.item = item;
|
||||
}
|
||||
|
||||
record = record._next;
|
||||
}
|
||||
} else {
|
||||
index = 0;
|
||||
iterateListLike(collection, (item) => {
|
||||
if (record === null || !looseIdentical(record.item, item)) {
|
||||
record = this._mismatch(record, item, index);
|
||||
itemTrackBy = this._trackByFn(index, item);
|
||||
if (record === null || !looseIdentical(record.trackById, itemTrackBy)) {
|
||||
record = this._mismatch(record, item, itemTrackBy, index);
|
||||
mayBeDirty = true;
|
||||
} else if (mayBeDirty) {
|
||||
// TODO(misko): can we limit this to duplicates only?
|
||||
record = this._verifyReinsertion(record, item, index);
|
||||
record = this._verifyReinsertion(record, item, itemTrackBy, index);
|
||||
}
|
||||
record = record._next;
|
||||
index++;
|
||||
@ -190,7 +199,8 @@ export class DefaultIterableDiffer implements IterableDiffer {
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
_mismatch(record: CollectionChangeRecord, item, index: number): CollectionChangeRecord {
|
||||
_mismatch(record: CollectionChangeRecord, item: any, itemTrackBy: any,
|
||||
index: number): CollectionChangeRecord {
|
||||
// The previous record after which we will append the current one.
|
||||
var previousRecord: CollectionChangeRecord;
|
||||
|
||||
@ -203,19 +213,20 @@ export class DefaultIterableDiffer implements IterableDiffer {
|
||||
}
|
||||
|
||||
// Attempt to see if we have seen the item before.
|
||||
record = this._linkedRecords === null ? null : this._linkedRecords.get(item, index);
|
||||
record = this._linkedRecords === null ? null : this._linkedRecords.get(itemTrackBy, index);
|
||||
if (record !== null) {
|
||||
// We have seen this before, we need to move it forward in the collection.
|
||||
this._moveAfter(record, previousRecord, index);
|
||||
} else {
|
||||
// Never seen it, check evicted list.
|
||||
record = this._unlinkedRecords === null ? null : this._unlinkedRecords.get(item);
|
||||
record = this._unlinkedRecords === null ? null : this._unlinkedRecords.get(itemTrackBy);
|
||||
if (record !== null) {
|
||||
// It is an item which we have evicted earlier: reinsert it back into the list.
|
||||
this._reinsertAfter(record, previousRecord, index);
|
||||
} else {
|
||||
// It is a new item: add it.
|
||||
record = this._addAfter(new CollectionChangeRecord(item), previousRecord, index);
|
||||
record =
|
||||
this._addAfter(new CollectionChangeRecord(item, itemTrackBy), previousRecord, index);
|
||||
}
|
||||
}
|
||||
return record;
|
||||
@ -248,15 +259,17 @@ export class DefaultIterableDiffer implements IterableDiffer {
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
_verifyReinsertion(record: CollectionChangeRecord, item, index: number): CollectionChangeRecord {
|
||||
_verifyReinsertion(record: CollectionChangeRecord, item: any, itemTrackBy: any,
|
||||
index: number): CollectionChangeRecord {
|
||||
var reinsertRecord: CollectionChangeRecord =
|
||||
this._unlinkedRecords === null ? null : this._unlinkedRecords.get(item);
|
||||
this._unlinkedRecords === null ? null : this._unlinkedRecords.get(itemTrackBy);
|
||||
if (reinsertRecord !== null) {
|
||||
record = this._reinsertAfter(reinsertRecord, record._prev, index);
|
||||
} else if (record.currentIndex != index) {
|
||||
record.currentIndex = index;
|
||||
this._addToMoves(record, index);
|
||||
}
|
||||
record.item = item;
|
||||
return record;
|
||||
}
|
||||
|
||||
@ -457,31 +470,20 @@ export class DefaultIterableDiffer implements IterableDiffer {
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
var record: CollectionChangeRecord;
|
||||
|
||||
var list = [];
|
||||
for (record = this._itHead; record !== null; record = record._next) {
|
||||
list.push(record);
|
||||
}
|
||||
this.forEachItem((record) => list.push(record));
|
||||
|
||||
var previous = [];
|
||||
for (record = this._previousItHead; record !== null; record = record._nextPrevious) {
|
||||
previous.push(record);
|
||||
}
|
||||
this.forEachPreviousItem((record) => previous.push(record));
|
||||
|
||||
var additions = [];
|
||||
for (record = this._additionsHead; record !== null; record = record._nextAdded) {
|
||||
additions.push(record);
|
||||
}
|
||||
this.forEachAddedItem((record) => additions.push(record));
|
||||
|
||||
var moves = [];
|
||||
for (record = this._movesHead; record !== null; record = record._nextMoved) {
|
||||
moves.push(record);
|
||||
}
|
||||
this.forEachMovedItem((record) => moves.push(record));
|
||||
|
||||
var removals = [];
|
||||
for (record = this._removalsHead; record !== null; record = record._nextRemoved) {
|
||||
removals.push(record);
|
||||
}
|
||||
this.forEachRemovedItem((record) => removals.push(record));
|
||||
|
||||
return "collection: " + list.join(', ') + "\n" + "previous: " + previous.join(', ') + "\n" +
|
||||
"additions: " + additions.join(', ') + "\n" + "moves: " + moves.join(', ') + "\n" +
|
||||
@ -512,7 +514,7 @@ export class CollectionChangeRecord {
|
||||
/** @internal */
|
||||
_nextMoved: CollectionChangeRecord = null;
|
||||
|
||||
constructor(public item: any) {}
|
||||
constructor(public item: any, public trackById: any) {}
|
||||
|
||||
toString(): string {
|
||||
return this.previousIndex === this.currentIndex ?
|
||||
@ -550,13 +552,13 @@ class _DuplicateItemRecordList {
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a CollectionChangeRecord having CollectionChangeRecord.item == item and
|
||||
// Returns a CollectionChangeRecord having CollectionChangeRecord.trackById == trackById and
|
||||
// CollectionChangeRecord.currentIndex >= afterIndex
|
||||
get(item: any, afterIndex: number): CollectionChangeRecord {
|
||||
get(trackById: any, afterIndex: number): CollectionChangeRecord {
|
||||
var record: CollectionChangeRecord;
|
||||
for (record = this._head; record !== null; record = record._nextDup) {
|
||||
if ((afterIndex === null || afterIndex < record.currentIndex) &&
|
||||
looseIdentical(record.item, item)) {
|
||||
looseIdentical(record.trackById, trackById)) {
|
||||
return record;
|
||||
}
|
||||
}
|
||||
@ -599,7 +601,7 @@ class _DuplicateMap {
|
||||
|
||||
put(record: CollectionChangeRecord) {
|
||||
// todo(vicb) handle corner cases
|
||||
var key = getMapKey(record.item);
|
||||
var key = getMapKey(record.trackById);
|
||||
|
||||
var duplicates = this.map.get(key);
|
||||
if (!isPresent(duplicates)) {
|
||||
@ -610,17 +612,17 @@ class _DuplicateMap {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the `value` using key. Because the CollectionChangeRecord value maybe one which we
|
||||
* Retrieve the `value` using key. Because the CollectionChangeRecord value may be one which we
|
||||
* have already iterated over, we use the afterIndex to pretend it is not there.
|
||||
*
|
||||
* Use case: `[a, b, c, a, a]` if we are at index `3` which is the second `a` then asking if we
|
||||
* have any more `a`s needs to return the last `a` not the first or second.
|
||||
*/
|
||||
get(value: any, afterIndex: number = null): CollectionChangeRecord {
|
||||
var key = getMapKey(value);
|
||||
get(trackById: any, afterIndex: number = null): CollectionChangeRecord {
|
||||
var key = getMapKey(trackById);
|
||||
|
||||
var recordList = this.map.get(key);
|
||||
return isBlank(recordList) ? null : recordList.get(value, afterIndex);
|
||||
return isBlank(recordList) ? null : recordList.get(trackById, afterIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -629,7 +631,7 @@ class _DuplicateMap {
|
||||
* The list of duplicates also is removed from the map if it gets empty.
|
||||
*/
|
||||
remove(record: CollectionChangeRecord): CollectionChangeRecord {
|
||||
var key = getMapKey(record.item);
|
||||
var key = getMapKey(record.trackById);
|
||||
// todo(vicb)
|
||||
// assert(this.map.containsKey(key));
|
||||
var recordList: _DuplicateItemRecordList = this.map.get(key);
|
||||
|
@ -13,12 +13,19 @@ export interface IterableDiffer {
|
||||
onDestroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* An optional function passed into {@link NgFor} that defines how to track
|
||||
* items in an iterable (e.g. by index or id)
|
||||
*/
|
||||
export interface TrackByFn { (index: number, item: any): any; }
|
||||
|
||||
|
||||
/**
|
||||
* Provides a factory for {@link IterableDiffer}.
|
||||
*/
|
||||
export interface IterableDifferFactory {
|
||||
supports(objects: any): boolean;
|
||||
create(cdRef: ChangeDetectorRef): IterableDiffer;
|
||||
create(cdRef: ChangeDetectorRef, trackByFn?: TrackByFn): IterableDiffer;
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user