Skip to content

Commit b071495

Browse files
waterpleaatscott
authored andcommitted
fix(core): fix multiple nested views removal from ViewContainerRef (angular#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 angular#38201. PR Close angular#38317
1 parent d5f819e commit b071495

File tree

3 files changed

+71
-16
lines changed

3 files changed

+71
-16
lines changed

packages/core/src/render3/node_manipulation.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -349,17 +349,6 @@ export function detachView(lContainer: LContainer, removeIndex: number): LView|u
349349
return viewToDetach;
350350
}
351351

352-
/**
353-
* Removes a view from a container, i.e. detaches it and then destroys the underlying LView.
354-
*
355-
* @param lContainer The container from which to remove a view
356-
* @param removeIndex The index of the view to remove
357-
*/
358-
export function removeView(lContainer: LContainer, removeIndex: number) {
359-
const detachedView = detachView(lContainer, removeIndex);
360-
detachedView && destroyLView(detachedView[TVIEW], detachedView);
361-
}
362-
363352
/**
364353
* A standalone function which destroys an LView,
365354
* conducting clean up (e.g. removing listeners, calling onDestroys).

packages/core/src/render3/view_engine_compatibility.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ import {assertLContainer} from './assert';
2222
import {getParentInjectorLocation, NodeInjector} from './di';
2323
import {addToViewTree, createLContainer, createLView, renderView} from './instructions/shared';
2424
import {CONTAINER_HEADER_OFFSET, LContainer, VIEW_REFS} from './interfaces/container';
25-
import {TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node';
25+
import {TContainerNode, TDirectiveHostNode, TElementContainerNode, TElementNode, TNode, TNodeType} from './interfaces/node';
2626
import {isProceduralRenderer, RComment, RElement} from './interfaces/renderer';
2727
import {isComponentHost, isLContainer, isLView, isRootView} from './interfaces/type_checks';
2828
import {DECLARATION_COMPONENT_VIEW, DECLARATION_LCONTAINER, LView, LViewFlags, PARENT, QUERIES, RENDERER, T_HOST, TVIEW, TView} from './interfaces/view';
2929
import {assertNodeOfPossibleTypes} from './node_assert';
30-
import {addRemoveViewFromContainer, appendChild, detachView, getBeforeNodeForView, insertView, nativeInsertBefore, nativeNextSibling, nativeParentNode, removeView} from './node_manipulation';
30+
import {addRemoveViewFromContainer, appendChild, destroyLView, detachView, getBeforeNodeForView, insertView, nativeInsertBefore, nativeNextSibling, nativeParentNode} from './node_manipulation';
3131
import {getParentInjectorTNode} from './node_util';
3232
import {getLView, getPreviousOrParentTNode} from './state';
3333
import {getParentInjectorView, hasParentInjector} from './util/injector_utils';
@@ -304,8 +304,18 @@ export function createContainerRef(
304304
remove(index?: number): void {
305305
this.allocateContainerIfNeeded();
306306
const adjustedIdx = this._adjustIndex(index, -1);
307-
removeView(this._lContainer, adjustedIdx);
308-
removeFromArray(this._lContainer[VIEW_REFS]!, adjustedIdx);
307+
const detachedView = detachView(this._lContainer, adjustedIdx);
308+
309+
if (detachedView) {
310+
// Before destroying the view, remove it from the container's array of `ViewRef`s.
311+
// This ensures the view container length is updated before calling
312+
// `destroyLView`, which could recursively call view container methods that
313+
// rely on an accurate container length.
314+
// (e.g. a method on this view container being called by a child directive's OnDestroy
315+
// lifecycle hook)
316+
removeFromArray(this._lContainer[VIEW_REFS]!, adjustedIdx);
317+
destroyLView(detachedView[TVIEW], detachedView);
318+
}
309319
}
310320

311321
detach(index?: number): viewEngine_ViewRef|null {

packages/core/test/acceptance/view_container_ref_spec.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {CommonModule, DOCUMENT} from '@angular/common';
1010
import {computeMsgId} from '@angular/compiler';
11-
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';
11+
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';
1212
import {Input} from '@angular/core/src/metadata';
1313
import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode';
1414
import {TestBed, TestComponentRenderer} from '@angular/core/testing';
@@ -936,6 +936,62 @@ describe('ViewContainerRef', () => {
936936
});
937937
});
938938

939+
describe('dependant views', () => {
940+
it('should not throw when view removes another view upon removal', () => {
941+
@Component({
942+
template: `
943+
<div *ngIf="visible" [template]="parent">I host a template</div>
944+
<ng-template #parent>
945+
<div [template]="child">I host a child template</div>
946+
</ng-template>
947+
<ng-template #child>
948+
I am child template
949+
</ng-template>
950+
`
951+
})
952+
class AppComponent {
953+
visible = true;
954+
955+
constructor(private readonly vcr: ViewContainerRef) {}
956+
957+
add<C>(template: TemplateRef<C>): EmbeddedViewRef<C> {
958+
return this.vcr.createEmbeddedView(template);
959+
}
960+
961+
remove<C>(viewRef: EmbeddedViewRef<C>) {
962+
this.vcr.remove(this.vcr.indexOf(viewRef));
963+
}
964+
}
965+
966+
@Directive({selector: '[template]'})
967+
class TemplateDirective<C> implements OnInit, OnDestroy {
968+
@Input() template !: TemplateRef<C>;
969+
ref!: EmbeddedViewRef<C>;
970+
971+
constructor(private readonly host: AppComponent) {}
972+
973+
ngOnInit() {
974+
this.ref = this.host.add(this.template);
975+
this.ref.detectChanges();
976+
}
977+
978+
ngOnDestroy() {
979+
this.host.remove(this.ref);
980+
}
981+
}
982+
983+
TestBed.configureTestingModule({
984+
imports: [CommonModule],
985+
declarations: [AppComponent, TemplateDirective],
986+
});
987+
988+
const fixture = TestBed.createComponent(AppComponent);
989+
fixture.detectChanges();
990+
fixture.componentRef.instance.visible = false;
991+
fixture.detectChanges();
992+
});
993+
});
994+
939995
describe('createEmbeddedView (incl. insert)', () => {
940996
it('should work on elements', () => {
941997
@Component({

0 commit comments

Comments
 (0)