Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 9 additions & 15 deletions src/cdk/portal/portal-directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>).rootNodes[0]);
this._insertContent((ref.hostView as EmbeddedViewRef<any>).rootNodes[0]);
}

super.setDisposeFn(() => ref.destroy());
Expand Down Expand Up @@ -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);
}
}
}

Expand Down
54 changes: 53 additions & 1 deletion src/cdk/portal/portal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -926,7 +958,6 @@ class PortalTestApp {
}
}

/** Test-bed component that contains a portal outlet and a couple of template portals. */
@Component({
template: `
<div class="portal-container">
Expand All @@ -939,3 +970,24 @@ class PortalTestApp {
class UnboundPortalTestApp {
@ViewChild(CdkPortalOutlet) portalOutlet!: CdkPortalOutlet;
}

@Component({
template: `
<div class="portal-container">
<span class="sibling-before">Before</span>
<ng-template [cdkPortalOutlet]="portal"/>
<span class="sibling-after">After</span>
</div>
<div class="dom-source-parent">
<p #domSource>DOM Portal Content</p>
</div>
`,
imports: [CdkPortalOutlet],
changeDetection: ChangeDetectionStrategy.Eager,
})
class DomPortalPositionTestApp {
@ViewChild(CdkPortalOutlet) portalOutlet!: CdkPortalOutlet;
@ViewChild('domSource') domSource!: ElementRef<HTMLElement>;
portal: DomPortal | undefined;
}
Loading