diff --git a/src/cdk/portal/dom-portal-outlet.ts b/src/cdk/portal/dom-portal-outlet.ts index 9c889f539689..78ed0f3df2d8 100644 --- a/src/cdk/portal/dom-portal-outlet.ts +++ b/src/cdk/portal/dom-portal-outlet.ts @@ -115,17 +115,23 @@ export class DomPortalOutlet extends BasePortalOutlet { throw Error('Cannot attach DOM portal without _document constructor parameter'); } + const element = portal.element; + if (!element.parentNode) { + throw Error('DOM portal content must be attached to a parent node.'); + } + // Anchor used to save the element's previous position so // that we can restore it when the portal is detached. - let anchorNode = this._document.createComment('dom-portal'); - let element = portal.element; + const anchorNode = this._document.createComment('dom-portal'); - element.parentNode!.insertBefore(anchorNode, element); + element.parentNode.insertBefore(anchorNode, element); this.outletElement.appendChild(element); super.setDisposeFn(() => { // We can't use `replaceWith` here because IE doesn't support it. - anchorNode.parentNode!.replaceChild(element, anchorNode); + if (anchorNode.parentNode) { + anchorNode.parentNode.replaceChild(element, anchorNode); + } }); } diff --git a/src/cdk/portal/portal-directives.ts b/src/cdk/portal/portal-directives.ts index b1279458321e..2a45fed54306 100644 --- a/src/cdk/portal/portal-directives.ts +++ b/src/cdk/portal/portal-directives.ts @@ -202,17 +202,23 @@ export class CdkPortalOutlet extends BasePortalOutlet implements OnInit, OnDestr throw Error('Cannot attach DOM portal without _document constructor parameter'); } + const element = portal.element; + if (!element.parentNode) { + throw Error('DOM portal content must be attached to a parent node.'); + } + // Anchor used to save the element's previous position so // that we can restore it when the portal is detached. const anchorNode = this._document.createComment('dom-portal'); - const element = portal.element; portal.setAttachedHost(this); - element.parentNode!.insertBefore(anchorNode, element); + element.parentNode.insertBefore(anchorNode, element); this._getRootNode().appendChild(element); super.setDisposeFn(() => { - anchorNode.parentNode!.replaceChild(element, anchorNode); + if (anchorNode.parentNode) { + anchorNode.parentNode!.replaceChild(element, anchorNode); + } }); } diff --git a/src/cdk/portal/portal.spec.ts b/src/cdk/portal/portal.spec.ts index f094218b4e9f..c9a1db266945 100644 --- a/src/cdk/portal/portal.spec.ts +++ b/src/cdk/portal/portal.spec.ts @@ -107,6 +107,33 @@ describe('Portals', () => { .toBe(false, 'Expected content to be removed from outlet on detach.'); }); + it('should throw when trying to load an element without a parent into a DOM portal', () => { + const testAppComponent = fixture.componentInstance; + const element = document.createElement('div'); + const domPortal = new DomPortal(element); + + expect(() => { + testAppComponent.selectedPortal = domPortal; + fixture.detectChanges(); + }).toThrowError('DOM portal content must be attached to a parent node.'); + }); + + it('should not throw when restoring if the outlet element was cleared', () => { + const testAppComponent = fixture.componentInstance; + const parent = fixture.nativeElement.querySelector('.dom-portal-parent'); + const domPortal = new DomPortal(testAppComponent.domPortalContent); + + testAppComponent.selectedPortal = domPortal; + fixture.detectChanges(); + + parent.innerHTML = ''; + + expect(() => { + testAppComponent.selectedPortal = undefined; + fixture.detectChanges(); + }).not.toThrow(); + }); + it('should project template context bindings in the portal', () => { let testAppComponent = fixture.componentInstance; let hostContainer = fixture.nativeElement.querySelector('.portal-container'); @@ -558,6 +585,29 @@ describe('Portals', () => { expect(someDomElement.textContent!.trim()).toBe(''); }); + it('should throw when trying to load an element without a parent into a DOM portal', () => { + const fixture = TestBed.createComponent(PortalTestApp); + fixture.detectChanges(); + const element = document.createElement('div'); + const portal = new DomPortal(element); + + expect(() => { + portal.attach(host); + fixture.detectChanges(); + }).toThrowError('DOM portal content must be attached to a parent node.'); + }); + + it('should not throw when restoring if the outlet element was cleared', () => { + const fixture = TestBed.createComponent(PortalTestApp); + fixture.detectChanges(); + const portal = new DomPortal(fixture.componentInstance.domPortalContent); + + portal.attach(host); + host.outletElement.innerHTML = ''; + + expect(() => host.detach()).not.toThrow(); + }); + }); }); @@ -618,8 +668,10 @@ class ArbitraryViewContainerRefComponent { {{fruit}} - {{ data?.status }}! -
-

Hello there

+
+
+

Hello there

+
`, })