Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Rendering A TemplateRef As A Child Of The Body Element In Angular 9.0.0-rc.5

By Ben Nadel on

CAUTION: The concept that I explore here goes outside my area of expertise. As such, please take this with a grain of salt and forgive anything that is grossly inaccurate.

A few weeks ago, I discovered that you could translocate Angular DOM nodes without breaking template bindings. This kind of blew my mind; and, since then, I've been on the lookout for how you might leverage such a feature. For example, I recently used it to render Select options in the root Stacking context. Then, this morning, I was poking around in the Angular Material repository when I saw that the Angular team using a similar technique to render Overlays. Only, they were doing it with a ng-template / TemplateRef. This blew my mind even further! So, I wanted to try it out for myself, creating a demo in which I render a TemplateRef in the document.body DOM node using Angular 9.0.0-rc.5.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

Not only was the Angular Material library using an ng-template element in their Overlay implementation, they were combining it with content-projection. This way, they could - loosely speaking - project content from within the Angular App into the document.body node (which is outside the Angular app). For my exploration, I tried to create a super simple version of what they were doing.

I created <app-body-content>. This Angular component takes the "child content" from the calling context and projects it / renders it in the body tag. This component has no appreciable output of its own, other than its host element - it simply projects the content into a TemplateRef:

// Import the core angular services.
import { Component } from "@angular/core";
import { TemplateRef } from "@angular/core";
import { ViewChild } from "@angular/core";
import { ViewContainerRef } from "@angular/core";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@Component({
	selector: "app-body-content",
	queries: {
		contentRef: new ViewChild( "contentRef" )
	},
	styleUrls: [ "./body-content.component.less" ],
	template:
	`
		<!--
			NOTE: On its own, the NgTemplate has no rendered output. As such, the
			projected content will have no output until the component explicitly renders
			it using the ViewContainerRef (in this case).
		-->
		<ng-template #contentRef>
			<ng-content></ng-content>
		</ng-template>
	`
})
export class BodyContentComponent {

	public contentRef!: TemplateRef<any>;

	private viewContainerRef: ViewContainerRef;

	// I initialize the body content component.
	constructor( viewContainerRef: ViewContainerRef ) {

		this.viewContainerRef = viewContainerRef;

	}

	// ---
	// PUBLIC METHODS.
	// ---

	// I get called once after the view bindings have been wired-up.
	public ngAfterViewInit() : void {

		// Render the TemplateRef as a SIBLING to THIS component.
		var embeddedViewRef = this.viewContainerRef.createEmbeddedView( this.contentRef );
		// NOTE: I don't if this call is actually needed. It doesn't seem to make a
		// difference in this particular demo; however, it is called in the Angular
		// Material code, so I assume it is important (in at least some cases).
		embeddedViewRef.detectChanges();

		// At this point, the embedded-view DOM (Document Object Model) branch has been
		// wired-together, complete with view-model bindings. We can now move the DOM
		// nodes - which, in this case, is made up of the NgContent-projected nodes -
		// into the BODY without breaking the template bindings.
		for ( var node of embeddedViewRef.rootNodes ) {

			document.body.appendChild( node );

		}

	}

}

This component is short, but it's complicated. First, I query for the TemplateRef which contains the projected-content from the calling context. On its own, a TemplateRef has no rendered output. As such, the user won't see the projected-content until we explicitly render it using the injected ViewContainerRef.

When we call .createEmbeddedView(), the projected-content is actually rendered as a set of sibling nodes to the <app-body-content> component. However, before the user has a chance to see the content, we move all of the rendered DOM nodes into the document.body. And, since view-template fragments can be safely translocated, all of the rendered DOM nodes retain their view-model bindings.

NOTE: In my code, I am calling .detectChanges() on the rendered template. This is what the Angular Material code was doing. In my demo, including or excluding that call seemed to make no difference. As such, I am not entirely sure what it is doing.

Now, to see this BodyContentComponent in action, let's consume it within our App component and project some dynamic template fragments into the document.body:

// Import the core angular services.
import { Component } from "@angular/core";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@Component({
	selector: "app-root",
	styleUrls: [ "./app.component.less" ],
	template:
	`
		<p>
			<a (click)="toggleContent()">Toggle Content</a>
		</p>

		<!--
			The content below this component will be projected into the Body.
			--
			NOTE: I'm using the term "projected" loosely here. The content is still
			technically projected into the BodyContent component; but, it is, in turn,
			moved into the document.body node.
		-->
		<app-body-content *ngIf="isShowingContent">

			The time is: {{ time.toTimeString() }}.

			<p [ngSwitch]="( time.getSeconds() % 2 )">
				<ng-template [ngSwitchCase]="0">
					The seconds are: even.
				</ng-template>
				<ng-template [ngSwitchCase]="1">
					The seconds are: odd.
				</ng-template>
			</p>

			<div *ngFor="let recording of recordings">
				{{ recording }}
			</div>

		</app-body-content>
	`
})
export class AppComponent {

	public isShowingContent: boolean;
	public recordings: any[];
	public time: Date;

	// I initialize the app component.
	constructor() {

		this.isShowingContent = false;
		this.recordings = [];
		this.time = new Date();

		setInterval(
			() => {
				this.time = new Date();
				this.recordings.unshift( this.time );
			},
			1000
		);

	}

	// ---
	// PUBLIC METHODS.
	// ---

	// I toggle the rendering of the body-content component.
	public toggleContent() : void {

		this.isShowingContent = ! this.isShowingContent;

	}

}

Within the App component, we are dynamically rendering the <app-body-content> using an ngIf structural directive. Then, within the content of this tag, we have some silly time-based bindings that will demonstrate that the translocated template fragment continues to update even after it is moved into the document.body node. And, when we run this Angular code, we get the following ouptut:

Projected content being rendered outside of the Angular app using a TemplateRef.

As you can see from the Elements tab (in the Chrome Dev Tools), the content that we defined within our App component is being rendered as a direct child of the document.body element. And, the setInterval() that we configured from our App component class is continuing to update the values within said content.

This is so cool!

I absolutely love how dynamic and flexible and powerful Angular is. It constantly amazes me. I'm still trying to figure out how to best take advantage of features like the rendering of TemplateRef content in the document.body node. But, the more I learn, the more inspired I become.



Reader Comments

This is very similar to Angular Material Portals.
I had recently done some thing similar to a dropdown which was being cut off because it's parent had overflow hidden element. So I used portal to render the template of the dropdown in parent component which did not have overflow: hidden set. I did not wanted to add angular material dependency to my project so I used example from the link below.

https://medium.com/angular-in-depth/how-do-cdk-portals-work-7c097c14a494

Reply to this Comment

@Hassam,

The Angular Material code is kind of mind-blowing. I've tried to poke around in it a bit, but it has so many abstractions and inversion-of-control and classes all over the place, it's a bit hard to follow. But, yes -- the portal stuff they have is really cool. I had never seen anything like that before I started playing around with these ideas.

Thanks for the link - Juri Strumpflohner really knows his stuff!

Reply to this Comment

I just switched to Angular 9 / Ivy and experience problems with this approach that worked fine for me until now.

errors.ts:30 ERROR TypeError: Cannot read property 'createEmbeddedView' of undefined
at ViewContainerRef.createEmbeddedView (view_engine_compatibility.ts:217)
at BodyContentComponent.ngAfterViewInit (app-body-content.ts:45)
at callHook (hooks.ts:245)
at callHooks (hooks.ts:214)
at executeInitAndCheckHooks (hooks.ts:159)
at refreshView (shared.ts:478)
at refreshComponent (shared.ts:1680)
at refreshChildComponents (shared.ts:141)
at refreshView (shared.ts:456)
at refreshComponent (shared.ts:1680)

What might that be and how do I solve it?

Regards, Dirk, Germany

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.