Component View-Template Fragments Retain Bindings And Can Be Moved Around In The DOM In Angular 9.0.0-rc.2
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>
—
<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:
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:
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.
Want to use code from this post? Check out the license.
Reader Comments
This is very interesting. I have to say, I often use:
In Angular, but I have never used it, in the context of a:
Reference.
So this is good to know, that it still maintains its bindings!
And, it is interesting that works when initiated inside:
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!
@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 bez-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.
@All,
As a quick follow-up, the same things works if you create an
EmbeddedViewRef
and the move all of the DOM nodes out of it into another portion of the DOM:www.bennadel.com/blog/3737-rendering-a-templateref-as-a-child-of-the-body-element-in-angular-9-0-0-rc-5.htm
.... this post wasn't about that, per-say, but it is how it works. Angular is so freaking cool!
Hello Ben,
This actually might be a solution for a problem I've been trying to solve for a long time: how to move a Component, or a piece of its template, into another "place" without resetting/reloading the DOM.
Basically I have a Component with an iframe, and I don't want to reload the iframe when navigating to another route. So my idea was to append the iframe somewhere in the body and hide it, and bring it back to its place if navigating back to the same route.
I tried moving the component using
fromContainer.detach()
,toContainer.insert()
, but the iframe reloads.This might work.
But, but... is this a bug or a feature? Unfinished feature? Unnoticed bug?
It's a bit weird but, anyway, I think I'll give it a try.
Thanks a lot,
André
@André,
So, I think this is a feature. Not in this post, but in some others, I believe @Charles actually pointed out that this feature is how Angular Material implements all of their pop-ups and drop-downs. Essentially, they have an abstraction they call a "Portal", which is basically doing the stuff I am doing here: moving DOM nodes to the
body
, inside of a "portal container", and then positioning things using fixed-position.Given that Angular is using this to power Angular Material, I think it's safe to say that this is how things are intended to work. As such, I think your idea of moving around the
iframe
container makes a lot of sense.