refactor: move angular source to /packages rather than modules/@angular
This commit is contained in:
39
packages/core/src/change_detection/change_detection.ts
Normal file
39
packages/core/src/change_detection/change_detection.ts
Normal file
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {DefaultIterableDifferFactory} from './differs/default_iterable_differ';
|
||||
import {DefaultKeyValueDifferFactory} from './differs/default_keyvalue_differ';
|
||||
import {IterableDifferFactory, IterableDiffers} from './differs/iterable_differs';
|
||||
import {KeyValueDifferFactory, KeyValueDiffers} from './differs/keyvalue_differs';
|
||||
|
||||
export {SimpleChanges} from '../metadata/lifecycle_hooks';
|
||||
export {SimpleChange, ValueUnwrapper, WrappedValue, devModeEqual} from './change_detection_util';
|
||||
export {ChangeDetectorRef} from './change_detector_ref';
|
||||
export {ChangeDetectionStrategy, ChangeDetectorStatus, isDefaultChangeDetectionStrategy} from './constants';
|
||||
export {DefaultIterableDifferFactory} from './differs/default_iterable_differ';
|
||||
export {DefaultIterableDiffer} from './differs/default_iterable_differ';
|
||||
export {DefaultKeyValueDifferFactory} from './differs/default_keyvalue_differ';
|
||||
export {CollectionChangeRecord, IterableChangeRecord, IterableChanges, IterableDiffer, IterableDifferFactory, IterableDiffers, NgIterable, TrackByFn, TrackByFunction} from './differs/iterable_differs';
|
||||
export {KeyValueChangeRecord, KeyValueChanges, KeyValueDiffer, KeyValueDifferFactory, KeyValueDiffers} from './differs/keyvalue_differs';
|
||||
export {PipeTransform} from './pipe_transform';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Structural diffing for `Object`s and `Map`s.
|
||||
*/
|
||||
const keyValDiff: KeyValueDifferFactory[] = [new DefaultKeyValueDifferFactory()];
|
||||
|
||||
/**
|
||||
* Structural diffing for `Iterable` types such as `Array`s.
|
||||
*/
|
||||
const iterableDiff: IterableDifferFactory[] = [new DefaultIterableDifferFactory()];
|
||||
|
||||
export const defaultIterableDiffers = new IterableDiffers(iterableDiff);
|
||||
|
||||
export const defaultKeyValueDiffers = new KeyValueDiffers(keyValDiff);
|
119
packages/core/src/change_detection/change_detection_util.ts
Normal file
119
packages/core/src/change_detection/change_detection_util.ts
Normal file
@ -0,0 +1,119 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {getSymbolIterator, looseIdentical} from '../util';
|
||||
|
||||
export function devModeEqual(a: any, b: any): boolean {
|
||||
const isListLikeIterableA = isListLikeIterable(a);
|
||||
const isListLikeIterableB = isListLikeIterable(b);
|
||||
if (isListLikeIterableA && isListLikeIterableB) {
|
||||
return areIterablesEqual(a, b, devModeEqual);
|
||||
} else {
|
||||
const isAObject = a && (typeof a === 'object' || typeof a === 'function');
|
||||
const isBObject = b && (typeof b === 'object' || typeof b === 'function');
|
||||
if (!isListLikeIterableA && isAObject && !isListLikeIterableB && isBObject) {
|
||||
return true;
|
||||
} else {
|
||||
return looseIdentical(a, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that the result of a {@link Pipe} transformation has changed even though the
|
||||
* reference
|
||||
* has not changed.
|
||||
*
|
||||
* The wrapped value will be unwrapped by change detection, and the unwrapped value will be stored.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* if (this._latestValue === this._latestReturnedValue) {
|
||||
* return this._latestReturnedValue;
|
||||
* } else {
|
||||
* this._latestReturnedValue = this._latestValue;
|
||||
* return WrappedValue.wrap(this._latestValue); // this will force update
|
||||
* }
|
||||
* ```
|
||||
* @stable
|
||||
*/
|
||||
export class WrappedValue {
|
||||
constructor(public wrapped: any) {}
|
||||
|
||||
static wrap(value: any): WrappedValue { return new WrappedValue(value); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for unwrapping WrappedValue s
|
||||
*/
|
||||
export class ValueUnwrapper {
|
||||
public hasWrappedValue = false;
|
||||
|
||||
unwrap(value: any): any {
|
||||
if (value instanceof WrappedValue) {
|
||||
this.hasWrappedValue = true;
|
||||
return value.wrapped;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
reset() { this.hasWrappedValue = false; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a basic change from a previous to a new value.
|
||||
* @stable
|
||||
*/
|
||||
export class SimpleChange {
|
||||
constructor(public previousValue: any, public currentValue: any, public firstChange: boolean) {}
|
||||
|
||||
/**
|
||||
* Check whether the new value is the first value assigned.
|
||||
*/
|
||||
isFirstChange(): boolean { return this.firstChange; }
|
||||
}
|
||||
|
||||
export function isListLikeIterable(obj: any): boolean {
|
||||
if (!isJsObject(obj)) return false;
|
||||
return Array.isArray(obj) ||
|
||||
(!(obj instanceof Map) && // JS Map are iterables but return entries as [k, v]
|
||||
getSymbolIterator() in obj); // JS Iterable have a Symbol.iterator prop
|
||||
}
|
||||
|
||||
export function areIterablesEqual(
|
||||
a: any, b: any, comparator: (a: any, b: any) => boolean): boolean {
|
||||
const iterator1 = a[getSymbolIterator()]();
|
||||
const iterator2 = b[getSymbolIterator()]();
|
||||
|
||||
while (true) {
|
||||
const item1 = iterator1.next();
|
||||
const item2 = iterator2.next();
|
||||
if (item1.done && item2.done) return true;
|
||||
if (item1.done || item2.done) return false;
|
||||
if (!comparator(item1.value, item2.value)) return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function iterateListLike(obj: any, fn: (p: any) => any) {
|
||||
if (Array.isArray(obj)) {
|
||||
for (let i = 0; i < obj.length; i++) {
|
||||
fn(obj[i]);
|
||||
}
|
||||
} else {
|
||||
const iterator = obj[getSymbolIterator()]();
|
||||
let item: any;
|
||||
while (!((item = iterator.next()).done)) {
|
||||
fn(item.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isJsObject(o: any): boolean {
|
||||
return o !== null && (typeof o === 'function' || typeof o === 'object');
|
||||
}
|
193
packages/core/src/change_detection/change_detector_ref.ts
Normal file
193
packages/core/src/change_detection/change_detector_ref.ts
Normal file
@ -0,0 +1,193 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* @stable
|
||||
*/
|
||||
export abstract class ChangeDetectorRef {
|
||||
/**
|
||||
* Marks all {@link ChangeDetectionStrategy#OnPush} ancestors as to be checked.
|
||||
*
|
||||
* <!-- TODO: Add a link to a chapter on OnPush components -->
|
||||
*
|
||||
* ### Example ([live demo](http://plnkr.co/edit/GC512b?p=preview))
|
||||
*
|
||||
* ```typescript
|
||||
* @Component({
|
||||
* selector: 'cmp',
|
||||
* changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
* template: `Number of ticks: {{numberOfTicks}}`
|
||||
* })
|
||||
* class Cmp {
|
||||
* numberOfTicks = 0;
|
||||
*
|
||||
* constructor(ref: ChangeDetectorRef) {
|
||||
* setInterval(() => {
|
||||
* this.numberOfTicks ++
|
||||
* // the following is required, otherwise the view will not be updated
|
||||
* this.ref.markForCheck();
|
||||
* }, 1000);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @Component({
|
||||
* selector: 'app',
|
||||
* changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
* template: `
|
||||
* <cmp><cmp>
|
||||
* `,
|
||||
* })
|
||||
* class App {
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
abstract markForCheck(): void;
|
||||
|
||||
/**
|
||||
* Detaches the change detector from the change detector tree.
|
||||
*
|
||||
* The detached change detector will not be checked until it is reattached.
|
||||
*
|
||||
* This can also be used in combination with {@link ChangeDetectorRef#detectChanges} to implement
|
||||
* local change
|
||||
* detection checks.
|
||||
*
|
||||
* <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->
|
||||
* <!-- TODO: Add a live demo once ref.detectChanges is merged into master -->
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* The following example defines a component with a large list of readonly data.
|
||||
* Imagine the data changes constantly, many times per second. For performance reasons,
|
||||
* we want to check and update the list every five seconds. We can do that by detaching
|
||||
* the component's change detector and doing a local check every five seconds.
|
||||
*
|
||||
* ```typescript
|
||||
* class DataProvider {
|
||||
* // in a real application the returned data will be different every time
|
||||
* get data() {
|
||||
* return [1,2,3,4,5];
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @Component({
|
||||
* selector: 'giant-list',
|
||||
* template: `
|
||||
* <li *ngFor="let d of dataProvider.data">Data {{d}}</lig>
|
||||
* `,
|
||||
* })
|
||||
* class GiantList {
|
||||
* constructor(private ref: ChangeDetectorRef, private dataProvider:DataProvider) {
|
||||
* ref.detach();
|
||||
* setInterval(() => {
|
||||
* this.ref.detectChanges();
|
||||
* }, 5000);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @Component({
|
||||
* selector: 'app',
|
||||
* providers: [DataProvider],
|
||||
* template: `
|
||||
* <giant-list><giant-list>
|
||||
* `,
|
||||
* })
|
||||
* class App {
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
abstract detach(): void;
|
||||
|
||||
/**
|
||||
* Checks the change detector and its children.
|
||||
*
|
||||
* This can also be used in combination with {@link ChangeDetectorRef#detach} to implement local
|
||||
* change detection
|
||||
* checks.
|
||||
*
|
||||
* <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->
|
||||
* <!-- TODO: Add a live demo once ref.detectChanges is merged into master -->
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* The following example defines a component with a large list of readonly data.
|
||||
* Imagine, the data changes constantly, many times per second. For performance reasons,
|
||||
* we want to check and update the list every five seconds.
|
||||
*
|
||||
* We can do that by detaching the component's change detector and doing a local change detection
|
||||
* check
|
||||
* every five seconds.
|
||||
*
|
||||
* See {@link ChangeDetectorRef#detach} for more information.
|
||||
*/
|
||||
abstract detectChanges(): void;
|
||||
|
||||
/**
|
||||
* Checks the change detector and its children, and throws if any changes are detected.
|
||||
*
|
||||
* This is used in development mode to verify that running change detection doesn't introduce
|
||||
* other changes.
|
||||
*/
|
||||
abstract checkNoChanges(): void;
|
||||
|
||||
/**
|
||||
* Reattach the change detector to the change detector tree.
|
||||
*
|
||||
* This also marks OnPush ancestors as to be checked. This reattached change detector will be
|
||||
* checked during the next change detection run.
|
||||
*
|
||||
* <!-- TODO: Add a link to a chapter on detach/reattach/local digest -->
|
||||
*
|
||||
* ### Example ([live demo](http://plnkr.co/edit/aUhZha?p=preview))
|
||||
*
|
||||
* The following example creates a component displaying `live` data. The component will detach
|
||||
* its change detector from the main change detector tree when the component's live property
|
||||
* is set to false.
|
||||
*
|
||||
* ```typescript
|
||||
* class DataProvider {
|
||||
* data = 1;
|
||||
*
|
||||
* constructor() {
|
||||
* setInterval(() => {
|
||||
* this.data = this.data * 2;
|
||||
* }, 500);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @Component({
|
||||
* selector: 'live-data',
|
||||
* inputs: ['live'],
|
||||
* template: 'Data: {{dataProvider.data}}'
|
||||
* })
|
||||
* class LiveData {
|
||||
* constructor(private ref: ChangeDetectorRef, private dataProvider:DataProvider) {}
|
||||
*
|
||||
* set live(value) {
|
||||
* if (value)
|
||||
* this.ref.reattach();
|
||||
* else
|
||||
* this.ref.detach();
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @Component({
|
||||
* selector: 'app',
|
||||
* providers: [DataProvider],
|
||||
* template: `
|
||||
* Live Update: <input type="checkbox" [(ngModel)]="live">
|
||||
* <live-data [live]="live"><live-data>
|
||||
* `,
|
||||
* })
|
||||
* class App {
|
||||
* live = true;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
abstract reattach(): void;
|
||||
}
|
72
packages/core/src/change_detection/constants.ts
Normal file
72
packages/core/src/change_detection/constants.ts
Normal file
@ -0,0 +1,72 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Describes within the change detector which strategy will be used the next time change
|
||||
* detection is triggered.
|
||||
* @stable
|
||||
*/
|
||||
export enum ChangeDetectionStrategy {
|
||||
/**
|
||||
* `OnPush` means that the change detector's mode will be set to `CheckOnce` during hydration.
|
||||
*/
|
||||
OnPush,
|
||||
|
||||
/**
|
||||
* `Default` means that the change detector's mode will be set to `CheckAlways` during hydration.
|
||||
*/
|
||||
Default,
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the status of the detector.
|
||||
*/
|
||||
export enum ChangeDetectorStatus {
|
||||
/**
|
||||
* `CheckOnce` means that after calling detectChanges the mode of the change detector
|
||||
* will become `Checked`.
|
||||
*/
|
||||
CheckOnce,
|
||||
|
||||
/**
|
||||
* `Checked` means that the change detector should be skipped until its mode changes to
|
||||
* `CheckOnce`.
|
||||
*/
|
||||
Checked,
|
||||
|
||||
/**
|
||||
* `CheckAlways` means that after calling detectChanges the mode of the change detector
|
||||
* will remain `CheckAlways`.
|
||||
*/
|
||||
CheckAlways,
|
||||
|
||||
/**
|
||||
* `Detached` means that the change detector sub tree is not a part of the main tree and
|
||||
* should be skipped.
|
||||
*/
|
||||
Detached,
|
||||
|
||||
/**
|
||||
* `Errored` means that the change detector encountered an error checking a binding
|
||||
* or calling a directive lifecycle method and is now in an inconsistent state. Change
|
||||
* detectors in this state will no longer detect changes.
|
||||
*/
|
||||
Errored,
|
||||
|
||||
/**
|
||||
* `Destroyed` means that the change detector is destroyed.
|
||||
*/
|
||||
Destroyed,
|
||||
}
|
||||
|
||||
export function isDefaultChangeDetectionStrategy(changeDetectionStrategy: ChangeDetectionStrategy):
|
||||
boolean {
|
||||
return changeDetectionStrategy == null ||
|
||||
changeDetectionStrategy === ChangeDetectionStrategy.Default;
|
||||
}
|
@ -0,0 +1,764 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {looseIdentical, stringify} from '../../util';
|
||||
import {isListLikeIterable, iterateListLike} from '../change_detection_util';
|
||||
import {ChangeDetectorRef} from '../change_detector_ref';
|
||||
|
||||
import {IterableChangeRecord, IterableChanges, IterableDiffer, IterableDifferFactory, NgIterable, TrackByFunction} from './iterable_differs';
|
||||
|
||||
|
||||
export class DefaultIterableDifferFactory implements IterableDifferFactory {
|
||||
constructor() {}
|
||||
supports(obj: Object): boolean { return isListLikeIterable(obj); }
|
||||
|
||||
create<V>(trackByFn?: TrackByFunction<any>): DefaultIterableDiffer<V>;
|
||||
|
||||
/**
|
||||
* @deprecated v4.0.0 - ChangeDetectorRef is not used and is no longer a parameter
|
||||
*/
|
||||
create<V>(
|
||||
cdRefOrTrackBy?: ChangeDetectorRef|TrackByFunction<any>,
|
||||
trackByFn?: TrackByFunction<any>): DefaultIterableDiffer<V> {
|
||||
return new DefaultIterableDiffer<V>(trackByFn || <TrackByFunction<any>>cdRefOrTrackBy);
|
||||
}
|
||||
}
|
||||
|
||||
const trackByIdentity = (index: number, item: any) => item;
|
||||
|
||||
/**
|
||||
* @deprecated v4.0.0 - Should not be part of public API.
|
||||
*/
|
||||
export class DefaultIterableDiffer<V> implements IterableDiffer<V>, IterableChanges<V> {
|
||||
private _length: number = null;
|
||||
private _collection: NgIterable<V> = null;
|
||||
// Keeps track of the used records at any point in time (during & across `_check()` calls)
|
||||
private _linkedRecords: _DuplicateMap<V> = null;
|
||||
// Keeps track of the removed records at any point in time during `_check()` calls.
|
||||
private _unlinkedRecords: _DuplicateMap<V> = null;
|
||||
private _previousItHead: IterableChangeRecord_<V> = null;
|
||||
private _itHead: IterableChangeRecord_<V> = null;
|
||||
private _itTail: IterableChangeRecord_<V> = null;
|
||||
private _additionsHead: IterableChangeRecord_<V> = null;
|
||||
private _additionsTail: IterableChangeRecord_<V> = null;
|
||||
private _movesHead: IterableChangeRecord_<V> = null;
|
||||
private _movesTail: IterableChangeRecord_<V> = null;
|
||||
private _removalsHead: IterableChangeRecord_<V> = null;
|
||||
private _removalsTail: IterableChangeRecord_<V> = null;
|
||||
// Keeps track of records where custom track by is the same, but item identity has changed
|
||||
private _identityChangesHead: IterableChangeRecord_<V> = null;
|
||||
private _identityChangesTail: IterableChangeRecord_<V> = null;
|
||||
|
||||
constructor(private _trackByFn?: TrackByFunction<V>) {
|
||||
this._trackByFn = this._trackByFn || trackByIdentity;
|
||||
}
|
||||
|
||||
get collection() { return this._collection; }
|
||||
|
||||
get length(): number { return this._length; }
|
||||
|
||||
forEachItem(fn: (record: IterableChangeRecord_<V>) => void) {
|
||||
let record: IterableChangeRecord_<V>;
|
||||
for (record = this._itHead; record !== null; record = record._next) {
|
||||
fn(record);
|
||||
}
|
||||
}
|
||||
|
||||
forEachOperation(
|
||||
fn: (item: IterableChangeRecord_<V>, previousIndex: number, currentIndex: number) => void) {
|
||||
let nextIt = this._itHead;
|
||||
let nextRemove = this._removalsHead;
|
||||
let addRemoveOffset = 0;
|
||||
let moveOffsets: number[] = null;
|
||||
while (nextIt || nextRemove) {
|
||||
// Figure out which is the next record to process
|
||||
// Order: remove, add, move
|
||||
const record = !nextRemove ||
|
||||
nextIt &&
|
||||
nextIt.currentIndex < getPreviousIndex(nextRemove, addRemoveOffset, moveOffsets) ?
|
||||
nextIt :
|
||||
nextRemove;
|
||||
const adjPreviousIndex = getPreviousIndex(record, addRemoveOffset, moveOffsets);
|
||||
const currentIndex = record.currentIndex;
|
||||
|
||||
// consume the item, and adjust the addRemoveOffset and update moveDistance if necessary
|
||||
if (record === nextRemove) {
|
||||
addRemoveOffset--;
|
||||
nextRemove = nextRemove._nextRemoved;
|
||||
} else {
|
||||
nextIt = nextIt._next;
|
||||
if (record.previousIndex == null) {
|
||||
addRemoveOffset++;
|
||||
} else {
|
||||
// INVARIANT: currentIndex < previousIndex
|
||||
if (!moveOffsets) moveOffsets = [];
|
||||
const localMovePreviousIndex = adjPreviousIndex - addRemoveOffset;
|
||||
const localCurrentIndex = currentIndex - addRemoveOffset;
|
||||
if (localMovePreviousIndex != localCurrentIndex) {
|
||||
for (let i = 0; i < localMovePreviousIndex; i++) {
|
||||
const offset = i < moveOffsets.length ? moveOffsets[i] : (moveOffsets[i] = 0);
|
||||
const index = offset + i;
|
||||
if (localCurrentIndex <= index && index < localMovePreviousIndex) {
|
||||
moveOffsets[i] = offset + 1;
|
||||
}
|
||||
}
|
||||
const previousIndex = record.previousIndex;
|
||||
moveOffsets[previousIndex] = localCurrentIndex - localMovePreviousIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (adjPreviousIndex !== currentIndex) {
|
||||
fn(record, adjPreviousIndex, currentIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
forEachPreviousItem(fn: (record: IterableChangeRecord_<V>) => void) {
|
||||
let record: IterableChangeRecord_<V>;
|
||||
for (record = this._previousItHead; record !== null; record = record._nextPrevious) {
|
||||
fn(record);
|
||||
}
|
||||
}
|
||||
|
||||
forEachAddedItem(fn: (record: IterableChangeRecord_<V>) => void) {
|
||||
let record: IterableChangeRecord_<V>;
|
||||
for (record = this._additionsHead; record !== null; record = record._nextAdded) {
|
||||
fn(record);
|
||||
}
|
||||
}
|
||||
|
||||
forEachMovedItem(fn: (record: IterableChangeRecord_<V>) => void) {
|
||||
let record: IterableChangeRecord_<V>;
|
||||
for (record = this._movesHead; record !== null; record = record._nextMoved) {
|
||||
fn(record);
|
||||
}
|
||||
}
|
||||
|
||||
forEachRemovedItem(fn: (record: IterableChangeRecord_<V>) => void) {
|
||||
let record: IterableChangeRecord_<V>;
|
||||
for (record = this._removalsHead; record !== null; record = record._nextRemoved) {
|
||||
fn(record);
|
||||
}
|
||||
}
|
||||
|
||||
forEachIdentityChange(fn: (record: IterableChangeRecord_<V>) => void) {
|
||||
let record: IterableChangeRecord_<V>;
|
||||
for (record = this._identityChangesHead; record !== null; record = record._nextIdentityChange) {
|
||||
fn(record);
|
||||
}
|
||||
}
|
||||
|
||||
diff(collection: NgIterable<V>): DefaultIterableDiffer<V> {
|
||||
if (collection == null) collection = [];
|
||||
if (!isListLikeIterable(collection)) {
|
||||
throw new Error(`Error trying to diff '${collection}'`);
|
||||
}
|
||||
|
||||
if (this.check(collection)) {
|
||||
return this;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy() {}
|
||||
|
||||
// todo(vicb): optim for UnmodifiableListView (frozen arrays)
|
||||
check(collection: NgIterable<V>): boolean {
|
||||
this._reset();
|
||||
|
||||
let record: IterableChangeRecord_<V> = this._itHead;
|
||||
let mayBeDirty: boolean = false;
|
||||
let index: number;
|
||||
let item: V;
|
||||
let itemTrackBy: any;
|
||||
if (Array.isArray(collection)) {
|
||||
this._length = collection.length;
|
||||
|
||||
for (let index = 0; index < this._length; index++) {
|
||||
item = collection[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, itemTrackBy, index);
|
||||
}
|
||||
if (!looseIdentical(record.item, item)) this._addIdentityChange(record, item);
|
||||
}
|
||||
|
||||
record = record._next;
|
||||
}
|
||||
} else {
|
||||
index = 0;
|
||||
iterateListLike(collection, (item: V) => {
|
||||
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, itemTrackBy, index);
|
||||
}
|
||||
if (!looseIdentical(record.item, item)) this._addIdentityChange(record, item);
|
||||
}
|
||||
record = record._next;
|
||||
index++;
|
||||
});
|
||||
this._length = index;
|
||||
}
|
||||
|
||||
this._truncate(record);
|
||||
this._collection = collection;
|
||||
return this.isDirty;
|
||||
}
|
||||
|
||||
/* CollectionChanges is considered dirty if it has any additions, moves, removals, or identity
|
||||
* changes.
|
||||
*/
|
||||
get isDirty(): boolean {
|
||||
return this._additionsHead !== null || this._movesHead !== null ||
|
||||
this._removalsHead !== null || this._identityChangesHead !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the state of the change objects to show no changes. This means set previousKey to
|
||||
* currentKey, and clear all of the queues (additions, moves, removals).
|
||||
* Set the previousIndexes of moved and added items to their currentIndexes
|
||||
* Reset the list of additions, moves and removals
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
_reset() {
|
||||
if (this.isDirty) {
|
||||
let record: IterableChangeRecord_<V>;
|
||||
let nextRecord: IterableChangeRecord_<V>;
|
||||
|
||||
for (record = this._previousItHead = this._itHead; record !== null; record = record._next) {
|
||||
record._nextPrevious = record._next;
|
||||
}
|
||||
|
||||
for (record = this._additionsHead; record !== null; record = record._nextAdded) {
|
||||
record.previousIndex = record.currentIndex;
|
||||
}
|
||||
this._additionsHead = this._additionsTail = null;
|
||||
|
||||
for (record = this._movesHead; record !== null; record = nextRecord) {
|
||||
record.previousIndex = record.currentIndex;
|
||||
nextRecord = record._nextMoved;
|
||||
}
|
||||
this._movesHead = this._movesTail = null;
|
||||
this._removalsHead = this._removalsTail = null;
|
||||
this._identityChangesHead = this._identityChangesTail = null;
|
||||
|
||||
// todo(vicb) when assert gets supported
|
||||
// assert(!this.isDirty);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the core function which handles differences between collections.
|
||||
*
|
||||
* - `record` is the record which we saw at this position last time. If null then it is a new
|
||||
* item.
|
||||
* - `item` is the current item in the collection
|
||||
* - `index` is the position of the item in the collection
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
_mismatch(record: IterableChangeRecord_<V>, item: V, itemTrackBy: any, index: number):
|
||||
IterableChangeRecord_<V> {
|
||||
// The previous record after which we will append the current one.
|
||||
let previousRecord: IterableChangeRecord_<V>;
|
||||
|
||||
if (record === null) {
|
||||
previousRecord = this._itTail;
|
||||
} else {
|
||||
previousRecord = record._prev;
|
||||
// Remove the record from the collection since we know it does not match the item.
|
||||
this._remove(record);
|
||||
}
|
||||
|
||||
// Attempt to see if we have seen the item before.
|
||||
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.
|
||||
// But first we need to check if identity changed, so we can update in view if necessary
|
||||
if (!looseIdentical(record.item, item)) this._addIdentityChange(record, item);
|
||||
|
||||
this._moveAfter(record, previousRecord, index);
|
||||
} else {
|
||||
// Never seen it, check evicted list.
|
||||
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.
|
||||
// But first we need to check if identity changed, so we can update in view if necessary
|
||||
if (!looseIdentical(record.item, item)) this._addIdentityChange(record, item);
|
||||
|
||||
this._reinsertAfter(record, previousRecord, index);
|
||||
} else {
|
||||
// It is a new item: add it.
|
||||
record =
|
||||
this._addAfter(new IterableChangeRecord_<V>(item, itemTrackBy), previousRecord, index);
|
||||
}
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* This check is only needed if an array contains duplicates. (Short circuit of nothing dirty)
|
||||
*
|
||||
* Use case: `[a, a]` => `[b, a, a]`
|
||||
*
|
||||
* If we did not have this check then the insertion of `b` would:
|
||||
* 1) evict first `a`
|
||||
* 2) insert `b` at `0` index.
|
||||
* 3) leave `a` at index `1` as is. <-- this is wrong!
|
||||
* 3) reinsert `a` at index 2. <-- this is wrong!
|
||||
*
|
||||
* The correct behavior is:
|
||||
* 1) evict first `a`
|
||||
* 2) insert `b` at `0` index.
|
||||
* 3) reinsert `a` at index 1.
|
||||
* 3) move `a` at from `1` to `2`.
|
||||
*
|
||||
*
|
||||
* Double check that we have not evicted a duplicate item. We need to check if the item type may
|
||||
* have already been removed:
|
||||
* The insertion of b will evict the first 'a'. If we don't reinsert it now it will be reinserted
|
||||
* at the end. Which will show up as the two 'a's switching position. This is incorrect, since a
|
||||
* better way to think of it is as insert of 'b' rather then switch 'a' with 'b' and then add 'a'
|
||||
* at the end.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
_verifyReinsertion(record: IterableChangeRecord_<V>, item: V, itemTrackBy: any, index: number):
|
||||
IterableChangeRecord_<V> {
|
||||
let reinsertRecord: IterableChangeRecord_<V> =
|
||||
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);
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rid of any excess {@link IterableChangeRecord_}s from the previous collection
|
||||
*
|
||||
* - `record` The first excess {@link IterableChangeRecord_}.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
_truncate(record: IterableChangeRecord_<V>) {
|
||||
// Anything after that needs to be removed;
|
||||
while (record !== null) {
|
||||
const nextRecord: IterableChangeRecord_<V> = record._next;
|
||||
this._addToRemovals(this._unlink(record));
|
||||
record = nextRecord;
|
||||
}
|
||||
if (this._unlinkedRecords !== null) {
|
||||
this._unlinkedRecords.clear();
|
||||
}
|
||||
|
||||
if (this._additionsTail !== null) {
|
||||
this._additionsTail._nextAdded = null;
|
||||
}
|
||||
if (this._movesTail !== null) {
|
||||
this._movesTail._nextMoved = null;
|
||||
}
|
||||
if (this._itTail !== null) {
|
||||
this._itTail._next = null;
|
||||
}
|
||||
if (this._removalsTail !== null) {
|
||||
this._removalsTail._nextRemoved = null;
|
||||
}
|
||||
if (this._identityChangesTail !== null) {
|
||||
this._identityChangesTail._nextIdentityChange = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_reinsertAfter(
|
||||
record: IterableChangeRecord_<V>, prevRecord: IterableChangeRecord_<V>,
|
||||
index: number): IterableChangeRecord_<V> {
|
||||
if (this._unlinkedRecords !== null) {
|
||||
this._unlinkedRecords.remove(record);
|
||||
}
|
||||
const prev = record._prevRemoved;
|
||||
const next = record._nextRemoved;
|
||||
|
||||
if (prev === null) {
|
||||
this._removalsHead = next;
|
||||
} else {
|
||||
prev._nextRemoved = next;
|
||||
}
|
||||
if (next === null) {
|
||||
this._removalsTail = prev;
|
||||
} else {
|
||||
next._prevRemoved = prev;
|
||||
}
|
||||
|
||||
this._insertAfter(record, prevRecord, index);
|
||||
this._addToMoves(record, index);
|
||||
return record;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_moveAfter(record: IterableChangeRecord_<V>, prevRecord: IterableChangeRecord_<V>, index: number):
|
||||
IterableChangeRecord_<V> {
|
||||
this._unlink(record);
|
||||
this._insertAfter(record, prevRecord, index);
|
||||
this._addToMoves(record, index);
|
||||
return record;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_addAfter(record: IterableChangeRecord_<V>, prevRecord: IterableChangeRecord_<V>, index: number):
|
||||
IterableChangeRecord_<V> {
|
||||
this._insertAfter(record, prevRecord, index);
|
||||
|
||||
if (this._additionsTail === null) {
|
||||
// todo(vicb)
|
||||
// assert(this._additionsHead === null);
|
||||
this._additionsTail = this._additionsHead = record;
|
||||
} else {
|
||||
// todo(vicb)
|
||||
// assert(_additionsTail._nextAdded === null);
|
||||
// assert(record._nextAdded === null);
|
||||
this._additionsTail = this._additionsTail._nextAdded = record;
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_insertAfter(
|
||||
record: IterableChangeRecord_<V>, prevRecord: IterableChangeRecord_<V>,
|
||||
index: number): IterableChangeRecord_<V> {
|
||||
// todo(vicb)
|
||||
// assert(record != prevRecord);
|
||||
// assert(record._next === null);
|
||||
// assert(record._prev === null);
|
||||
|
||||
const next: IterableChangeRecord_<V> = prevRecord === null ? this._itHead : prevRecord._next;
|
||||
// todo(vicb)
|
||||
// assert(next != record);
|
||||
// assert(prevRecord != record);
|
||||
record._next = next;
|
||||
record._prev = prevRecord;
|
||||
if (next === null) {
|
||||
this._itTail = record;
|
||||
} else {
|
||||
next._prev = record;
|
||||
}
|
||||
if (prevRecord === null) {
|
||||
this._itHead = record;
|
||||
} else {
|
||||
prevRecord._next = record;
|
||||
}
|
||||
|
||||
if (this._linkedRecords === null) {
|
||||
this._linkedRecords = new _DuplicateMap<V>();
|
||||
}
|
||||
this._linkedRecords.put(record);
|
||||
|
||||
record.currentIndex = index;
|
||||
return record;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_remove(record: IterableChangeRecord_<V>): IterableChangeRecord_<V> {
|
||||
return this._addToRemovals(this._unlink(record));
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_unlink(record: IterableChangeRecord_<V>): IterableChangeRecord_<V> {
|
||||
if (this._linkedRecords !== null) {
|
||||
this._linkedRecords.remove(record);
|
||||
}
|
||||
|
||||
const prev = record._prev;
|
||||
const next = record._next;
|
||||
|
||||
// todo(vicb)
|
||||
// assert((record._prev = null) === null);
|
||||
// assert((record._next = null) === null);
|
||||
|
||||
if (prev === null) {
|
||||
this._itHead = next;
|
||||
} else {
|
||||
prev._next = next;
|
||||
}
|
||||
if (next === null) {
|
||||
this._itTail = prev;
|
||||
} else {
|
||||
next._prev = prev;
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_addToMoves(record: IterableChangeRecord_<V>, toIndex: number): IterableChangeRecord_<V> {
|
||||
// todo(vicb)
|
||||
// assert(record._nextMoved === null);
|
||||
|
||||
if (record.previousIndex === toIndex) {
|
||||
return record;
|
||||
}
|
||||
|
||||
if (this._movesTail === null) {
|
||||
// todo(vicb)
|
||||
// assert(_movesHead === null);
|
||||
this._movesTail = this._movesHead = record;
|
||||
} else {
|
||||
// todo(vicb)
|
||||
// assert(_movesTail._nextMoved === null);
|
||||
this._movesTail = this._movesTail._nextMoved = record;
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
private _addToRemovals(record: IterableChangeRecord_<V>): IterableChangeRecord_<V> {
|
||||
if (this._unlinkedRecords === null) {
|
||||
this._unlinkedRecords = new _DuplicateMap<V>();
|
||||
}
|
||||
this._unlinkedRecords.put(record);
|
||||
record.currentIndex = null;
|
||||
record._nextRemoved = null;
|
||||
|
||||
if (this._removalsTail === null) {
|
||||
// todo(vicb)
|
||||
// assert(_removalsHead === null);
|
||||
this._removalsTail = this._removalsHead = record;
|
||||
record._prevRemoved = null;
|
||||
} else {
|
||||
// todo(vicb)
|
||||
// assert(_removalsTail._nextRemoved === null);
|
||||
// assert(record._nextRemoved === null);
|
||||
record._prevRemoved = this._removalsTail;
|
||||
this._removalsTail = this._removalsTail._nextRemoved = record;
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_addIdentityChange(record: IterableChangeRecord_<V>, item: V) {
|
||||
record.item = item;
|
||||
if (this._identityChangesTail === null) {
|
||||
this._identityChangesTail = this._identityChangesHead = record;
|
||||
} else {
|
||||
this._identityChangesTail = this._identityChangesTail._nextIdentityChange = record;
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
|
||||
toString(): string {
|
||||
const list: IterableChangeRecord_<V>[] = [];
|
||||
this.forEachItem((record: IterableChangeRecord_<V>) => list.push(record));
|
||||
|
||||
const previous: IterableChangeRecord_<V>[] = [];
|
||||
this.forEachPreviousItem((record: IterableChangeRecord_<V>) => previous.push(record));
|
||||
|
||||
const additions: IterableChangeRecord_<V>[] = [];
|
||||
this.forEachAddedItem((record: IterableChangeRecord_<V>) => additions.push(record));
|
||||
|
||||
const moves: IterableChangeRecord_<V>[] = [];
|
||||
this.forEachMovedItem((record: IterableChangeRecord_<V>) => moves.push(record));
|
||||
|
||||
const removals: IterableChangeRecord_<V>[] = [];
|
||||
this.forEachRemovedItem((record: IterableChangeRecord_<V>) => removals.push(record));
|
||||
|
||||
const identityChanges: IterableChangeRecord_<V>[] = [];
|
||||
this.forEachIdentityChange((record: IterableChangeRecord_<V>) => identityChanges.push(record));
|
||||
|
||||
return 'collection: ' + list.join(', ') + '\n' +
|
||||
'previous: ' + previous.join(', ') + '\n' +
|
||||
'additions: ' + additions.join(', ') + '\n' +
|
||||
'moves: ' + moves.join(', ') + '\n' +
|
||||
'removals: ' + removals.join(', ') + '\n' +
|
||||
'identityChanges: ' + identityChanges.join(', ') + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @stable
|
||||
*/
|
||||
export class IterableChangeRecord_<V> implements IterableChangeRecord<V> {
|
||||
currentIndex: number = null;
|
||||
previousIndex: number = null;
|
||||
|
||||
/** @internal */
|
||||
_nextPrevious: IterableChangeRecord_<V> = null;
|
||||
/** @internal */
|
||||
_prev: IterableChangeRecord_<V> = null;
|
||||
/** @internal */
|
||||
_next: IterableChangeRecord_<V> = null;
|
||||
/** @internal */
|
||||
_prevDup: IterableChangeRecord_<V> = null;
|
||||
/** @internal */
|
||||
_nextDup: IterableChangeRecord_<V> = null;
|
||||
/** @internal */
|
||||
_prevRemoved: IterableChangeRecord_<V> = null;
|
||||
/** @internal */
|
||||
_nextRemoved: IterableChangeRecord_<V> = null;
|
||||
/** @internal */
|
||||
_nextAdded: IterableChangeRecord_<V> = null;
|
||||
/** @internal */
|
||||
_nextMoved: IterableChangeRecord_<V> = null;
|
||||
/** @internal */
|
||||
_nextIdentityChange: IterableChangeRecord_<V> = null;
|
||||
|
||||
|
||||
constructor(public item: V, public trackById: any) {}
|
||||
|
||||
toString(): string {
|
||||
return this.previousIndex === this.currentIndex ? stringify(this.item) :
|
||||
stringify(this.item) + '[' +
|
||||
stringify(this.previousIndex) + '->' + stringify(this.currentIndex) + ']';
|
||||
}
|
||||
}
|
||||
|
||||
// A linked list of CollectionChangeRecords with the same IterableChangeRecord_.item
|
||||
class _DuplicateItemRecordList<V> {
|
||||
/** @internal */
|
||||
_head: IterableChangeRecord_<V> = null;
|
||||
/** @internal */
|
||||
_tail: IterableChangeRecord_<V> = null;
|
||||
|
||||
/**
|
||||
* Append the record to the list of duplicates.
|
||||
*
|
||||
* Note: by design all records in the list of duplicates hold the same value in record.item.
|
||||
*/
|
||||
add(record: IterableChangeRecord_<V>): void {
|
||||
if (this._head === null) {
|
||||
this._head = this._tail = record;
|
||||
record._nextDup = null;
|
||||
record._prevDup = null;
|
||||
} else {
|
||||
// todo(vicb)
|
||||
// assert(record.item == _head.item ||
|
||||
// record.item is num && record.item.isNaN && _head.item is num && _head.item.isNaN);
|
||||
this._tail._nextDup = record;
|
||||
record._prevDup = this._tail;
|
||||
record._nextDup = null;
|
||||
this._tail = record;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a IterableChangeRecord_ having IterableChangeRecord_.trackById == trackById and
|
||||
// IterableChangeRecord_.currentIndex >= afterIndex
|
||||
get(trackById: any, afterIndex: number): IterableChangeRecord_<V> {
|
||||
let record: IterableChangeRecord_<V>;
|
||||
for (record = this._head; record !== null; record = record._nextDup) {
|
||||
if ((afterIndex === null || afterIndex < record.currentIndex) &&
|
||||
looseIdentical(record.trackById, trackById)) {
|
||||
return record;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove one {@link IterableChangeRecord_} from the list of duplicates.
|
||||
*
|
||||
* Returns whether the list of duplicates is empty.
|
||||
*/
|
||||
remove(record: IterableChangeRecord_<V>): boolean {
|
||||
// todo(vicb)
|
||||
// assert(() {
|
||||
// // verify that the record being removed is in the list.
|
||||
// for (IterableChangeRecord_ cursor = _head; cursor != null; cursor = cursor._nextDup) {
|
||||
// if (identical(cursor, record)) return true;
|
||||
// }
|
||||
// return false;
|
||||
//});
|
||||
|
||||
const prev: IterableChangeRecord_<V> = record._prevDup;
|
||||
const next: IterableChangeRecord_<V> = record._nextDup;
|
||||
if (prev === null) {
|
||||
this._head = next;
|
||||
} else {
|
||||
prev._nextDup = next;
|
||||
}
|
||||
if (next === null) {
|
||||
this._tail = prev;
|
||||
} else {
|
||||
next._prevDup = prev;
|
||||
}
|
||||
return this._head === null;
|
||||
}
|
||||
}
|
||||
|
||||
class _DuplicateMap<V> {
|
||||
map = new Map<any, _DuplicateItemRecordList<V>>();
|
||||
|
||||
put(record: IterableChangeRecord_<V>) {
|
||||
const key = record.trackById;
|
||||
|
||||
let duplicates = this.map.get(key);
|
||||
if (!duplicates) {
|
||||
duplicates = new _DuplicateItemRecordList<V>();
|
||||
this.map.set(key, duplicates);
|
||||
}
|
||||
duplicates.add(record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the `value` using key. Because the IterableChangeRecord_ 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(trackById: any, afterIndex: number = null): IterableChangeRecord_<V> {
|
||||
const key = trackById;
|
||||
const recordList = this.map.get(key);
|
||||
return recordList ? recordList.get(trackById, afterIndex) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a {@link IterableChangeRecord_} from the list of duplicates.
|
||||
*
|
||||
* The list of duplicates also is removed from the map if it gets empty.
|
||||
*/
|
||||
remove(record: IterableChangeRecord_<V>): IterableChangeRecord_<V> {
|
||||
const key = record.trackById;
|
||||
const recordList: _DuplicateItemRecordList<V> = this.map.get(key);
|
||||
// Remove the list of duplicates when it gets empty
|
||||
if (recordList.remove(record)) {
|
||||
this.map.delete(key);
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
get isEmpty(): boolean { return this.map.size === 0; }
|
||||
|
||||
clear() { this.map.clear(); }
|
||||
|
||||
toString(): string { return '_DuplicateMap(' + stringify(this.map) + ')'; }
|
||||
}
|
||||
|
||||
function getPreviousIndex(item: any, addRemoveOffset: number, moveOffsets: number[]): number {
|
||||
const previousIndex = item.previousIndex;
|
||||
if (previousIndex === null) return previousIndex;
|
||||
let moveOffset = 0;
|
||||
if (moveOffsets && previousIndex < moveOffsets.length) {
|
||||
moveOffset = moveOffsets[previousIndex];
|
||||
}
|
||||
return previousIndex + addRemoveOffset + moveOffset;
|
||||
}
|
@ -0,0 +1,322 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {looseIdentical, stringify} from '../../util';
|
||||
import {isJsObject} from '../change_detection_util';
|
||||
import {ChangeDetectorRef} from '../change_detector_ref';
|
||||
|
||||
import {KeyValueChangeRecord, KeyValueChanges, KeyValueDiffer, KeyValueDifferFactory} from './keyvalue_differs';
|
||||
|
||||
|
||||
export class DefaultKeyValueDifferFactory<K, V> implements KeyValueDifferFactory {
|
||||
constructor() {}
|
||||
supports(obj: any): boolean { return obj instanceof Map || isJsObject(obj); }
|
||||
|
||||
create<K, V>(): DefaultKeyValueDiffer<K, V>;
|
||||
|
||||
/**
|
||||
* @deprecated v4.0.0 - ChangeDetectorRef is not used and is no longer a parameter
|
||||
*/
|
||||
create<K, V>(cd?: ChangeDetectorRef): KeyValueDiffer<K, V> {
|
||||
return new DefaultKeyValueDiffer<K, V>();
|
||||
}
|
||||
}
|
||||
|
||||
export class DefaultKeyValueDiffer<K, V> implements KeyValueDiffer<K, V>, KeyValueChanges<K, V> {
|
||||
private _records: Map<K, V> = new Map<K, V>();
|
||||
private _mapHead: KeyValueChangeRecord_<K, V> = null;
|
||||
private _previousMapHead: KeyValueChangeRecord_<K, V> = null;
|
||||
private _changesHead: KeyValueChangeRecord_<K, V> = null;
|
||||
private _changesTail: KeyValueChangeRecord_<K, V> = null;
|
||||
private _additionsHead: KeyValueChangeRecord_<K, V> = null;
|
||||
private _additionsTail: KeyValueChangeRecord_<K, V> = null;
|
||||
private _removalsHead: KeyValueChangeRecord_<K, V> = null;
|
||||
private _removalsTail: KeyValueChangeRecord_<K, V> = null;
|
||||
|
||||
get isDirty(): boolean {
|
||||
return this._additionsHead !== null || this._changesHead !== null ||
|
||||
this._removalsHead !== null;
|
||||
}
|
||||
|
||||
forEachItem(fn: (r: KeyValueChangeRecord<K, V>) => void) {
|
||||
let record: KeyValueChangeRecord_<K, V>;
|
||||
for (record = this._mapHead; record !== null; record = record._next) {
|
||||
fn(record);
|
||||
}
|
||||
}
|
||||
|
||||
forEachPreviousItem(fn: (r: KeyValueChangeRecord<K, V>) => void) {
|
||||
let record: KeyValueChangeRecord_<K, V>;
|
||||
for (record = this._previousMapHead; record !== null; record = record._nextPrevious) {
|
||||
fn(record);
|
||||
}
|
||||
}
|
||||
|
||||
forEachChangedItem(fn: (r: KeyValueChangeRecord<K, V>) => void) {
|
||||
let record: KeyValueChangeRecord_<K, V>;
|
||||
for (record = this._changesHead; record !== null; record = record._nextChanged) {
|
||||
fn(record);
|
||||
}
|
||||
}
|
||||
|
||||
forEachAddedItem(fn: (r: KeyValueChangeRecord<K, V>) => void) {
|
||||
let record: KeyValueChangeRecord_<K, V>;
|
||||
for (record = this._additionsHead; record !== null; record = record._nextAdded) {
|
||||
fn(record);
|
||||
}
|
||||
}
|
||||
|
||||
forEachRemovedItem(fn: (r: KeyValueChangeRecord<K, V>) => void) {
|
||||
let record: KeyValueChangeRecord_<K, V>;
|
||||
for (record = this._removalsHead; record !== null; record = record._nextRemoved) {
|
||||
fn(record);
|
||||
}
|
||||
}
|
||||
|
||||
diff(map: Map<any, any>|{[k: string]: any}): any {
|
||||
if (!map) {
|
||||
map = new Map();
|
||||
} else if (!(map instanceof Map || isJsObject(map))) {
|
||||
throw new Error(`Error trying to diff '${map}'`);
|
||||
}
|
||||
|
||||
return this.check(map) ? this : null;
|
||||
}
|
||||
|
||||
onDestroy() {}
|
||||
|
||||
check(map: Map<any, any>|{[k: string]: any}): boolean {
|
||||
this._reset();
|
||||
const records = this._records;
|
||||
let oldSeqRecord: KeyValueChangeRecord_<K, V> = this._mapHead;
|
||||
let lastOldSeqRecord: KeyValueChangeRecord_<K, V> = null;
|
||||
let lastNewSeqRecord: KeyValueChangeRecord_<K, V> = null;
|
||||
let seqChanged: boolean = false;
|
||||
|
||||
this._forEach(map, (value: any, key: any) => {
|
||||
let newSeqRecord: any;
|
||||
if (oldSeqRecord && key === oldSeqRecord.key) {
|
||||
newSeqRecord = oldSeqRecord;
|
||||
this._maybeAddToChanges(newSeqRecord, value);
|
||||
} else {
|
||||
seqChanged = true;
|
||||
if (oldSeqRecord !== null) {
|
||||
this._removeFromSeq(lastOldSeqRecord, oldSeqRecord);
|
||||
this._addToRemovals(oldSeqRecord);
|
||||
}
|
||||
if (records.has(key)) {
|
||||
newSeqRecord = records.get(key);
|
||||
this._maybeAddToChanges(newSeqRecord, value);
|
||||
} else {
|
||||
newSeqRecord = new KeyValueChangeRecord_<K, V>(key);
|
||||
records.set(key, newSeqRecord);
|
||||
newSeqRecord.currentValue = value;
|
||||
this._addToAdditions(newSeqRecord);
|
||||
}
|
||||
}
|
||||
|
||||
if (seqChanged) {
|
||||
if (this._isInRemovals(newSeqRecord)) {
|
||||
this._removeFromRemovals(newSeqRecord);
|
||||
}
|
||||
if (lastNewSeqRecord == null) {
|
||||
this._mapHead = newSeqRecord;
|
||||
} else {
|
||||
lastNewSeqRecord._next = newSeqRecord;
|
||||
}
|
||||
}
|
||||
lastOldSeqRecord = oldSeqRecord;
|
||||
lastNewSeqRecord = newSeqRecord;
|
||||
oldSeqRecord = oldSeqRecord && oldSeqRecord._next;
|
||||
});
|
||||
this._truncate(lastOldSeqRecord, oldSeqRecord);
|
||||
return this.isDirty;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_reset() {
|
||||
if (this.isDirty) {
|
||||
let record: KeyValueChangeRecord_<K, V>;
|
||||
// Record the state of the mapping
|
||||
for (record = this._previousMapHead = this._mapHead; record !== null; record = record._next) {
|
||||
record._nextPrevious = record._next;
|
||||
}
|
||||
|
||||
for (record = this._changesHead; record !== null; record = record._nextChanged) {
|
||||
record.previousValue = record.currentValue;
|
||||
}
|
||||
|
||||
for (record = this._additionsHead; record != null; record = record._nextAdded) {
|
||||
record.previousValue = record.currentValue;
|
||||
}
|
||||
|
||||
this._changesHead = this._changesTail = null;
|
||||
this._additionsHead = this._additionsTail = null;
|
||||
this._removalsHead = this._removalsTail = null;
|
||||
}
|
||||
}
|
||||
|
||||
private _truncate(lastRecord: KeyValueChangeRecord_<K, V>, record: KeyValueChangeRecord_<K, V>) {
|
||||
while (record !== null) {
|
||||
if (lastRecord === null) {
|
||||
this._mapHead = null;
|
||||
} else {
|
||||
lastRecord._next = null;
|
||||
}
|
||||
const nextRecord = record._next;
|
||||
this._addToRemovals(record);
|
||||
lastRecord = record;
|
||||
record = nextRecord;
|
||||
}
|
||||
|
||||
for (let rec: KeyValueChangeRecord_<K, V> = this._removalsHead; rec !== null;
|
||||
rec = rec._nextRemoved) {
|
||||
rec.previousValue = rec.currentValue;
|
||||
rec.currentValue = null;
|
||||
this._records.delete(rec.key);
|
||||
}
|
||||
}
|
||||
|
||||
private _maybeAddToChanges(record: KeyValueChangeRecord_<K, V>, newValue: any): void {
|
||||
if (!looseIdentical(newValue, record.currentValue)) {
|
||||
record.previousValue = record.currentValue;
|
||||
record.currentValue = newValue;
|
||||
this._addToChanges(record);
|
||||
}
|
||||
}
|
||||
|
||||
private _isInRemovals(record: KeyValueChangeRecord_<K, V>) {
|
||||
return record === this._removalsHead || record._nextRemoved !== null ||
|
||||
record._prevRemoved !== null;
|
||||
}
|
||||
|
||||
private _addToRemovals(record: KeyValueChangeRecord_<K, V>) {
|
||||
if (this._removalsHead === null) {
|
||||
this._removalsHead = this._removalsTail = record;
|
||||
} else {
|
||||
this._removalsTail._nextRemoved = record;
|
||||
record._prevRemoved = this._removalsTail;
|
||||
this._removalsTail = record;
|
||||
}
|
||||
}
|
||||
|
||||
private _removeFromSeq(prev: KeyValueChangeRecord_<K, V>, record: KeyValueChangeRecord_<K, V>) {
|
||||
const next = record._next;
|
||||
if (prev === null) {
|
||||
this._mapHead = next;
|
||||
} else {
|
||||
prev._next = next;
|
||||
}
|
||||
record._next = null;
|
||||
}
|
||||
|
||||
private _removeFromRemovals(record: KeyValueChangeRecord_<K, V>) {
|
||||
const prev = record._prevRemoved;
|
||||
const next = record._nextRemoved;
|
||||
if (prev === null) {
|
||||
this._removalsHead = next;
|
||||
} else {
|
||||
prev._nextRemoved = next;
|
||||
}
|
||||
if (next === null) {
|
||||
this._removalsTail = prev;
|
||||
} else {
|
||||
next._prevRemoved = prev;
|
||||
}
|
||||
record._prevRemoved = record._nextRemoved = null;
|
||||
}
|
||||
|
||||
private _addToAdditions(record: KeyValueChangeRecord_<K, V>) {
|
||||
if (this._additionsHead === null) {
|
||||
this._additionsHead = this._additionsTail = record;
|
||||
} else {
|
||||
this._additionsTail._nextAdded = record;
|
||||
this._additionsTail = record;
|
||||
}
|
||||
}
|
||||
|
||||
private _addToChanges(record: KeyValueChangeRecord_<K, V>) {
|
||||
if (this._changesHead === null) {
|
||||
this._changesHead = this._changesTail = record;
|
||||
} else {
|
||||
this._changesTail._nextChanged = record;
|
||||
this._changesTail = record;
|
||||
}
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
const items: any[] = [];
|
||||
const previous: any[] = [];
|
||||
const changes: any[] = [];
|
||||
const additions: any[] = [];
|
||||
const removals: any[] = [];
|
||||
let record: KeyValueChangeRecord_<K, V>;
|
||||
|
||||
for (record = this._mapHead; record !== null; record = record._next) {
|
||||
items.push(stringify(record));
|
||||
}
|
||||
for (record = this._previousMapHead; record !== null; record = record._nextPrevious) {
|
||||
previous.push(stringify(record));
|
||||
}
|
||||
for (record = this._changesHead; record !== null; record = record._nextChanged) {
|
||||
changes.push(stringify(record));
|
||||
}
|
||||
for (record = this._additionsHead; record !== null; record = record._nextAdded) {
|
||||
additions.push(stringify(record));
|
||||
}
|
||||
for (record = this._removalsHead; record !== null; record = record._nextRemoved) {
|
||||
removals.push(stringify(record));
|
||||
}
|
||||
|
||||
return 'map: ' + items.join(', ') + '\n' +
|
||||
'previous: ' + previous.join(', ') + '\n' +
|
||||
'additions: ' + additions.join(', ') + '\n' +
|
||||
'changes: ' + changes.join(', ') + '\n' +
|
||||
'removals: ' + removals.join(', ') + '\n';
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
private _forEach<K, V>(obj: Map<K, V>|{[k: string]: V}, fn: (v: V, k: any) => void) {
|
||||
if (obj instanceof Map) {
|
||||
obj.forEach(fn);
|
||||
} else {
|
||||
Object.keys(obj).forEach(k => fn(obj[k], k));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @stable
|
||||
*/
|
||||
class KeyValueChangeRecord_<K, V> implements KeyValueChangeRecord<K, V> {
|
||||
previousValue: V = null;
|
||||
currentValue: V = null;
|
||||
|
||||
/** @internal */
|
||||
_nextPrevious: KeyValueChangeRecord_<K, V> = null;
|
||||
/** @internal */
|
||||
_next: KeyValueChangeRecord_<K, V> = null;
|
||||
/** @internal */
|
||||
_nextAdded: KeyValueChangeRecord_<K, V> = null;
|
||||
/** @internal */
|
||||
_nextRemoved: KeyValueChangeRecord_<K, V> = null;
|
||||
/** @internal */
|
||||
_prevRemoved: KeyValueChangeRecord_<K, V> = null;
|
||||
/** @internal */
|
||||
_nextChanged: KeyValueChangeRecord_<K, V> = null;
|
||||
|
||||
constructor(public key: K) {}
|
||||
|
||||
toString(): string {
|
||||
return looseIdentical(this.previousValue, this.currentValue) ?
|
||||
stringify(this.key) :
|
||||
(stringify(this.key) + '[' + stringify(this.previousValue) + '->' +
|
||||
stringify(this.currentValue) + ']');
|
||||
}
|
||||
}
|
218
packages/core/src/change_detection/differs/iterable_differs.ts
Normal file
218
packages/core/src/change_detection/differs/iterable_differs.ts
Normal file
@ -0,0 +1,218 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Optional, Provider, SkipSelf} from '../../di';
|
||||
import {ChangeDetectorRef} from '../change_detector_ref';
|
||||
|
||||
/**
|
||||
* A type describing supported interable types.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export type NgIterable<T> = Array<T>| Iterable<T>;
|
||||
|
||||
/**
|
||||
* A strategy for tracking changes over time to an iterable. Used by {@link NgFor} to
|
||||
* respond to changes in an iterable by effecting equivalent changes in the DOM.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export interface IterableDiffer<V> {
|
||||
/**
|
||||
* Compute a difference between the previous state and the new `object` state.
|
||||
*
|
||||
* @param object containing the new value.
|
||||
* @returns an object describing the difference. The return value is only valid until the next
|
||||
* `diff()` invocation.
|
||||
*/
|
||||
diff(object: NgIterable<V>): IterableChanges<V>;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object describing the changes in the `Iterable` collection since last time
|
||||
* `IterableDiffer#diff()` was invoked.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export interface IterableChanges<V> {
|
||||
/**
|
||||
* Iterate over all changes. `IterableChangeRecord` will contain information about changes
|
||||
* to each item.
|
||||
*/
|
||||
forEachItem(fn: (record: IterableChangeRecord<V>) => void): void;
|
||||
|
||||
/**
|
||||
* Iterate over a set of operations which when applied to the original `Iterable` will produce the
|
||||
* new `Iterable`.
|
||||
*
|
||||
* NOTE: These are not necessarily the actual operations which were applied to the original
|
||||
* `Iterable`, rather these are a set of computed operations which may not be the same as the
|
||||
* ones applied.
|
||||
*
|
||||
* @param record A change which needs to be applied
|
||||
* @param previousIndex The `IterableChangeRecord#previousIndex` of the `record` refers to the
|
||||
* original `Iterable` location, where as `previousIndex` refers to the transient location
|
||||
* of the item, after applying the operations up to this point.
|
||||
* @param currentIndex The `IterableChangeRecord#currentIndex` of the `record` refers to the
|
||||
* original `Iterable` location, where as `currentIndex` refers to the transient location
|
||||
* of the item, after applying the operations up to this point.
|
||||
*/
|
||||
forEachOperation(
|
||||
fn: (record: IterableChangeRecord<V>, previousIndex: number, currentIndex: number) => void):
|
||||
void;
|
||||
|
||||
/**
|
||||
* Iterate over changes in the order of original `Iterable` showing where the original items
|
||||
* have moved.
|
||||
*/
|
||||
forEachPreviousItem(fn: (record: IterableChangeRecord<V>) => void): void;
|
||||
|
||||
/** Iterate over all added items. */
|
||||
forEachAddedItem(fn: (record: IterableChangeRecord<V>) => void): void;
|
||||
|
||||
/** Iterate over all moved items. */
|
||||
forEachMovedItem(fn: (record: IterableChangeRecord<V>) => void): void;
|
||||
|
||||
/** Iterate over all removed items. */
|
||||
forEachRemovedItem(fn: (record: IterableChangeRecord<V>) => void): void;
|
||||
|
||||
/** Iterate over all items which had their identity (as computed by the `trackByFn`) changed. */
|
||||
forEachIdentityChange(fn: (record: IterableChangeRecord<V>) => void): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record representing the item change information.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export interface IterableChangeRecord<V> {
|
||||
/** Current index of the item in `Iterable` or null if removed. */
|
||||
// TODO(TS2.1): make readonly once we move to TS v2.1
|
||||
/* readonly */ currentIndex: number;
|
||||
|
||||
/** Previous index of the item in `Iterable` or null if added. */
|
||||
// TODO(TS2.1): make readonly once we move to TS v2.1
|
||||
/* readonly */ previousIndex: number;
|
||||
|
||||
/** The item. */
|
||||
// TODO(TS2.1): make readonly once we move to TS v2.1
|
||||
/* readonly */ item: V;
|
||||
|
||||
/** Track by identity as computed by the `trackByFn`. */
|
||||
// TODO(TS2.1): make readonly once we move to TS v2.1
|
||||
/* readonly */ trackById: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated v4.0.0 - Use IterableChangeRecord instead.
|
||||
*/
|
||||
export interface CollectionChangeRecord<V> extends IterableChangeRecord<V> {}
|
||||
|
||||
|
||||
/**
|
||||
* Nolonger used.
|
||||
*
|
||||
* @deprecated v4.0.0 - Use TrackByFunction instead
|
||||
*/
|
||||
export interface TrackByFn { (index: number, item: any): any; }
|
||||
|
||||
/**
|
||||
* An optional function passed into {@link NgForOf} that defines how to track
|
||||
* items in an iterable (e.g. fby index or id)
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export interface TrackByFunction<T> { (index: number, item: T): any; }
|
||||
|
||||
/**
|
||||
* Provides a factory for {@link IterableDiffer}.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export interface IterableDifferFactory {
|
||||
supports(objects: any): boolean;
|
||||
create<V>(trackByFn?: TrackByFunction<V>): IterableDiffer<V>;
|
||||
|
||||
/**
|
||||
* @deprecated v4.0.0 - ChangeDetectorRef is not used and is no longer a parameter
|
||||
*/
|
||||
create<V>(_cdr?: ChangeDetectorRef|TrackByFunction<V>, trackByFn?: TrackByFunction<V>):
|
||||
IterableDiffer<V>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A repository of different iterable diffing strategies used by NgFor, NgClass, and others.
|
||||
* @stable
|
||||
*/
|
||||
export class IterableDiffers {
|
||||
/**
|
||||
* @deprecated v4.0.0 - Should be private
|
||||
*/
|
||||
factories: IterableDifferFactory[];
|
||||
constructor(factories: IterableDifferFactory[]) { this.factories = factories; }
|
||||
|
||||
static create(factories: IterableDifferFactory[], parent?: IterableDiffers): IterableDiffers {
|
||||
if (parent != null) {
|
||||
const copied = parent.factories.slice();
|
||||
factories = factories.concat(copied);
|
||||
return new IterableDiffers(factories);
|
||||
} else {
|
||||
return new IterableDiffers(factories);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an array of {@link IterableDifferFactory} and returns a provider used to extend the
|
||||
* inherited {@link IterableDiffers} instance with the provided factories and return a new
|
||||
* {@link IterableDiffers} instance.
|
||||
*
|
||||
* The following example shows how to extend an existing list of factories,
|
||||
* which will only be applied to the injector for this component and its children.
|
||||
* This step is all that's required to make a new {@link IterableDiffer} available.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* ```
|
||||
* @Component({
|
||||
* viewProviders: [
|
||||
* IterableDiffers.extend([new ImmutableListDiffer()])
|
||||
* ]
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
static extend(factories: IterableDifferFactory[]): Provider {
|
||||
return {
|
||||
provide: IterableDiffers,
|
||||
useFactory: (parent: IterableDiffers) => {
|
||||
if (!parent) {
|
||||
// Typically would occur when calling IterableDiffers.extend inside of dependencies passed
|
||||
// to
|
||||
// bootstrap(), which would override default pipes instead of extending them.
|
||||
throw new Error('Cannot extend IterableDiffers without a parent injector');
|
||||
}
|
||||
return IterableDiffers.create(factories, parent);
|
||||
},
|
||||
// Dependency technically isn't optional, but we can provide a better error message this way.
|
||||
deps: [[IterableDiffers, new SkipSelf(), new Optional()]]
|
||||
};
|
||||
}
|
||||
|
||||
find(iterable: any): IterableDifferFactory {
|
||||
const factory = this.factories.find(f => f.supports(iterable));
|
||||
if (factory != null) {
|
||||
return factory;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Cannot find a differ supporting object '${iterable}' of type '${getTypeNameForDebugging(iterable)}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getTypeNameForDebugging(type: any): string {
|
||||
return type['name'] || typeof type;
|
||||
}
|
182
packages/core/src/change_detection/differs/keyvalue_differs.ts
Normal file
182
packages/core/src/change_detection/differs/keyvalue_differs.ts
Normal file
@ -0,0 +1,182 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Optional, Provider, SkipSelf} from '../../di';
|
||||
import {ChangeDetectorRef} from '../change_detector_ref';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A differ that tracks changes made to an object over time.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export interface KeyValueDiffer<K, V> {
|
||||
/**
|
||||
* Compute a difference between the previous state and the new `object` state.
|
||||
*
|
||||
* @param object containing the new value.
|
||||
* @returns an object describing the difference. The return value is only valid until the next
|
||||
* `diff()` invocation.
|
||||
*/
|
||||
diff(object: Map<K, V>): KeyValueChanges<K, V>;
|
||||
|
||||
/**
|
||||
* Compute a difference between the previous state and the new `object` state.
|
||||
*
|
||||
* @param object containing the new value.
|
||||
* @returns an object describing the difference. The return value is only valid until the next
|
||||
* `diff()` invocation.
|
||||
*/
|
||||
diff(object: {[key: string]: V}): KeyValueChanges<string, V>;
|
||||
// TODO(TS2.1): diff<KP extends string>(this: KeyValueDiffer<KP, V>, object: Record<KP, V>):
|
||||
// KeyValueDiffer<KP, V>;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object describing the changes in the `Map` or `{[k:string]: string}` since last time
|
||||
* `KeyValueDiffer#diff()` was invoked.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export interface KeyValueChanges<K, V> {
|
||||
/**
|
||||
* Iterate over all changes. `KeyValueChangeRecord` will contain information about changes
|
||||
* to each item.
|
||||
*/
|
||||
forEachItem(fn: (r: KeyValueChangeRecord<K, V>) => void): void;
|
||||
|
||||
/**
|
||||
* Iterate over changes in the order of original Map showing where the original items
|
||||
* have moved.
|
||||
*/
|
||||
forEachPreviousItem(fn: (r: KeyValueChangeRecord<K, V>) => void): void;
|
||||
|
||||
/**
|
||||
* Iterate over all keys for which values have changed.
|
||||
*/
|
||||
forEachChangedItem(fn: (r: KeyValueChangeRecord<K, V>) => void): void;
|
||||
|
||||
/**
|
||||
* Iterate over all added items.
|
||||
*/
|
||||
forEachAddedItem(fn: (r: KeyValueChangeRecord<K, V>) => void): void;
|
||||
|
||||
/**
|
||||
* Iterate over all removed items.
|
||||
*/
|
||||
forEachRemovedItem(fn: (r: KeyValueChangeRecord<K, V>) => void): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record representing the item change information.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export interface KeyValueChangeRecord<K, V> {
|
||||
/**
|
||||
* Current key in the Map.
|
||||
*/
|
||||
/* readonly */ key: K;
|
||||
|
||||
/**
|
||||
* Current value for the key or `undefined` if removed.
|
||||
*/
|
||||
/* readonly */ currentValue: V;
|
||||
|
||||
/**
|
||||
* Previous value for the key or `undefined` if added.
|
||||
*/
|
||||
/* readonly */ previousValue: V;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a factory for {@link KeyValueDiffer}.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export interface KeyValueDifferFactory {
|
||||
/**
|
||||
* Test to see if the differ knows how to diff this kind of object.
|
||||
*/
|
||||
supports(objects: any): boolean;
|
||||
|
||||
/**
|
||||
* Create a `KeyValueDiffer`.
|
||||
*/
|
||||
create<K, V>(): KeyValueDiffer<K, V>;
|
||||
|
||||
/**
|
||||
* @deprecated v4.0.0 - ChangeDetectorRef is not used and is no longer a parameter
|
||||
*/
|
||||
create<K, V>(_cdr?: ChangeDetectorRef): KeyValueDiffer<K, V>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A repository of different Map diffing strategies used by NgClass, NgStyle, and others.
|
||||
* @stable
|
||||
*/
|
||||
export class KeyValueDiffers {
|
||||
/**
|
||||
* @deprecated v4.0.0 - Should be private.
|
||||
*/
|
||||
factories: KeyValueDifferFactory[];
|
||||
|
||||
constructor(factories: KeyValueDifferFactory[]) { this.factories = factories; }
|
||||
|
||||
static create<S>(factories: KeyValueDifferFactory[], parent?: KeyValueDiffers): KeyValueDiffers {
|
||||
if (parent) {
|
||||
const copied = parent.factories.slice();
|
||||
factories = factories.concat(copied);
|
||||
}
|
||||
return new KeyValueDiffers(factories);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an array of {@link KeyValueDifferFactory} and returns a provider used to extend the
|
||||
* inherited {@link KeyValueDiffers} instance with the provided factories and return a new
|
||||
* {@link KeyValueDiffers} instance.
|
||||
*
|
||||
* The following example shows how to extend an existing list of factories,
|
||||
* which will only be applied to the injector for this component and its children.
|
||||
* This step is all that's required to make a new {@link KeyValueDiffer} available.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* ```
|
||||
* @Component({
|
||||
* viewProviders: [
|
||||
* KeyValueDiffers.extend([new ImmutableMapDiffer()])
|
||||
* ]
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
static extend<S>(factories: KeyValueDifferFactory[]): Provider {
|
||||
return {
|
||||
provide: KeyValueDiffers,
|
||||
useFactory: (parent: KeyValueDiffers) => {
|
||||
if (!parent) {
|
||||
// Typically would occur when calling KeyValueDiffers.extend inside of dependencies passed
|
||||
// to bootstrap(), which would override default pipes instead of extending them.
|
||||
throw new Error('Cannot extend KeyValueDiffers without a parent injector');
|
||||
}
|
||||
return KeyValueDiffers.create(factories, parent);
|
||||
},
|
||||
// Dependency technically isn't optional, but we can provide a better error message this way.
|
||||
deps: [[KeyValueDiffers, new SkipSelf(), new Optional()]]
|
||||
};
|
||||
}
|
||||
|
||||
find(kv: any): KeyValueDifferFactory {
|
||||
const factory = this.factories.find(f => f.supports(kv));
|
||||
if (factory) {
|
||||
return factory;
|
||||
}
|
||||
throw new Error(`Cannot find a differ supporting object '${kv}'`);
|
||||
}
|
||||
}
|
38
packages/core/src/change_detection/pipe_transform.ts
Normal file
38
packages/core/src/change_detection/pipe_transform.ts
Normal file
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* To create a Pipe, you must implement this interface.
|
||||
*
|
||||
* Angular invokes the `transform` method with the value of a binding
|
||||
* as the first argument, and any parameters as the second argument in list form.
|
||||
*
|
||||
* ## Syntax
|
||||
*
|
||||
* `value | pipeName[:arg0[:arg1...]]`
|
||||
*
|
||||
* ### Example ([live demo](http://plnkr.co/edit/f5oyIked9M2cKzvZNKHV?p=preview))
|
||||
*
|
||||
* The `RepeatPipe` below repeats the value as many times as indicated by the first argument:
|
||||
*
|
||||
* ```
|
||||
* import {Pipe, PipeTransform} from '@angular/core';
|
||||
*
|
||||
* @Pipe({name: 'repeat'})
|
||||
* export class RepeatPipe implements PipeTransform {
|
||||
* transform(value: any, times: number) {
|
||||
* return value.repeat(times);
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Invoking `{{ 'ok' | repeat:3 }}` in a template produces `okokok`.
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export interface PipeTransform { transform(value: any, ...args: any[]): any; }
|
Reference in New Issue
Block a user