diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 2fb868b5e0..717fa06d73 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -243,6 +243,15 @@ export function addRemoveViewFromContainer( } } +/** + * Detach a `LView` from the DOM by detaching its nodes. + * + * @param lView the `LView` to be detached. + */ +export function renderDetachView(lView: LView) { + walkTNodeTree(lView, WalkTNodeTreeAction.Detach, lView[RENDERER], null); +} + /** * Traverses down and up the tree of views and containers to remove listeners and * call onDestroy callbacks. diff --git a/packages/core/src/render3/view_ref.ts b/packages/core/src/render3/view_ref.ts index 95539e1b5b..51bcc314d6 100644 --- a/packages/core/src/render3/view_ref.ts +++ b/packages/core/src/render3/view_ref.ts @@ -14,7 +14,7 @@ import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEn import {checkNoChangesInRootView, checkNoChangesInternal, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupFn} from './instructions'; import {TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node'; import {FLAGS, HOST, LView, LViewFlags, T_HOST} from './interfaces/view'; -import {destroyLView} from './node_manipulation'; +import {destroyLView, renderDetachView} from './node_manipulation'; import {findComponentView, getLViewParent} from './util/view_traversal_utils'; import {getNativeByTNode} from './util/view_utils'; @@ -262,7 +262,10 @@ export class ViewRef implements viewEngine_EmbeddedViewRef, viewEngine_Int this._viewContainerRef = vcRef; } - detachFromAppRef() { this._appRef = null; } + detachFromAppRef() { + this._appRef = null; + renderDetachView(this._lView); + } attachToAppRef(appRef: ApplicationRef) { if (this._viewContainerRef) { diff --git a/packages/core/test/acceptance/view_ref_spec.ts b/packages/core/test/acceptance/view_ref_spec.ts new file mode 100644 index 0000000000..975d38e6a2 --- /dev/null +++ b/packages/core/test/acceptance/view_ref_spec.ts @@ -0,0 +1,56 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ApplicationRef, Component, ComponentFactoryResolver, ComponentRef, ElementRef, Injector, NgModule} from '@angular/core'; +import {InternalViewRef} from '@angular/core/src/linker/view_ref'; +import {TestBed} from '@angular/core/testing'; + + +describe('ViewRef', () => { + it('should remove nodes from DOM when the view is detached from app ref', () => { + + @Component({selector: 'dynamic-cpt', template: '
'}) + class DynamicComponent { + constructor(public elRef: ElementRef) {} + } + + @Component({template: ``}) + class App { + componentRef !: ComponentRef; + constructor( + public appRef: ApplicationRef, private cfr: ComponentFactoryResolver, + private injector: Injector) {} + + create() { + const componentFactory = this.cfr.resolveComponentFactory(DynamicComponent); + this.componentRef = componentFactory.create(this.injector); + (this.componentRef.hostView as InternalViewRef).attachToAppRef(this.appRef); + document.body.appendChild(this.componentRef.instance.elRef.nativeElement); + } + + destroy() { (this.componentRef.hostView as InternalViewRef).detachFromAppRef(); } + } + + @NgModule({declarations: [App, DynamicComponent], entryComponents: [DynamicComponent]}) + class MyTestModule { + } + + TestBed.configureTestingModule({imports: [MyTestModule]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const appComponent = fixture.componentInstance; + appComponent.create(); + fixture.detectChanges(); + expect(document.body.querySelector('dynamic-cpt')).not.toBeUndefined(); + + appComponent.destroy(); + fixture.detectChanges(); + expect(document.body.querySelector('dynamic-cpt')).toBeUndefined(); + }); +}); diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 03caa09233..87495a342b 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -1145,6 +1145,9 @@ { "name": "renderComponentOrTemplate" }, + { + "name": "renderDetachView" + }, { "name": "renderEmbeddedTemplate" }, diff --git a/tools/material-ci/angular_material_test_blocklist.js b/tools/material-ci/angular_material_test_blocklist.js index 5bd5cc72b2..0e9ef7b185 100644 --- a/tools/material-ci/angular_material_test_blocklist.js +++ b/tools/material-ci/angular_material_test_blocklist.js @@ -17,18 +17,10 @@ // tslint:disable window.testBlocklist = { - "Portals DomPortalOutlet should attach and detach a component portal without a ViewContainerRef": { - "error": "Error: Expected '

Pizza

Chocolate

' to be '', 'Expected the DomPortalOutlet to be empty after detach'.", - "notes": "Unknown" - }, "CdkTable should be able to render multiple header and footer rows": { "error": "Error: Missing definitions for header, footer, and row; cannot determine which columns should be rendered.", "notes": "Attempting to access content children before view is initialized" }, - "CdkTable should be able to render and change multiple header and footer rows": { - "error": "Error: Missing definitions for header, footer, and row; cannot determine which columns should be rendered.", - "notes": "Attempting to access content children before view is initialized" - }, "CdkTable should render correctly when using native HTML tags": { "error": "Error: Missing definitions for header, footer, and row; cannot determine which columns should be rendered.", "notes": "Unknown"