diff --git a/src/cdk/overlay/overlay-container.ts b/src/cdk/overlay/overlay-container.ts index a97eb9a28d7..048e1d1adcd 100644 --- a/src/cdk/overlay/overlay-container.ts +++ b/src/cdk/overlay/overlay-container.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injectable, Optional, SkipSelf} from '@angular/core'; +import {Injectable, Optional, SkipSelf, OnDestroy} from '@angular/core'; /** @@ -14,7 +14,7 @@ import {Injectable, Optional, SkipSelf} from '@angular/core'; * It should be provided in the root component to ensure it is properly shared. */ @Injectable() -export class OverlayContainer { +export class OverlayContainer implements OnDestroy { protected _containerElement: HTMLElement; private _themeClass: string; @@ -37,6 +37,12 @@ export class OverlayContainer { this._themeClass = value; } + ngOnDestroy() { + if (this._containerElement && this._containerElement.parentNode) { + this._containerElement.parentNode.removeChild(this._containerElement); + } + } + /** * This method returns the overlay container element. It will lazily * create the element the first time it is called to facilitate using diff --git a/src/lib/core/overlay/overlay-container.spec.ts b/src/lib/core/overlay/overlay-container.spec.ts new file mode 100644 index 00000000000..218f3f35882 --- /dev/null +++ b/src/lib/core/overlay/overlay-container.spec.ts @@ -0,0 +1,63 @@ +import {async, inject, TestBed} from '@angular/core/testing'; +import {Component, NgModule, ViewChild, ViewContainerRef} from '@angular/core'; +import {PortalModule, TemplatePortalDirective} from '../portal/portal-directives'; +import {Overlay, OverlayContainer, OverlayModule} from './index'; + +describe('OverlayContainer', () => { + let overlay: Overlay; + let overlayContainer: OverlayContainer; + + beforeAll(() => { + // Remove any stale overlay containers from previous tests that didn't clean up correctly. + const staleContainers = document.querySelectorAll('.cdk-overlay-container'); + for (let i = staleContainers.length - 1; i >= 0; i--) { + staleContainers[i].parentNode!.removeChild(staleContainers[i]); + } + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [OverlayTestModule] + }).compileComponents(); + })); + + beforeEach(inject([Overlay, OverlayContainer], (o: Overlay, oc: OverlayContainer) => { + overlay = o; + overlayContainer = oc; + })); + + it('should remove the overlay container element from the DOM on destruction', () => { + const fixture = TestBed.createComponent(TestComponentWithTemplatePortals); + + const overlayRef = overlay.create(); + overlayRef.attach(fixture.componentInstance.templatePortal); + fixture.detectChanges(); + + expect(document.querySelectorAll('.cdk-overlay-container')) + .not.toBeNull('Expected the overlay container to be in the DOM after opening an overlay'); + + // Manually call `ngOnDestroy` because there is no way to force Angular to destroy an + // injectable in a unit test. + overlayContainer.ngOnDestroy(); + + expect(document.querySelector('.cdk-overlay-container')) + .toBeNull('Expected the overlay container *not* to be in the DOM after destruction'); + }); +}); + +/** Test-bed component that contains a TempatePortal and an ElementRef. */ +@Component({ + template: `Cake`, + providers: [Overlay], +}) +class TestComponentWithTemplatePortals { + @ViewChild(TemplatePortalDirective) templatePortal: TemplatePortalDirective; + + constructor(public viewContainerRef: ViewContainerRef) { } +} + +@NgModule({ + imports: [OverlayModule, PortalModule], + declarations: [TestComponentWithTemplatePortals] +}) +class OverlayTestModule { }