fix(core): fix multiple nested views removal from ViewContainerRef (#38317)
When removal of one view causes removal of another one from the same ViewContainerRef it triggers an error with views length calculation. This commit fixes this bug by removing a view from the list of available views before invoking actual view removal (which might be recursive and relies on the length of the list of available views). Fixes #38201. PR Close #38317
This commit is contained in:
parent
d5f819ebc1
commit
b071495f92
@ -349,17 +349,6 @@ export function detachView(lContainer: LContainer, removeIndex: number): LView|u
|
|||||||
return viewToDetach;
|
return viewToDetach;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a view from a container, i.e. detaches it and then destroys the underlying LView.
|
|
||||||
*
|
|
||||||
* @param lContainer The container from which to remove a view
|
|
||||||
* @param removeIndex The index of the view to remove
|
|
||||||
*/
|
|
||||||
export function removeView(lContainer: LContainer, removeIndex: number) {
|
|
||||||
const detachedView = detachView(lContainer, removeIndex);
|
|
||||||
detachedView && destroyLView(detachedView[TVIEW], detachedView);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A standalone function which destroys an LView,
|
* A standalone function which destroys an LView,
|
||||||
* conducting clean up (e.g. removing listeners, calling onDestroys).
|
* conducting clean up (e.g. removing listeners, calling onDestroys).
|
||||||
|
@ -22,12 +22,12 @@ import {assertLContainer} from './assert';
|
|||||||
import {getParentInjectorLocation, NodeInjector} from './di';
|
import {getParentInjectorLocation, NodeInjector} from './di';
|
||||||
import {addToViewTree, createLContainer, createLView, renderView} from './instructions/shared';
|
import {addToViewTree, createLContainer, createLView, renderView} from './instructions/shared';
|
||||||
import {CONTAINER_HEADER_OFFSET, LContainer, VIEW_REFS} from './interfaces/container';
|
import {CONTAINER_HEADER_OFFSET, LContainer, VIEW_REFS} from './interfaces/container';
|
||||||
import {TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node';
|
import {TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TNode, TNodeType} from './interfaces/node';
|
||||||
import {isProceduralRenderer, RComment, RElement} from './interfaces/renderer';
|
import {isProceduralRenderer, RComment, RElement} from './interfaces/renderer';
|
||||||
import {isComponentHost, isLContainer, isLView, isRootView} from './interfaces/type_checks';
|
import {isComponentHost, isLContainer, isLView, isRootView} from './interfaces/type_checks';
|
||||||
import {DECLARATION_COMPONENT_VIEW, DECLARATION_LCONTAINER, LView, LViewFlags, PARENT, QUERIES, RENDERER, T_HOST, TVIEW, TView} from './interfaces/view';
|
import {DECLARATION_COMPONENT_VIEW, DECLARATION_LCONTAINER, LView, LViewFlags, PARENT, QUERIES, RENDERER, T_HOST, TVIEW, TView} from './interfaces/view';
|
||||||
import {assertNodeOfPossibleTypes} from './node_assert';
|
import {assertNodeOfPossibleTypes} from './node_assert';
|
||||||
import {addRemoveViewFromContainer, appendChild, detachView, getBeforeNodeForView, insertView, nativeInsertBefore, nativeNextSibling, nativeParentNode, removeView} from './node_manipulation';
|
import {addRemoveViewFromContainer, appendChild, destroyLView, detachView, getBeforeNodeForView, insertView, nativeInsertBefore, nativeNextSibling, nativeParentNode} from './node_manipulation';
|
||||||
import {getParentInjectorTNode} from './node_util';
|
import {getParentInjectorTNode} from './node_util';
|
||||||
import {getLView, getPreviousOrParentTNode} from './state';
|
import {getLView, getPreviousOrParentTNode} from './state';
|
||||||
import {getParentInjectorView, hasParentInjector} from './util/injector_utils';
|
import {getParentInjectorView, hasParentInjector} from './util/injector_utils';
|
||||||
@ -304,8 +304,18 @@ export function createContainerRef(
|
|||||||
remove(index?: number): void {
|
remove(index?: number): void {
|
||||||
this.allocateContainerIfNeeded();
|
this.allocateContainerIfNeeded();
|
||||||
const adjustedIdx = this._adjustIndex(index, -1);
|
const adjustedIdx = this._adjustIndex(index, -1);
|
||||||
removeView(this._lContainer, adjustedIdx);
|
const detachedView = detachView(this._lContainer, adjustedIdx);
|
||||||
removeFromArray(this._lContainer[VIEW_REFS]!, adjustedIdx);
|
|
||||||
|
if (detachedView) {
|
||||||
|
// Before destroying the view, remove it from the container's array of `ViewRef`s.
|
||||||
|
// This ensures the view container length is updated before calling
|
||||||
|
// `destroyLView`, which could recursively call view container methods that
|
||||||
|
// rely on an accurate container length.
|
||||||
|
// (e.g. a method on this view container being called by a child directive's OnDestroy
|
||||||
|
// lifecycle hook)
|
||||||
|
removeFromArray(this._lContainer[VIEW_REFS]!, adjustedIdx);
|
||||||
|
destroyLView(detachedView[TVIEW], detachedView);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
detach(index?: number): viewEngine_ViewRef|null {
|
detach(index?: number): viewEngine_ViewRef|null {
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import {CommonModule, DOCUMENT} from '@angular/common';
|
import {CommonModule, DOCUMENT} from '@angular/common';
|
||||||
import {computeMsgId} from '@angular/compiler';
|
import {computeMsgId} from '@angular/compiler';
|
||||||
import {Compiler, Component, ComponentFactoryResolver, Directive, DoCheck, ElementRef, EmbeddedViewRef, ErrorHandler, Injector, NgModule, NO_ERRORS_SCHEMA, OnInit, Pipe, PipeTransform, QueryList, RendererFactory2, RendererType2, Sanitizer, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, ɵsetDocument} from '@angular/core';
|
import {Compiler, Component, ComponentFactoryResolver, Directive, DoCheck, ElementRef, EmbeddedViewRef, ErrorHandler, Injector, NgModule, NO_ERRORS_SCHEMA, OnDestroy, OnInit, Pipe, PipeTransform, QueryList, RendererFactory2, RendererType2, Sanitizer, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, ɵsetDocument} from '@angular/core';
|
||||||
import {Input} from '@angular/core/src/metadata';
|
import {Input} from '@angular/core/src/metadata';
|
||||||
import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode';
|
import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode';
|
||||||
import {TestBed, TestComponentRenderer} from '@angular/core/testing';
|
import {TestBed, TestComponentRenderer} from '@angular/core/testing';
|
||||||
@ -936,6 +936,62 @@ describe('ViewContainerRef', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('dependant views', () => {
|
||||||
|
it('should not throw when view removes another view upon removal', () => {
|
||||||
|
@Component({
|
||||||
|
template: `
|
||||||
|
<div *ngIf="visible" [template]="parent">I host a template</div>
|
||||||
|
<ng-template #parent>
|
||||||
|
<div [template]="child">I host a child template</div>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #child>
|
||||||
|
I am child template
|
||||||
|
</ng-template>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class AppComponent {
|
||||||
|
visible = true;
|
||||||
|
|
||||||
|
constructor(private readonly vcr: ViewContainerRef) {}
|
||||||
|
|
||||||
|
add<C>(template: TemplateRef<C>): EmbeddedViewRef<C> {
|
||||||
|
return this.vcr.createEmbeddedView(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove<C>(viewRef: EmbeddedViewRef<C>) {
|
||||||
|
this.vcr.remove(this.vcr.indexOf(viewRef));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Directive({selector: '[template]'})
|
||||||
|
class TemplateDirective<C> implements OnInit, OnDestroy {
|
||||||
|
@Input() template !: TemplateRef<C>;
|
||||||
|
ref!: EmbeddedViewRef<C>;
|
||||||
|
|
||||||
|
constructor(private readonly host: AppComponent) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.ref = this.host.add(this.template);
|
||||||
|
this.ref.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.host.remove(this.ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule],
|
||||||
|
declarations: [AppComponent, TemplateDirective],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
fixture.componentRef.instance.visible = false;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('createEmbeddedView (incl. insert)', () => {
|
describe('createEmbeddedView (incl. insert)', () => {
|
||||||
it('should work on elements', () => {
|
it('should work on elements', () => {
|
||||||
@Component({
|
@Component({
|
||||||
|
Loading…
x
Reference in New Issue
Block a user