fix(ivy): move views that are already attached in insert() (#29047)

Currently if a user accidentally calls ViewContainerRef.insert() with
a view that has already been attached, we do not clean up the references
properly, so we create a view tree with a cycle. This causes an infinite
loop when the view is destroyed.

This PR ensures that we fall back to ViewContainerRef.move() behavior
if we try to insert a view that is already attached. This fixes the
cycle and honors the user intention.

PR Close #29047
This commit is contained in:
Kara Erickson
2019-03-01 13:45:02 -08:00
committed by Andrew Kushnir
parent ff8e4dddb2
commit 7ac58bec8a
9 changed files with 148 additions and 312 deletions

View File

@ -47,7 +47,7 @@ import {ANIMATION_PROP_PREFIX, allocateDirectiveIntoContext, createEmptyStylingC
import {NO_CHANGE} from './tokens';
import {INTERPOLATION_DELIMITER, renderStringify} from './util/misc_utils';
import {findComponentView, getLViewParent, getRootContext, getRootView} from './util/view_traversal_utils';
import {getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getTNode, isComponent, isComponentDef, isContentQueryHost, isRootView, loadInternal, readPatchedLView, unwrapRNode} from './util/view_utils';
import {getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getTNode, isComponent, isComponentDef, isContentQueryHost, isRootView, loadInternal, readPatchedLView, unwrapRNode, viewAttachedToChangeDetector} from './util/view_utils';
@ -2531,7 +2531,8 @@ export function componentRefresh<T>(adjustedElementIndex: number): void {
ngDevMode && assertNodeType(lView[TVIEW].data[adjustedElementIndex] as TNode, TNodeType.Element);
// Only attached CheckAlways components or attached, dirty OnPush components should be checked
if (viewAttached(hostView) && hostView[FLAGS] & (LViewFlags.CheckAlways | LViewFlags.Dirty)) {
if (viewAttachedToChangeDetector(hostView) &&
hostView[FLAGS] & (LViewFlags.CheckAlways | LViewFlags.Dirty)) {
syncViewWithBlueprint(hostView);
checkView(hostView, hostView[CONTEXT]);
}
@ -2570,11 +2571,6 @@ function syncViewWithBlueprint(componentView: LView) {
}
}
/** Returns a boolean for whether the view is attached */
export function viewAttached(view: LView): boolean {
return (view[FLAGS] & LViewFlags.Attached) === LViewFlags.Attached;
}
/**
* Instruction to distribute projectable nodes among <ng-content> occurrences in a given template.
* It takes all the selectors from the entire component's template and decides where

View File

@ -354,6 +354,7 @@ export function detachView(lContainer: LContainer, removeIndex: number): LView {
viewToDetach[QUERIES] !.removeView();
}
viewToDetach[PARENT] = null;
viewToDetach[NEXT] = null;
// Unsets the attached flag
viewToDetach[FLAGS] &= ~LViewFlags.Attached;
return viewToDetach;

View File

@ -13,7 +13,7 @@ import {ComponentDef, DirectiveDef} from '../interfaces/definition';
import {TNode, TNodeFlags} from '../interfaces/node';
import {RNode} from '../interfaces/renderer';
import {StylingContext} from '../interfaces/styling';
import {FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, TData, TVIEW} from '../interfaces/view';
import {FLAGS, HEADER_OFFSET, HOST, LView, LViewFlags, PARENT, TData, TVIEW} from '../interfaces/view';
@ -182,3 +182,18 @@ export function readPatchedLView(target: any): LView|null {
}
return null;
}
/**
* Returns a boolean for whether the view is attached to the change detection tree.
*
* Note: This determines whether a view should be checked, not whether it's inserted
* into a container. For that, you'll want `viewAttachedToContainer` below.
*/
export function viewAttachedToChangeDetector(view: LView): boolean {
return (view[FLAGS] & LViewFlags.Attached) === LViewFlags.Attached;
}
/** Returns a boolean for whether the view is attached to a container. */
export function viewAttachedToContainer(view: LView): boolean {
return isLContainer(view[PARENT]);
}

View File

@ -22,14 +22,14 @@ import {addToViewTree, createEmbeddedViewAndNode, createLContainer, renderEmbedd
import {ACTIVE_INDEX, LContainer, NATIVE, VIEWS} from './interfaces/container';
import {TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node';
import {RComment, RElement, isProceduralRenderer} from './interfaces/renderer';
import {CONTEXT, LView, QUERIES, RENDERER, TView, T_HOST} from './interfaces/view';
import {CONTEXT, FLAGS, LView, LViewFlags, PARENT, QUERIES, RENDERER, TView, T_HOST} from './interfaces/view';
import {assertNodeOfPossibleTypes} from './node_assert';
import {addRemoveViewFromContainer, appendChild, detachView, getBeforeNodeForView, insertView, nativeInsertBefore, nativeNextSibling, nativeParentNode, removeView} from './node_manipulation';
import {getParentInjectorTNode} from './node_util';
import {getLView, getPreviousOrParentTNode} from './state';
import {getParentInjectorView, hasParentInjector} from './util/injector_utils';
import {findComponentView} from './util/view_traversal_utils';
import {getComponentViewByIndex, getNativeByTNode, isComponent, isLContainer, isRootView} from './util/view_utils';
import {getComponentViewByIndex, getNativeByTNode, isComponent, isLContainer, isRootView, viewAttachedToChangeDetector, viewAttachedToContainer} from './util/view_utils';
import {ViewRef} from './view_ref';
@ -236,6 +236,12 @@ export function createContainerRef(
const lView = (viewRef as ViewRef<any>)._lView !;
const adjustedIdx = this._adjustIndex(index);
if (viewAttachedToContainer(lView)) {
// If view is already attached, fall back to move() so we clean up
// references appropriately.
return this.move(viewRef, adjustedIdx);
}
insertView(lView, this._lContainer, adjustedIdx);
const beforeNode =
@ -253,8 +259,8 @@ export function createContainerRef(
throw new Error('Cannot move a destroyed View in a ViewContainer!');
}
const index = this.indexOf(viewRef);
this.detach(index);
this.insert(viewRef, this._adjustIndex(newIndex));
if (index !== -1) this.detach(index);
this.insert(viewRef, newIndex);
return viewRef;
}