Rendering Angular Components with Portals

12 June 2024

Updated: 12 June 2024

When working with web frameworks we are often constrained to rendering content in a specific part of the DOM in which the component we are working on is rendered, and changing this ordering can often be a lot of work. In order to simplify this, most frameworks provide a concept of a Portal that allows us to define some content in one part of our app and render it elsewhere.

For the sake of our example, I would like to be able to write some markup that looks like this:

1
<portal-content>
2
<p>This is some content that is rendered inside a portal</p>
3
4
<button (click)="increment()">Count: {{ count }}</button>
5
6
<blockquote>Angular functionality will work as normal within the portal</blockquote>
7
</portal-content>

And have it render at some other place in the DOM. Since we’re using Angular we’ll use a ng-template to denote where we ouput our content, and we can programatically render stuff into this template with Angular

To create this kind of API we need to define the following:

Portal Content

A component that defines the content for the portal, we need this so we can directly access its contents and render it elsewhere. This content will need to render content somewhere else in the component tree later on

This component is fairly simple and makes use of a small composition of an ng-template and ng-content as follows:

1
@Component({
2
selector: "portal-content",
3
standalone: true,
4
template: `
5
<ng-template #content>
6
<ng-content></ng-content>
7
</ng-template>
8
`,
9
})
10
export class PortalContentComponent {
11
@ViewChild("content")
12
content?: TemplateRef<unknown>;
13
}

This also exposes the contents of the portal via the content Template Reference so that it can be accessed by components that will render this

Portal Renderer

The portal rendering component takes the content defined with the portal-content and renders it programatically somewhere - for our example we’re using the same component but this can render content elsewhere by passing around the TemplateRef we defined above

This component uses the <ng-template [cdkPortalOutlet]="contentOutput"></ng-template> to specify the template target. We will then use the @angular/cdk/portal module to construct the portal content:

1
@Component({
2
selector: "portal-root",
3
standalone: true,
4
imports: [PortalContentComponent, PortalModule],
5
template: `
6
<portal-content>
7
<!-- some content to render goes in here -->
8
</portal-content>
9
10
<hr />
11
12
<ng-template [cdkPortalOutlet]="contentOutput">
13
<!-- the content will instead be rendered here -->
14
</ng-template>
15
`,
16
})
17
export class PortalRootComponent {
18
@ViewChild(PortalContentComponent)
19
content?: PortalContentComponent;
20
contentOutput?: Portal<any>;
21
22
constructor(readonly elem: ElementRef, readonly ref: ViewContainerRef) {}
23
24
showPortalContent() {
25
const content = this.content?.content;
26
27
if (content) {
28
this.contentOutput = new TemplatePortal(content, this.ref);
29
}
30
}
31
}

Complete Code

A complete example using the two concepts demonstrated above can be seen below:

1
import { Component, ElementRef, TemplateRef, ViewChild, ViewContainerRef } from "@angular/core";
2
3
import { Portal, TemplatePortal, PortalModule } from "@angular/cdk/portal";
4
5
@Component({
6
selector: "portal-content",
7
standalone: true,
8
template: `
9
<ng-template #content>
10
<ng-content></ng-content>
11
</ng-template>
12
`,
13
})
14
export class PortalContentComponent {
15
@ViewChild("content")
16
content?: TemplateRef<unknown>;
17
}
18
19
@Component({
20
selector: "portal-root",
21
standalone: true,
22
imports: [PortalContentComponent, PortalModule],
23
template: `
24
<h1>Hello World</h1>
25
26
<p>This is where the portal content is localted in the template:</p>
27
28
<portal-content>
29
<p>This is some content that is rendered inside a portal</p>
30
31
<button (click)="increment()">Count: {{ count }}</button>
32
33
<blockquote>Angular functionality will work as normal within the portal</blockquote>
34
</portal-content>
35
36
<p>The Portal content will be rendered here in the DOM:</p>
37
38
<ng-template [cdkPortalOutlet]="contentOutput"></ng-template>
39
40
<hr />
41
<button (click)="showPortalContent()">Show Portal Content</button>
42
`,
43
})
44
export class PortalRootComponent {
45
count = 0;
46
47
@ViewChild(PortalContentComponent)
48
content?: PortalContentComponent;
49
contentOutput?: Portal<any>;
50
51
constructor(readonly elem: ElementRef, readonly ref: ViewContainerRef) {}
52
53
showPortalContent() {
54
const content = this.content?.content;
55
56
if (content) {
57
this.contentOutput = new TemplatePortal(content, this.ref);
58
}
59
}
60
61
increment() {
62
this.count++;
63
}
64
}