Skip to content

Commit 2e6045c

Browse files
crisbetojelbourn
authored andcommitted
fix(portal): better handling when dom portal content can't be restored (#17851)
Avoids a cryptic error being thrown if we're unable to restore the content of a DOM portal. Also adds a better error when trying to attach an element without a parent to a DOM portal.
1 parent 0c10828 commit 2e6045c

File tree

3 files changed

+73
-9
lines changed

3 files changed

+73
-9
lines changed

src/cdk/portal/dom-portal-outlet.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,17 +115,23 @@ export class DomPortalOutlet extends BasePortalOutlet {
115115
throw Error('Cannot attach DOM portal without _document constructor parameter');
116116
}
117117

118+
const element = portal.element;
119+
if (!element.parentNode) {
120+
throw Error('DOM portal content must be attached to a parent node.');
121+
}
122+
118123
// Anchor used to save the element's previous position so
119124
// that we can restore it when the portal is detached.
120-
let anchorNode = this._document.createComment('dom-portal');
121-
let element = portal.element;
125+
const anchorNode = this._document.createComment('dom-portal');
122126

123-
element.parentNode!.insertBefore(anchorNode, element);
127+
element.parentNode.insertBefore(anchorNode, element);
124128
this.outletElement.appendChild(element);
125129

126130
super.setDisposeFn(() => {
127131
// We can't use `replaceWith` here because IE doesn't support it.
128-
anchorNode.parentNode!.replaceChild(element, anchorNode);
132+
if (anchorNode.parentNode) {
133+
anchorNode.parentNode.replaceChild(element, anchorNode);
134+
}
129135
});
130136
}
131137

src/cdk/portal/portal-directives.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,17 +202,23 @@ export class CdkPortalOutlet extends BasePortalOutlet implements OnInit, OnDestr
202202
throw Error('Cannot attach DOM portal without _document constructor parameter');
203203
}
204204

205+
const element = portal.element;
206+
if (!element.parentNode) {
207+
throw Error('DOM portal content must be attached to a parent node.');
208+
}
209+
205210
// Anchor used to save the element's previous position so
206211
// that we can restore it when the portal is detached.
207212
const anchorNode = this._document.createComment('dom-portal');
208-
const element = portal.element;
209213

210214
portal.setAttachedHost(this);
211-
element.parentNode!.insertBefore(anchorNode, element);
215+
element.parentNode.insertBefore(anchorNode, element);
212216
this._getRootNode().appendChild(element);
213217

214218
super.setDisposeFn(() => {
215-
anchorNode.parentNode!.replaceChild(element, anchorNode);
219+
if (anchorNode.parentNode) {
220+
anchorNode.parentNode!.replaceChild(element, anchorNode);
221+
}
216222
});
217223
}
218224

src/cdk/portal/portal.spec.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,33 @@ describe('Portals', () => {
107107
.toBe(false, 'Expected content to be removed from outlet on detach.');
108108
});
109109

110+
it('should throw when trying to load an element without a parent into a DOM portal', () => {
111+
const testAppComponent = fixture.componentInstance;
112+
const element = document.createElement('div');
113+
const domPortal = new DomPortal(element);
114+
115+
expect(() => {
116+
testAppComponent.selectedPortal = domPortal;
117+
fixture.detectChanges();
118+
}).toThrowError('DOM portal content must be attached to a parent node.');
119+
});
120+
121+
it('should not throw when restoring if the outlet element was cleared', () => {
122+
const testAppComponent = fixture.componentInstance;
123+
const parent = fixture.nativeElement.querySelector('.dom-portal-parent');
124+
const domPortal = new DomPortal(testAppComponent.domPortalContent);
125+
126+
testAppComponent.selectedPortal = domPortal;
127+
fixture.detectChanges();
128+
129+
parent.innerHTML = '';
130+
131+
expect(() => {
132+
testAppComponent.selectedPortal = undefined;
133+
fixture.detectChanges();
134+
}).not.toThrow();
135+
});
136+
110137
it('should project template context bindings in the portal', () => {
111138
let testAppComponent = fixture.componentInstance;
112139
let hostContainer = fixture.nativeElement.querySelector('.portal-container');
@@ -558,6 +585,29 @@ describe('Portals', () => {
558585
expect(someDomElement.textContent!.trim()).toBe('');
559586
});
560587

588+
it('should throw when trying to load an element without a parent into a DOM portal', () => {
589+
const fixture = TestBed.createComponent(PortalTestApp);
590+
fixture.detectChanges();
591+
const element = document.createElement('div');
592+
const portal = new DomPortal(element);
593+
594+
expect(() => {
595+
portal.attach(host);
596+
fixture.detectChanges();
597+
}).toThrowError('DOM portal content must be attached to a parent node.');
598+
});
599+
600+
it('should not throw when restoring if the outlet element was cleared', () => {
601+
const fixture = TestBed.createComponent(PortalTestApp);
602+
fixture.detectChanges();
603+
const portal = new DomPortal(fixture.componentInstance.domPortalContent);
604+
605+
portal.attach(host);
606+
host.outletElement.innerHTML = '';
607+
608+
expect(() => host.detach()).not.toThrow();
609+
});
610+
561611
});
562612
});
563613

@@ -618,8 +668,10 @@ class ArbitraryViewContainerRefComponent {
618668
619669
<ng-template #templateRef let-data> {{fruit}} - {{ data?.status }}!</ng-template>
620670
621-
<div #domPortalContent>
622-
<p class="dom-portal-inner-content">Hello there</p>
671+
<div class="dom-portal-parent">
672+
<div #domPortalContent>
673+
<p class="dom-portal-inner-content">Hello there</p>
674+
</div>
623675
</div>
624676
`,
625677
})

0 commit comments

Comments
 (0)