Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 24 additions & 57 deletions packages/common/src/directives/ng_for_of.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@
* found in the LICENSE file at https://angular.io/license
*/

import {ChangeDetectorRef, Directive, DoCheck, EmbeddedViewRef, Input, IterableChangeRecord, IterableChanges, IterableDiffer, IterableDiffers, NgIterable, OnChanges, SimpleChanges, TemplateRef, TrackByFunction, ViewContainerRef, forwardRef, isDevMode} from '@angular/core';
import { ChangeDetectorRef, Directive, DoCheck, EmbeddedViewRef, Input, IterableChangeRecord, IterableChanges, IterableDiffer, IterableDiffers, NgIterable, OnChanges, SimpleChanges, TemplateRef, TrackByFunction, ViewContainerRef, forwardRef, isDevMode } from '@angular/core';
import { ViewSyncerContext, ViewSyncer } from './../utilities/view_syncer';

/**
* @stable
*/
export class NgForOfContext<T> {
export class NgForOfContext<T> implements ViewSyncerContext<T>{
constructor(
public $implicit: T, public ngForOf: NgIterable<T>, public index: number,
public count: number) {}
public $implicit: T, public ngForOf: NgIterable<T>, public index: number,
public count: number) { }

get first(): boolean { return this.index === 0; }

Expand Down Expand Up @@ -96,7 +97,7 @@ export class NgForOfContext<T> {
*
* @stable
*/
@Directive({selector: '[ngFor][ngForOf]'})
@Directive({ selector: '[ngFor][ngForOf]' })
export class NgForOf<T> implements DoCheck, OnChanges {
@Input() ngForOf: NgIterable<T>;
@Input()
Expand All @@ -105,28 +106,39 @@ export class NgForOf<T> implements DoCheck, OnChanges {
// TODO(vicb): use a log service once there is a public one available
if (<any>console && <any>console.warn) {
console.warn(
`trackBy must be a function, but received ${JSON.stringify(fn)}. ` +
`See https://angular.io/docs/ts/latest/api/common/index/NgFor-directive.html#!#change-propagation for more information.`);
`trackBy must be a function, but received ${JSON.stringify(fn)}. ` +
`See https://angular.io/docs/ts/latest/api/common/index/NgFor-directive.html#!#change-propagation for more information.`);
}
}
this._trackByFn = fn;
}

get ngForTrackBy(): TrackByFunction<T> { return this._trackByFn; }

private _differ: IterableDiffer<T>|null = null;
private _differ: IterableDiffer<T> | null = null;
private _trackByFn: TrackByFunction<T>;
private _viewSyncer: ViewSyncer<T>;

constructor(
private _viewContainer: ViewContainerRef, private _template: TemplateRef<NgForOfContext<T>>,
private _differs: IterableDiffers) {}
private _viewContainer: ViewContainerRef, private _template: TemplateRef<NgForOfContext<T>>,
private _differs: IterableDiffers) {
const viewSyncerContextFactory = () => new NgForOfContext<T>(null!, this.ngForOf, -1, -1);
const contextUpdate = (index: number, count: number, viewRef: EmbeddedViewRef<ViewSyncerContext<T>>) => {
viewRef.context.index = index;
viewRef.context.count = count;
}
this._viewSyncer = new ViewSyncer(
this._viewContainer, _template, viewSyncerContextFactory.bind(this), contextUpdate
)
}

@Input()
set ngForTemplate(value: TemplateRef<NgForOfContext<T>>) {
// TODO(TS2.1): make TemplateRef<Partial<NgForRowOf<T>>> once we move to TS v2.1
// The current type is too restrictive; a template that just uses index, for example,
// should be acceptable.
if (value) {
this._viewSyncer.setTemplate(value);
this._template = value;
}
}
Expand All @@ -140,7 +152,7 @@ export class NgForOf<T> implements DoCheck, OnChanges {
this._differ = this._differs.find(value).create(this.ngForTrackBy);
} catch (e) {
throw new Error(
`Cannot find a differ supporting object '${value}' of type '${getTypeNameForDebugging(value)}'. NgFor only supports binding to Iterables such as Arrays.`);
`Cannot find a differ supporting object '${value}' of type '${getTypeNameForDebugging(value)}'. NgFor only supports binding to Iterables such as Arrays.`);
}
}
}
Expand All @@ -149,56 +161,11 @@ export class NgForOf<T> implements DoCheck, OnChanges {
ngDoCheck(): void {
if (this._differ) {
const changes = this._differ.diff(this.ngForOf);
if (changes) this._applyChanges(changes);
}
}

private _applyChanges(changes: IterableChanges<T>) {
const insertTuples: RecordViewTuple<T>[] = [];
changes.forEachOperation(
(item: IterableChangeRecord<any>, adjustedPreviousIndex: number, currentIndex: number) => {
if (item.previousIndex == null) {
const view = this._viewContainer.createEmbeddedView(
this._template, new NgForOfContext<T>(null !, this.ngForOf, -1, -1), currentIndex);
const tuple = new RecordViewTuple<T>(item, view);
insertTuples.push(tuple);
} else if (currentIndex == null) {
this._viewContainer.remove(adjustedPreviousIndex);
} else {
const view = this._viewContainer.get(adjustedPreviousIndex) !;
this._viewContainer.move(view, currentIndex);
const tuple = new RecordViewTuple(item, <EmbeddedViewRef<NgForOfContext<T>>>view);
insertTuples.push(tuple);
}
});

for (let i = 0; i < insertTuples.length; i++) {
this._perViewChange(insertTuples[i].view, insertTuples[i].record);
}

for (let i = 0, ilen = this._viewContainer.length; i < ilen; i++) {
const viewRef = <EmbeddedViewRef<NgForOfContext<T>>>this._viewContainer.get(i);
viewRef.context.index = i;
viewRef.context.count = ilen;
if (changes) this._viewSyncer.applyChanges(changes);
}

changes.forEachIdentityChange((record: any) => {
const viewRef =
<EmbeddedViewRef<NgForOfContext<T>>>this._viewContainer.get(record.currentIndex);
viewRef.context.$implicit = record.item;
});
}

private _perViewChange(
view: EmbeddedViewRef<NgForOfContext<T>>, record: IterableChangeRecord<any>) {
view.context.$implicit = record.item;
}
}

class RecordViewTuple<T> {
constructor(public record: any, public view: EmbeddedViewRef<NgForOfContext<T>>) {}
}

/**
* @deprecated from v4.0.0 - Use NgForOf<any> instead.
*/
Expand Down
114 changes: 114 additions & 0 deletions packages/common/src/util/view_syncer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* @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 { EmbeddedViewRef, ViewContainerRef, TemplateRef, IterableChangeRecord, IterableChanges } from '@angular/core';
import { Subject } from 'rxjs/Subject';

/**
* @experimental
*/
export interface ViewSyncerContext<T> {
$implicit: T;
index: number;
count: number;
}

/**
* @experimental
*/
export type ViewSyncerContextFactory<T> = () => ViewSyncerContext<T>;

/**
* @experimental
*/
export type ViewSyncerContextUpdate<T> = (index: number, count: number, viewRef: EmbeddedViewRef<ViewSyncerContext<T>>) => void;

export type RecordPositionChangesOperation = 'INSERT' | 'MOVE' | 'REMOVE';

/**
* The `ViewSyncer` class is designed to keep the `changes` from an `IterableDiffer` in sync with the view.
*
* @experimental
*/
export class ViewSyncer<T> {

recordPositionChanges = new Subject<{index: number, item: any, operation: RecordPositionChangesOperation}>();

constructor(
private _viewContainer: ViewContainerRef,
private _template: TemplateRef<ViewSyncerContext<T>>,
private _contextFactory: ViewSyncerContextFactory<T>,
private _contextUpdate?: ViewSyncerContextUpdate<T>,
) { }

setTemplate(value: TemplateRef<ViewSyncerContext<T>>) {
this._template = value;
}

applyChanges(changes: IterableChanges<T>) {
changes.forEachOperation(
(item: IterableChangeRecord<any>, adjustedPreviousIndex: number, currentIndex: number) => {
if (item.previousIndex == null) {
this._applyInsert(item, currentIndex);
} else if (currentIndex == null) {
this._applyRemove(adjustedPreviousIndex, item);
} else {
this._applyMove(adjustedPreviousIndex, currentIndex, item);
}
});

for (let i = 0, ilen = this._viewContainer.length; i < ilen; i++) {
const viewRef = <EmbeddedViewRef<ViewSyncerContext<T>>>this._viewContainer.get(i);
if (this._contextUpdate) {
this._contextUpdate(i, ilen, viewRef);
}
}

changes.forEachIdentityChange((record: any) => {
const viewRef =
<EmbeddedViewRef<ViewSyncerContext<T>>>this._viewContainer.get(record.currentIndex);
this._setImplicit(record, viewRef);
});
}

private _setImplicit<T>(record: any, view: EmbeddedViewRef<ViewSyncerContext<T>>) {
view.context.$implicit = record.item;
}

private _applyRemove(index: number, item: any) {
this._viewContainer.remove(index);
this.recordPositionChanges.next({
item,
index,
operation: 'REMOVE'
});
}

private _applyInsert(item: any, adjustedPreviousIndex: number) {
const view = this._viewContainer.createEmbeddedView(
this._template, this._contextFactory(), adjustedPreviousIndex);
this._setImplicit(item, view);
this.recordPositionChanges.next({
item,
index: adjustedPreviousIndex,
operation: 'INSERT'
});
}

private _applyMove(adjustedPreviousIndex: number, currentIndex: number, item: any) {
const view = this._viewContainer.get(adjustedPreviousIndex)!;
this._viewContainer.move(view, currentIndex);
this._setImplicit(item, <EmbeddedViewRef<ViewSyncerContext<T>>>view);
this.recordPositionChanges.next({
item,
index: currentIndex,
operation: 'MOVE'
});
}

}