Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/core/src/render3/node_manipulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,13 @@ export function detachMovedView(declarationContainer: LContainer, lView: LView)
);
const movedViews = declarationContainer[MOVED_VIEWS]!;
const declarationViewIndex = movedViews.indexOf(lView);
movedViews.splice(declarationViewIndex, 1);
if (declarationViewIndex !== -1) {
movedViews.splice(declarationViewIndex, 1);
if (movedViews.length === 0) {
declarationContainer[MOVED_VIEWS] = null;
declarationContainer[FLAGS] &= ~LContainerFlags.HasTransplantedViews;
}
}
}

/**
Expand Down
25 changes: 16 additions & 9 deletions packages/core/src/render3/view/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
} from '../interfaces/container';
import {TNode} from '../interfaces/node';
import {RComment, RElement} from '../interfaces/renderer_dom';
import {isLView} from '../interfaces/type_checks';
import {isDestroyed, isLView} from '../interfaces/type_checks';
import {
DECLARATION_COMPONENT_VIEW,
DECLARATION_LCONTAINER,
Expand Down Expand Up @@ -153,21 +153,28 @@ export function detachView(lContainer: LContainer, removeIndex: number): LView |
const viewToDetach = lContainer[indexInContainer];

if (viewToDetach) {
const declarationLContainer = viewToDetach[DECLARATION_LCONTAINER];
if (declarationLContainer !== null && declarationLContainer !== lContainer) {
detachMovedView(declarationLContainer, viewToDetach);
const isViewDestroyed = isDestroyed(viewToDetach);

if (!isViewDestroyed) {
const declarationLContainer = viewToDetach[DECLARATION_LCONTAINER];
if (declarationLContainer !== null && declarationLContainer !== lContainer) {
detachMovedView(declarationLContainer, viewToDetach);
}
}

if (removeIndex > 0) {
lContainer[indexInContainer - 1][NEXT] = viewToDetach[NEXT] as LView;
}
const removedLView = removeFromArray(lContainer, CONTAINER_HEADER_OFFSET + removeIndex);
removeViewFromDOM(viewToDetach[TVIEW], viewToDetach);

// notify query that a view has been removed
const lQueries = removedLView[QUERIES];
if (lQueries !== null) {
lQueries.detachView(removedLView[TVIEW]);
if (!isViewDestroyed) {
removeViewFromDOM(viewToDetach[TVIEW], viewToDetach);

// notify query that a view has been removed
const lQueries = removedLView[QUERIES];
if (lQueries !== null) {
lQueries.detachView(removedLView[TVIEW]);
}
}

viewToDetach[PARENT] = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
*/

import {AsyncPipe, CommonModule, NgTemplateOutlet} from '@angular/common';
import {timeout} from '@angular/private/testing';
import {expect} from '@angular/private/testing/matchers';
import {of} from 'rxjs';
import {provideCheckNoChangesConfig} from '../../src/change_detection/provide_check_no_changes_config';
import {
AfterViewChecked,
ApplicationRef,
Expand All @@ -29,11 +33,9 @@ import {
ViewChild,
ViewContainerRef,
} from '../../src/core';
import {provideCheckNoChangesConfig} from '../../src/change_detection/provide_check_no_changes_config';
import {LContainerFlags, MOVED_VIEWS} from '../../src/render3/interfaces/container';
import {FLAGS, LView, QUERIES} from '../../src/render3/interfaces/view';
import {ComponentFixture, TestBed} from '../../testing';
import {expect} from '@angular/private/testing/matchers';
import {timeout} from '@angular/private/testing';
import {of} from 'rxjs';

describe('change detection for transplanted views', () => {
beforeEach(() => {
Expand Down Expand Up @@ -756,6 +758,110 @@ describe('change detection for transplanted views', () => {
);
});

it('keeps refreshing other transplanted views when an insertion container is cleared on destroy', () => {
@Component({
selector: 'outlet',
template: '<ng-container #container />',
changeDetection: ChangeDetectionStrategy.OnPush,
})
class Outlet {
@Input() template!: TemplateRef<{}>;
@ViewChild('container', {read: ViewContainerRef, static: true})
container!: ViewContainerRef;

ngOnInit() {
this.container.createEmbeddedView(this.template);
}

ngOnDestroy() {
this.container.clear();
}
}

@Component({
template: `
<ng-template #template>{{ name }}</ng-template>
@for (item of items; track item) {
<outlet [template]="template"></outlet>
}
`,
imports: [Outlet],
changeDetection: ChangeDetectionStrategy.Eager,
})
class App {
items = [1, 2, 3];
name = 'Penny';
}

const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('PennyPennyPenny');

fixture.componentInstance.items = [1, 2];
fixture.detectChanges();
fixture.componentInstance.name = 'Sheldon';
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toEqual('SheldonSheldon');
});

it('should not double-detach queries and should clear HasTransplantedViews flag', () => {
@Component({
selector: 'outlet',
template: '<ng-container #container />',
})
class Outlet {
@Input() template!: TemplateRef<{}>;
@ViewChild('container', {read: ViewContainerRef, static: true})
container!: ViewContainerRef;

ngOnInit() {
this.container.createEmbeddedView(this.template);
}

ngOnDestroy() {
this.container.clear();
}
}

@Component({
template: `
<!-- We use a query to force the instantiation of LQueries -->
<ng-template #template><div #myQuery></div></ng-template>
@if (show) {
<outlet [template]="template"></outlet>
}
`,
imports: [Outlet],
})
class App {
@ViewChild('template', {read: TemplateRef, static: true})
template!: TemplateRef<any>;
show = true;
}

const fixture = TestBed.createComponent(App);
fixture.detectChanges();

const appLView = (fixture.componentInstance as any).__ngContext__ as LView;
const declarationContainer = appLView[20] as any;
const transplantedLView = declarationContainer[MOVED_VIEWS]![0] as LView;
const lQueries = transplantedLView[QUERIES]!;

const detachViewSpy = spyOn(lQueries, 'detachView').and.callThrough();

fixture.componentInstance.show = false;
fixture.detectChanges();

expect(detachViewSpy).toHaveBeenCalledTimes(1);

const hasTransplantedViewsFlag =
(declarationContainer[FLAGS] & LContainerFlags.HasTransplantedViews) ===
LContainerFlags.HasTransplantedViews;

expect(declarationContainer[MOVED_VIEWS]).toBeNull();
expect(hasTransplantedViewsFlag).toBeFalse();
});

describe('ViewRef and ViewContainerRef operations', () => {
@Component({
template: '<ng-template>{{incrementChecks()}}</ng-template>',
Expand Down
Loading