From 9022f2f2f7489960840770780cd239bf6e701302 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 3 Nov 2026 15:07:06 +0100 Subject: [PATCH] fix(cdk/portal): insert DOM portal in right location Fixes that if a DOM portal is attached to an `ng-template`, we were inserting the content into the end of the parent, rather than the portal outlet location. Fixes #34025. --- src/cdk/portal/portal-directives.ts | 24 +++++-------- src/cdk/portal/portal.spec.ts | 54 ++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/src/cdk/portal/portal-directives.ts b/src/cdk/portal/portal-directives.ts index 75a257e9e386..08f64ca73c52 100644 --- a/src/cdk/portal/portal-directives.ts +++ b/src/cdk/portal/portal-directives.ts @@ -140,7 +140,7 @@ export class CdkPortalOutlet extends BasePortalOutlet implements OnInit, OnDestr // specifies its own) we need to move the component into the outlet, otherwise it'll be rendered // inside of the alternate view container. if (viewContainerRef !== this._viewContainerRef) { - this._getRootNode().appendChild((ref.hostView as EmbeddedViewRef).rootNodes[0]); + this._insertContent((ref.hostView as EmbeddedViewRef).rootNodes[0]); } super.setDisposeFn(() => ref.destroy()); @@ -185,30 +185,24 @@ export class CdkPortalOutlet extends BasePortalOutlet implements OnInit, OnDestr // 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'); - portal.setAttachedHost(this); element.parentNode!.insertBefore(anchorNode, element); - this._getRootNode().appendChild(element); + this._insertContent(element); this._attachedPortal = portal; - - super.setDisposeFn(() => { - if (anchorNode.parentNode) { - anchorNode.parentNode!.replaceChild(element, anchorNode); - } - }); + super.setDisposeFn(() => anchorNode.parentNode?.replaceChild(element, anchorNode)); }; /** Gets the root node of the portal outlet. */ - private _getRootNode(): HTMLElement { + private _insertContent(content: Element): void { const nativeElement: Node = this._viewContainerRef.element.nativeElement; // The directive could be set on a template which will result in a comment // node being the root. Use the comment's parent node if that is the case. - return ( - nativeElement.nodeType === nativeElement.ELEMENT_NODE - ? nativeElement - : nativeElement.parentNode! - ) as HTMLElement; + if (nativeElement.nodeType === nativeElement.ELEMENT_NODE) { + nativeElement.appendChild(content); + } else { + nativeElement.parentNode?.insertBefore(content, nativeElement); + } } } diff --git a/src/cdk/portal/portal.spec.ts b/src/cdk/portal/portal.spec.ts index e910307ba319..ff0fd369b14a 100644 --- a/src/cdk/portal/portal.spec.ts +++ b/src/cdk/portal/portal.spec.ts @@ -146,6 +146,38 @@ describe('Portals', () => { expect(testAppComponent.portalOutlet.hasAttached()).toBe(false); }); + it('should insert a DOM portal at the location of an ng-template outlet rather than at the end of the parent', () => { + const customFixture = TestBed.createComponent(DomPortalPositionTestApp); + customFixture.detectChanges(); + + const testApp = customFixture.componentInstance; + const domPortal = new DomPortal(testApp.domSource); + const container = customFixture.nativeElement.querySelector( + '.portal-container', + ) as HTMLElement; + const siblingBefore = container.querySelector('.sibling-before')!; + const siblingAfter = container.querySelector('.sibling-after')!; + const domElement = testApp.domSource.nativeElement; + + testApp.portal = domPortal; + customFixture.changeDetectorRef.markForCheck(); + customFixture.detectChanges(); + + expect(domElement.nextElementSibling) + .withContext('Expected DOM portal element to be inserted before subsequent siblings') + .toBe(siblingAfter); + expect(siblingAfter.previousElementSibling) + .withContext('Expected sibling after to be preceded by DOM portal element') + .toBe(domElement); + + testApp.portal = undefined; + customFixture.changeDetectorRef.markForCheck(); + customFixture.detectChanges(); + + expect(container.contains(domElement)).toBe(false); + expect(siblingBefore.nextElementSibling).toBe(siblingAfter); + }); + it('should throw when trying to load an element without a parent into a DOM portal via a CdkPortalOutlet', () => { const testAppComponent = fixture.componentInstance; const element = document.createElement('div'); @@ -926,7 +958,6 @@ class PortalTestApp { } } -/** Test-bed component that contains a portal outlet and a couple of template portals. */ @Component({ template: `
@@ -939,3 +970,24 @@ class PortalTestApp { class UnboundPortalTestApp { @ViewChild(CdkPortalOutlet) portalOutlet!: CdkPortalOutlet; } + +@Component({ + template: ` +
+ Before + + After +
+ +
+

DOM Portal Content

+
+ `, + imports: [CdkPortalOutlet], + changeDetection: ChangeDetectionStrategy.Eager, +}) +class DomPortalPositionTestApp { + @ViewChild(CdkPortalOutlet) portalOutlet!: CdkPortalOutlet; + @ViewChild('domSource') domSource!: ElementRef; + portal: DomPortal | undefined; +}