From e18626b7a21f9256987d63274a480c48a407999f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Mon, 1 Aug 2016 11:09:52 -0700 Subject: [PATCH] fix(core): ensure ngFor only inserts/moves/removes elements when necessary (#10287) Closes #9960 Closes #7239 Closes #9672 Closes #9454 Closes #10287 --- .../@angular/common/src/directives/ng_for.ts | 68 ++------ .../differs/default_iterable_differ.ts | 60 +++++++ modules/@angular/core/src/linker/element.ts | 24 +++ modules/@angular/core/src/linker/view.ts | 2 + .../core/src/linker/view_container_ref.ts | 15 ++ .../animation/animation_integration_spec.ts | 163 ++++++++++++++---- .../differs/default_iterable_differ_spec.ts | 159 +++++++++++++++++ .../playground/src/animate/app/animate-app.ts | 23 ++- .../src/animate/css/animate-app.css | 3 +- tools/public_api_guard/core/index.d.ts | 2 + 10 files changed, 428 insertions(+), 91 deletions(-) diff --git a/modules/@angular/common/src/directives/ng_for.ts b/modules/@angular/common/src/directives/ng_for.ts index 3307a48382..baf82e3e64 100644 --- a/modules/@angular/common/src/directives/ng_for.ts +++ b/modules/@angular/common/src/directives/ng_for.ts @@ -117,24 +117,23 @@ export class NgFor implements DoCheck, OnChanges { } private _applyChanges(changes: DefaultIterableDiffer) { - // TODO(rado): check if change detection can produce a change record that is - // easier to consume than current. - const recordViewTuples: RecordViewTuple[] = []; - changes.forEachRemovedItem( - (removedRecord: CollectionChangeRecord) => - recordViewTuples.push(new RecordViewTuple(removedRecord, null))); - - changes.forEachMovedItem( - (movedRecord: CollectionChangeRecord) => - recordViewTuples.push(new RecordViewTuple(movedRecord, null))); - - const insertTuples = this._bulkRemove(recordViewTuples); - - changes.forEachAddedItem( - (addedRecord: CollectionChangeRecord) => - insertTuples.push(new RecordViewTuple(addedRecord, null))); - - this._bulkInsert(insertTuples); + const insertTuples: RecordViewTuple[] = []; + changes.forEachOperation( + (item: CollectionChangeRecord, adjustedPreviousIndex: number, currentIndex: number) => { + if (item.previousIndex == null) { + let view = this._viewContainer.createEmbeddedView( + this._templateRef, new NgForRow(null, null, null), currentIndex); + let tuple = new RecordViewTuple(item, view); + insertTuples.push(tuple); + } else if (currentIndex == null) { + this._viewContainer.remove(adjustedPreviousIndex); + } else { + let view = this._viewContainer.get(adjustedPreviousIndex); + this._viewContainer.move(view, currentIndex); + let tuple = new RecordViewTuple(item, >view); + insertTuples.push(tuple); + } + }); for (let i = 0; i < insertTuples.length; i++) { this._perViewChange(insertTuples[i].view, insertTuples[i].record); @@ -155,39 +154,6 @@ export class NgFor implements DoCheck, OnChanges { private _perViewChange(view: EmbeddedViewRef, record: CollectionChangeRecord) { view.context.$implicit = record.item; } - - private _bulkRemove(tuples: RecordViewTuple[]): RecordViewTuple[] { - tuples.sort( - (a: RecordViewTuple, b: RecordViewTuple) => - a.record.previousIndex - b.record.previousIndex); - const movedTuples: RecordViewTuple[] = []; - for (let i = tuples.length - 1; i >= 0; i--) { - const tuple = tuples[i]; - // separate moved views from removed views. - if (isPresent(tuple.record.currentIndex)) { - tuple.view = - >this._viewContainer.detach(tuple.record.previousIndex); - movedTuples.push(tuple); - } else { - this._viewContainer.remove(tuple.record.previousIndex); - } - } - return movedTuples; - } - - private _bulkInsert(tuples: RecordViewTuple[]): RecordViewTuple[] { - tuples.sort((a, b) => a.record.currentIndex - b.record.currentIndex); - for (let i = 0; i < tuples.length; i++) { - var tuple = tuples[i]; - if (isPresent(tuple.view)) { - this._viewContainer.insert(tuple.view, tuple.record.currentIndex); - } else { - tuple.view = this._viewContainer.createEmbeddedView( - this._templateRef, new NgForRow(null, null, null), tuple.record.currentIndex); - } - } - return tuples; - } } class RecordViewTuple { diff --git a/modules/@angular/core/src/change_detection/differs/default_iterable_differ.ts b/modules/@angular/core/src/change_detection/differs/default_iterable_differ.ts index 46c737f396..945c2d3456 100644 --- a/modules/@angular/core/src/change_detection/differs/default_iterable_differ.ts +++ b/modules/@angular/core/src/change_detection/differs/default_iterable_differ.ts @@ -63,6 +63,56 @@ export class DefaultIterableDiffer implements IterableDiffer { } } + forEachOperation( + fn: (item: CollectionChangeRecord, previousIndex: number, currentIndex: number) => void) { + var nextIt = this._itHead; + var nextRemove = this._removalsHead; + var addRemoveOffset = 0; + var moveOffsets: number[] = null; + while (nextIt || nextRemove) { + // Figure out which is the next record to process + // Order: remove, add, move + let record = !nextRemove || + nextIt && + nextIt.currentIndex < getPreviousIndex(nextRemove, addRemoveOffset, moveOffsets) ? + nextIt : + nextRemove; + var adjPreviousIndex = getPreviousIndex(record, addRemoveOffset, moveOffsets); + var 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 = []; + let localMovePreviousIndex = adjPreviousIndex - addRemoveOffset; + let localCurrentIndex = currentIndex - addRemoveOffset; + if (localMovePreviousIndex != localCurrentIndex) { + for (var i = 0; i < localMovePreviousIndex; i++) { + var offset = i < moveOffsets.length ? moveOffsets[i] : (moveOffsets[i] = 0); + var index = offset + i; + if (localCurrentIndex <= index && index < localMovePreviousIndex) { + moveOffsets[i] = offset + 1; + } + } + var previousIndex = record.previousIndex; + moveOffsets[previousIndex] = localCurrentIndex - localMovePreviousIndex; + } + } + } + + if (adjPreviousIndex !== currentIndex) { + fn(record, adjPreviousIndex, currentIndex); + } + } + } + forEachPreviousItem(fn: Function) { var record: CollectionChangeRecord; for (record = this._previousItHead; record !== null; record = record._nextPrevious) { @@ -700,3 +750,13 @@ class _DuplicateMap { toString(): string { return '_DuplicateMap(' + stringify(this.map) + ')'; } } + +function getPreviousIndex(item: any, addRemoveOffset: number, moveOffsets: number[]): number { + var previousIndex = item.previousIndex; + if (previousIndex === null) return previousIndex; + var moveOffset = 0; + if (moveOffsets && previousIndex < moveOffsets.length) { + moveOffset = moveOffsets[previousIndex]; + } + return previousIndex + addRemoveOffset + moveOffset; +} diff --git a/modules/@angular/core/src/linker/element.ts b/modules/@angular/core/src/linker/element.ts index 937b714341..bc81102605 100644 --- a/modules/@angular/core/src/linker/element.ts +++ b/modules/@angular/core/src/linker/element.ts @@ -60,6 +60,30 @@ export class AppElement { return result; } + moveView(view: AppView, currentIndex: number) { + var previousIndex = this.nestedViews.indexOf(view); + if (view.type === ViewType.COMPONENT) { + throw new BaseException(`Component views can't be moved!`); + } + var nestedViews = this.nestedViews; + if (nestedViews == null) { + nestedViews = []; + this.nestedViews = nestedViews; + } + ListWrapper.removeAt(nestedViews, previousIndex); + ListWrapper.insert(nestedViews, currentIndex, view); + var refRenderNode: any /** TODO #9100 */; + if (currentIndex > 0) { + var prevView = nestedViews[currentIndex - 1]; + refRenderNode = prevView.lastRootNode; + } else { + refRenderNode = this.nativeElement; + } + if (isPresent(refRenderNode)) { + view.renderer.attachViewAfter(refRenderNode, view.flatRootNodes); + } + view.markContentChildAsMoved(this); + } attachView(view: AppView, viewIndex: number) { if (view.type === ViewType.COMPONENT) { diff --git a/modules/@angular/core/src/linker/view.ts b/modules/@angular/core/src/linker/view.ts index 2a3a3b2993..4f353d59c2 100644 --- a/modules/@angular/core/src/linker/view.ts +++ b/modules/@angular/core/src/linker/view.ts @@ -288,6 +288,8 @@ export abstract class AppView { } } + markContentChildAsMoved(renderAppElement: AppElement): void { this.dirtyParentQueriesInternal(); } + addToContentChildren(renderAppElement: AppElement): void { renderAppElement.parentView.contentChildren.push(this); this.viewContainerElement = renderAppElement; diff --git a/modules/@angular/core/src/linker/view_container_ref.ts b/modules/@angular/core/src/linker/view_container_ref.ts index c2135e3ebd..1c42bb66eb 100644 --- a/modules/@angular/core/src/linker/view_container_ref.ts +++ b/modules/@angular/core/src/linker/view_container_ref.ts @@ -99,6 +99,13 @@ export abstract class ViewContainerRef { */ abstract insert(viewRef: ViewRef, index?: number): ViewRef; + /** + * Moves a View identified by a {@link ViewRef} into the container at the specified `index`. + * + * Returns the inserted {@link ViewRef}. + */ + abstract move(viewRef: ViewRef, currentIndex: number): ViewRef; + /** * Returns the index of the View, specified via {@link ViewRef}, within the current container or * `-1` if this container doesn't contain the View. @@ -170,6 +177,14 @@ export class ViewContainerRef_ implements ViewContainerRef { return wtfLeave(s, viewRef_); } + move(viewRef: ViewRef, currentIndex: number): ViewRef { + var s = this._insertScope(); + if (currentIndex == -1) return; + var viewRef_ = >viewRef; + this._element.moveView(viewRef_.internalView, currentIndex); + return wtfLeave(s, viewRef_); + } + indexOf(viewRef: ViewRef): number { return ListWrapper.indexOf(this._element.nestedViews, (>viewRef).internalView); } diff --git a/modules/@angular/core/test/animation/animation_integration_spec.ts b/modules/@angular/core/test/animation/animation_integration_spec.ts index d6bf14a58a..8151989e6f 100644 --- a/modules/@angular/core/test/animation/animation_integration_spec.ts +++ b/modules/@angular/core/test/animation/animation_integration_spec.ts @@ -749,6 +749,135 @@ function declareTests({useJit}: {useJit: boolean}) { }))); }); + describe('ng directives', () => { + describe('*ngFor', () => { + let tpl = '
{{ item }}
'; + + let getText = + (node: any) => { return node.innerHTML ? node.innerHTML : node.children[0].data; }; + + let assertParentChildContents = (parent: any, content: string) => { + var values: string[] = []; + for (var i = 0; i < parent.childNodes.length; i++) { + let child = parent.childNodes[i]; + if (child['nodeType'] == 1) { + values.push(getText(child).trim()); + } + } + var value = values.join(' -> '); + expect(value).toEqual(content); + }; + + it('should animate when items are inserted into the list at different points', + inject( + [TestComponentBuilder, AnimationDriver], + fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { + makeAnimationCmp( + tcb, tpl, + [ + trigger('trigger', [transition('void => *', [animate(1000)])]), + ], + (fixture: any /** TODO #9100 */) => { + var cmp = fixture.debugElement.componentInstance; + var parent = fixture.debugElement.nativeElement; + cmp.items = [0, 2, 4, 6, 8]; + fixture.detectChanges(); + flushMicrotasks(); + + expect(driver.log.length).toEqual(5); + assertParentChildContents(parent, '0 -> 2 -> 4 -> 6 -> 8'); + + driver.log = []; + cmp.items = [0, 1, 2, 3, 4, 5, 6, 7, 8]; + fixture.detectChanges(); + flushMicrotasks(); + + expect(driver.log.length).toEqual(4); + assertParentChildContents( + parent, '0 -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8'); + }); + }))); + + it('should animate when items are removed + moved into the list at different points and retain DOM ordering during the animation', + inject( + [TestComponentBuilder, AnimationDriver], + fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { + makeAnimationCmp( + tcb, tpl, + [ + trigger('trigger', [transition('* => *', [animate(1000)])]), + ], + (fixture: any /** TODO #9100 */) => { + var cmp = fixture.debugElement.componentInstance; + var parent = fixture.debugElement.nativeElement; + + cmp.items = [0, 1, 2, 3, 4]; + fixture.detectChanges(); + flushMicrotasks(); + + expect(driver.log.length).toEqual(5); + driver.log = []; + + cmp.items = [3, 4, 0, 9]; + fixture.detectChanges(); + flushMicrotasks(); + + // TODO (matsko): update comment below once move animations are a thing + // there are only three animations since we do + // not yet support move-based animations + expect(driver.log.length).toEqual(3); + + // move(~), add(+), remove(-) + // -1, -2, ~3, ~4, ~0, +9 + var rm0 = driver.log.shift(); + var rm1 = driver.log.shift(); + var in0 = driver.log.shift(); + + // we want to assert that the DOM chain is still preserved + // until the animations are closed + assertParentChildContents(parent, '3 -> 4 -> 0 -> 9 -> 1 -> 2'); + + rm0['player'].finish(); + assertParentChildContents(parent, '3 -> 4 -> 0 -> 9 -> 2'); + + rm1['player'].finish(); + assertParentChildContents(parent, '3 -> 4 -> 0 -> 9'); + }); + }))); + }); + + describe('[ngClass]', () => { + it('should persist ngClass class values when a remove element animation is active', + inject( + [TestComponentBuilder, AnimationDriver], + fakeAsync( + (tcb: TestComponentBuilder, driver: InnerContentTrackingAnimationDriver) => { + makeAnimationCmp( + tcb, `
`, + [ + trigger('trigger', [transition('* => void', [animate(1000)])]), + ], + (fixture: any /** TODO #9100 */) => { + var cmp = fixture.debugElement.componentInstance; + cmp.exp = true; + cmp.exp2 = 'blue'; + fixture.detectChanges(); + flushMicrotasks(); + + expect(driver.log.length).toEqual(0); + + cmp.exp = false; + fixture.detectChanges(); + flushMicrotasks(); + + var animation = driver.log.pop(); + var element = animation['element']; + (expect(element)).toHaveCssClass('blue'); + }); + }))); + }); + }); + describe('DOM order tracking', () => { if (!getDOM().supportsDOMEvents()) return; @@ -875,39 +1004,6 @@ function declareTests({useJit}: {useJit: boolean}) { }))); }); - describe('ng directives', () => { - describe('[ngClass]', () => { - it('should persist ngClass class values when a remove element animation is active', - inject( - [TestComponentBuilder, AnimationDriver], - fakeAsync( - (tcb: TestComponentBuilder, driver: InnerContentTrackingAnimationDriver) => { - makeAnimationCmp( - tcb, `
`, - [ - trigger('trigger', [transition('* => void', [animate(1000)])]), - ], - (fixture: any /** TODO #9100 */) => { - var cmp = fixture.debugElement.componentInstance; - cmp.exp = true; - cmp.exp2 = 'blue'; - fixture.detectChanges(); - flushMicrotasks(); - - expect(driver.log.length).toEqual(0); - - cmp.exp = false; - fixture.detectChanges(); - flushMicrotasks(); - - var animation = driver.log.pop(); - var element = animation['element']; - (expect(element)).toHaveCssClass('blue'); - }); - }))); - }); - }); - describe('animation states', () => { it('should throw an error when an animation is referenced that isn\'t defined within the component annotation', inject( @@ -1273,6 +1369,7 @@ class InnerContentTrackingAnimationPlayer extends MockAnimationPlayer { class DummyIfCmp { exp = false; exp2 = false; + items = [0, 1, 2, 3, 4]; } @Component({ diff --git a/modules/@angular/core/test/change_detection/differs/default_iterable_differ_spec.ts b/modules/@angular/core/test/change_detection/differs/default_iterable_differ_spec.ts index e8b491e152..b34d7e4c73 100644 --- a/modules/@angular/core/test/change_detection/differs/default_iterable_differ_spec.ts +++ b/modules/@angular/core/test/change_detection/differs/default_iterable_differ_spec.ts @@ -298,6 +298,165 @@ export function main() { })); }); + describe('forEachOperation', () => { + function stringifyItemChange(record: any, p: number, c: number, originalIndex: number) { + var suffix = originalIndex == null ? '' : ' [o=' + originalIndex + ']'; + var value = record.item; + if (record.currentIndex == null) { + return `REMOVE ${value} (${p} -> VOID)${suffix}`; + } else if (record.previousIndex == null) { + return `INSERT ${value} (VOID -> ${c})${suffix}`; + } else { + return `MOVE ${value} (${p} -> ${c})${suffix}`; + } + } + + function modifyArrayUsingOperation( + arr: number[], endData: any[], prev: number, next: number) { + var value: number = null; + if (prev == null) { + value = endData[next]; + arr.splice(next, 0, value); + } else if (next == null) { + value = arr[prev]; + arr.splice(prev, 1); + } else { + value = arr[prev]; + arr.splice(prev, 1); + arr.splice(next, 0, value); + } + return value; + } + + it('should trigger a series of insert/move/remove changes for inputs that have been diffed', + () => { + var startData = [0, 1, 2, 3, 4, 5]; + var endData = [6, 2, 7, 0, 4, 8]; + + differ = differ.diff(startData); + differ = differ.diff(endData); + + var operations: string[] = []; + differ.forEachOperation((item: any, prev: number, next: number) => { + var value = modifyArrayUsingOperation(startData, endData, prev, next); + operations.push(stringifyItemChange(item, prev, next, item.previousIndex)); + }); + + expect(operations).toEqual([ + 'INSERT 6 (VOID -> 0)', 'MOVE 2 (3 -> 1) [o=2]', 'INSERT 7 (VOID -> 2)', + 'REMOVE 1 (4 -> VOID) [o=1]', 'REMOVE 3 (4 -> VOID) [o=3]', + 'REMOVE 5 (5 -> VOID) [o=5]', 'INSERT 8 (VOID -> 5)' + ]); + + expect(startData).toEqual(endData); + }); + + it('should consider inserting/removing/moving items with respect to items that have not moved at all', + () => { + var startData = [0, 1, 2, 3]; + var endData = [2, 1]; + + differ = differ.diff(startData); + differ = differ.diff(endData); + + var operations: string[] = []; + differ.forEachOperation((item: any, prev: number, next: number) => { + var value = modifyArrayUsingOperation(startData, endData, prev, next); + operations.push(stringifyItemChange(item, prev, next, item.previousIndex)); + }); + + expect(operations).toEqual([ + 'REMOVE 0 (0 -> VOID) [o=0]', 'MOVE 2 (1 -> 0) [o=2]', 'REMOVE 3 (2 -> VOID) [o=3]' + ]); + + expect(startData).toEqual(endData); + }); + + it('should be able to manage operations within a criss/cross of move operations', () => { + var startData = [1, 2, 3, 4, 5, 6]; + var endData = [3, 6, 4, 9, 1, 2]; + + differ = differ.diff(startData); + differ = differ.diff(endData); + + var operations: string[] = []; + differ.forEachOperation((item: any, prev: number, next: number) => { + var value = modifyArrayUsingOperation(startData, endData, prev, next); + operations.push(stringifyItemChange(item, prev, next, item.previousIndex)); + }); + + expect(operations).toEqual([ + 'MOVE 3 (2 -> 0) [o=2]', 'MOVE 6 (5 -> 1) [o=5]', 'MOVE 4 (4 -> 2) [o=3]', + 'INSERT 9 (VOID -> 3)', 'REMOVE 5 (6 -> VOID) [o=4]' + ]); + + expect(startData).toEqual(endData); + }); + + it('should skip moves for multiple nodes that have not moved', () => { + var startData = [0, 1, 2, 3, 4]; + var endData = [4, 1, 2, 3, 0, 5]; + + differ = differ.diff(startData); + differ = differ.diff(endData); + + var operations: string[] = []; + differ.forEachOperation((item: any, prev: number, next: number) => { + var value = modifyArrayUsingOperation(startData, endData, prev, next); + operations.push(stringifyItemChange(item, prev, next, item.previousIndex)); + }); + + expect(operations).toEqual([ + 'MOVE 4 (4 -> 0) [o=4]', 'MOVE 1 (2 -> 1) [o=1]', 'MOVE 2 (3 -> 2) [o=2]', + 'MOVE 3 (4 -> 3) [o=3]', 'INSERT 5 (VOID -> 5)' + ]); + + expect(startData).toEqual(endData); + }); + + it('should not fail', () => { + var startData = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; + var endData = [10, 11, 1, 5, 7, 8, 0, 5, 3, 6]; + + differ = differ.diff(startData); + differ = differ.diff(endData); + + var operations: string[] = []; + differ.forEachOperation((item: any, prev: number, next: number) => { + var value = modifyArrayUsingOperation(startData, endData, prev, next); + operations.push(stringifyItemChange(item, prev, next, item.previousIndex)); + }); + + expect(operations).toEqual([ + 'MOVE 10 (10 -> 0) [o=10]', 'MOVE 11 (11 -> 1) [o=11]', 'MOVE 1 (3 -> 2) [o=1]', + 'MOVE 5 (7 -> 3) [o=5]', 'MOVE 7 (9 -> 4) [o=7]', 'MOVE 8 (10 -> 5) [o=8]', + 'REMOVE 2 (7 -> VOID) [o=2]', 'INSERT 5 (VOID -> 7)', 'REMOVE 4 (9 -> VOID) [o=4]', + 'REMOVE 9 (10 -> VOID) [o=9]' + ]); + + expect(startData).toEqual(endData); + }); + + it('should trigger nothing when the list is completely full of replaced items that are tracked by the index', + () => { + differ = new DefaultIterableDiffer((index: number) => index); + + var startData = [1, 2, 3, 4]; + var endData = [5, 6, 7, 8]; + + differ = differ.diff(startData); + differ = differ.diff(endData); + + var operations: string[] = []; + differ.forEachOperation((item: any, prev: number, next: number) => { + var value = modifyArrayUsingOperation(startData, endData, prev, next); + operations.push(stringifyItemChange(item, prev, next, item.previousIndex)); + }); + + expect(operations).toEqual([]); + }); + }); + describe('diff', () => { it('should return self when there is a change', () => { expect(differ.diff(['a', 'b'])).toBe(differ); diff --git a/modules/playground/src/animate/app/animate-app.ts b/modules/playground/src/animate/app/animate-app.ts index 636f2abdda..439247b88d 100644 --- a/modules/playground/src/animate/app/animate-app.ts +++ b/modules/playground/src/animate/app/animate-app.ts @@ -29,14 +29,13 @@ import { | +
-
- {{ item }} -
- something inside -
+
+ {{ item }} - {{ i }} +
`, animations: [ @@ -77,6 +76,20 @@ export class AnimateApp { public bgStatus = 'focus'; + remove(item: any) { + var index = this.items.indexOf(item); + if (index >= 0) { + this.items.splice(index, 1); + } + } + + reorderAndRemove() { + this.items = this.items.sort((a: any,b: any) => Math.random() - 0.5); + this.items.splice(Math.floor(Math.random() * this.items.length), 1); + this.items.splice(Math.floor(Math.random() * this.items.length), 1); + this.items[Math.floor(Math.random() * this.items.length)] = 99; + } + get state() { return this._state; } set state(s) { this._state = s; diff --git a/modules/playground/src/animate/css/animate-app.css b/modules/playground/src/animate/css/animate-app.css index 2f23121e79..0f846cb823 100644 --- a/modules/playground/src/animate/css/animate-app.css +++ b/modules/playground/src/animate/css/animate-app.css @@ -7,10 +7,9 @@ button { } .box { - font-size:50px; + font-size:40px; border:10px solid black; width:200px; - font-size:80px; line-height:100px; height:100px; display:inline-block; diff --git a/tools/public_api_guard/core/index.d.ts b/tools/public_api_guard/core/index.d.ts index 76df76497c..7c70f042dd 100644 --- a/tools/public_api_guard/core/index.d.ts +++ b/tools/public_api_guard/core/index.d.ts @@ -463,6 +463,7 @@ export declare class DefaultIterableDiffer implements IterableDiffer { forEachIdentityChange(fn: Function): void; forEachItem(fn: Function): void; forEachMovedItem(fn: Function): void; + forEachOperation(fn: (item: CollectionChangeRecord, previousIndex: number, currentIndex: number) => void): void; forEachPreviousItem(fn: Function): void; forEachRemovedItem(fn: Function): void; onDestroy(): void; @@ -1370,6 +1371,7 @@ export declare abstract class ViewContainerRef { abstract get(index: number): ViewRef; abstract indexOf(viewRef: ViewRef): number; abstract insert(viewRef: ViewRef, index?: number): ViewRef; + abstract move(viewRef: ViewRef, currentIndex: number): ViewRef; abstract remove(index?: number): void; }