fix(core): ensure ngFor only inserts/moves/removes elements when necessary (#10287)
Closes #9960 Closes #7239 Closes #9672 Closes #9454 Closes #10287
This commit is contained in:
@ -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;
|
||||
}
|
||||
|
@ -60,6 +60,30 @@ export class AppElement {
|
||||
return result;
|
||||
}
|
||||
|
||||
moveView(view: AppView<any>, 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<any>, viewIndex: number) {
|
||||
if (view.type === ViewType.COMPONENT) {
|
||||
|
@ -288,6 +288,8 @@ export abstract class AppView<T> {
|
||||
}
|
||||
}
|
||||
|
||||
markContentChildAsMoved(renderAppElement: AppElement): void { this.dirtyParentQueriesInternal(); }
|
||||
|
||||
addToContentChildren(renderAppElement: AppElement): void {
|
||||
renderAppElement.parentView.contentChildren.push(this);
|
||||
this.viewContainerElement = renderAppElement;
|
||||
|
@ -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_<any>>viewRef;
|
||||
this._element.moveView(viewRef_.internalView, currentIndex);
|
||||
return wtfLeave(s, viewRef_);
|
||||
}
|
||||
|
||||
indexOf(viewRef: ViewRef): number {
|
||||
return ListWrapper.indexOf(this._element.nestedViews, (<ViewRef_<any>>viewRef).internalView);
|
||||
}
|
||||
|
Reference in New Issue
Block a user