Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at the jQuery Conference 2010 (Boston, MA) with: Alex Sexton
Ben Nadel at the jQuery Conference 2010 (Boston, MA) with: Alex Sexton@SlexAxton )

Component View-Template Fragments Retain Bindings And Can Be Moved Around In The DOM In Angular 9.0.0-rc.2

By Ben Nadel on

Yesterday, while "walking" the dog, I had the most random thought: What happens if I take an Element Reference from one of my Angular Components and just move it into the document.body after the Component has been initialized? Would it continue to work? Or, would it blow-up? Well, after trying it out for myself, it appears - at least in this experiment - that View-template fragments retain their bindings after initialization and can be safely moved around in the DOM (Document Object Model) Tree in Angular 9.0.0-rc.2.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

To experiment with this idea, I created an App component that would conditionally render a child component. In this case, a Stopwatch component. I wanted to be able to create and destroy the context for this experiment to make sure I wasn't accidentally tapping into some byproduct of static analysis:

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

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

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

		<app-stopwatch
			*ngIf="isShowingStopwatch">
		</app-stopwatch>
	`
})
export class AppComponent {

	public isShowingStopwatch: boolean;

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

		this.isShowingStopwatch = false;

	}

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

	// I toggle the rendering of the Stopwatch component.
	public toggle() : void {

		this.isShowingStopwatch = ! this.isShowingStopwatch;

	}

}

As you can see, this App component is doing nothing but creating and destroying the <app-stopwatch> component. It's the Stopwatch component that is doing the fun stuff!

From a function standpoint, the Stopwatch component doesn't do much: it has Start and Stop buttons which control a timer. These are here to test the Angular bindings. The exciting part is that this component hooks into the ngAfterViewInit() lifecycle method and moves one of its embedded DOM node references out of the confines of the View-Template and into the document.body:

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

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

@Component({
	selector: "app-stopwatch",
	styleUrls: [ "./stopwatch.component.less" ],
	template:
	`
		<div #divRef class="content">
			<div class="tick">
				<strong>Tick</strong>: {{ tickCount }}
			</div>
			<div class="controls">
				<a (click)="start()">Start Timer</a>
				&mdash;
				<a (click)="stop()">Stop Timer</a>
			</div>
		</div>
	`
})
export class StopwatchComponent {

	@ViewChild( "divRef" )
	// CAUTION: Normally I would NOT BE USING a property annotation to define a Query - I
	// prefer to use the Component.queries metadata (and keep all my metadata at the top
	// of the compnoent in one place where they are easily consumable). However, said
	// approach does not appear to work in this version of Angular (9.0.0-rc.2) when the
	// Ivy renderer is enabled.
	public divRef!: ElementRef;

	public tickCount: number;
	public timer: any;

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

		this.tickCount = 0;
		this.timer = null;

	}

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

	// I get called once after the component's view and its child views have been
	// initialized.
	public ngAfterViewInit() : void {

		// EXPERIMENT: Now that the view is initialized, all of the bindings and event-
		// handlers have been wired-up. As such, it is safe to move portions of the DOM
		// branch around in the DOM tree without breaking these connections. In this
		// case, we're going to move the DIV reference out of the Angular app and into
		// the root of the DOCUMENT BODY.
		// --
		// NOTE: While this movement serves no purpose in this context, this could be
		// useful in situations where an Element needs to be above the rest of the app
		// such as in a Drag-n-Drop action, Toast, Modal, Pop-Over, etc.
		document.body.appendChild( this.divRef.nativeElement );

	}


	// I get called once when the component is being unmounted.
	public ngOnDestroy() : void {

		// There's a chance that the component has been destroyed before its view was
		// initialized (such as if a Child guard redirected the router). As such, let's
		// make sure we have a valid reference to our View element before we try to
		// clean up the document.
		if ( this.divRef ) {

			document.body.removeChild( this.divRef.nativeElement );

		}

		this.stop();

	}


	// I start the stopwatch timer.
	public start() : void {

		// If the timer is already running, ignore this request - it is redundant.
		if ( this.timer ) {

			return;

		}

		this.timer = window.setInterval(
			() => {

				this.tickCount = Date.now();

			},
			123
		);

	}


	// I stop the stopwatch timer.
	public stop() : void {

		window.clearInterval( this.timer );
		this.timer = null;

	}

}

As you can see, when the Stopwatch component is being initialized, I use the @ViewChild() decorator to access a <div> reference within the View-Template. I then use the .appendChild() DOM method to move said reference out of the Component and into the root of the document. And, when we run this Angular code in the browser, we get the following output:

A portion of the Stopwatch component view is moved out of the component and into the document body while maintaining template-bindigns in Angular 9.0.0-rc.2.

As you can see, even after we move the View-Template fragment out of the Component and into the body Element, all of the Angular bindings continue to work: the {{tickCount}} continues to update and the Start and Stop buttons continue to set and clear the Interval, respectively. How cool is that?! We can even destroy and re-render the Stopwatch component and this behavior will persist.

And, just to be sure, if we look at the DOM Tree in the Dev Tools, we can see that the view-template fragment has, indeed, been moved to the body element:

The view template-fragment appears as a direct decendant of the body element and a sibling of the app-root component in Angular 9.0.0-rc.2.

Needing to move elements around within an Angular application is not very common. However, there are definitely use-cases in which you may want to move an element into the body of the document; Drag-n-Drop actions, toast notifications, pop-ups, and basically anything where a maximal z-index is critical to the functionality. It's awesome to see, at least in this experiment, that you can move component View-fragments around in the DOM while still maintaining template-bindings in Angular 9.0.0-rc.2.



Reader Comments

This is very interesting. I have to say, I often use:

appendChild()

In Angular, but I have never used it, in the context of a:

viewChild()

Reference.

So this is good to know, that it still maintains its bindings!
And, it is interesting that works when initiated inside:

ngAfterViewInit()

I was concerned, it would become disconnected because, if my memory serves me correctly, this is the last handler in the Angular Life Cycle.

Nice exploration!

Reply to this Comment

@Charles,

Yeah, this stuff is pretty cool. I started thinking about this because I got curious about creating an HTML Dropdown menu in which the "menu" portion is actually appended to the body so that it can be z-index'd above the rest of the content. This is akin to how the native <select> works, in that the native dropdown actually floats above all the other content (it's a specialized part of the browser UI). I wanted to try to re-create something like that.

I was inspired by this presentation that I watched: https://www.youtube.com/watch?v=2SnVxPeJdwE ... after hearing Stephen Cooper on Adventures in Angular.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
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.