Querying DOM References
Before diving into the various DOM abstractions, it's important to understand how we obtain them within our component or directive classes. Angular offers a feature known as DOM queries, implemented through the @ViewChild and @ViewChildren decorators. Both function identically, with the key difference being that @ViewChild returns a single reference while @ViewChildren provides multiple references in a QueryList collection. For the illustrations in this piece, I'll rely primarily on the ViewChild decorator.
These decorators are commonly used alongside template reference variables. A template reference variable is essentially a named handle to a DOM element within a template, similar in concept to an id attribute on an html node. You tag a DOM element with a template reference and then access it within your class using the ViewChild decorator. Consider this fundamental example:
@Component({
selector: 'sample',
template: `
<span #tref>I am span</span>
`
})
export class SampleComponent implements AfterViewInit {
@ViewChild("tref", {read: ElementRef}) tref: ElementRef;
ngAfterViewInit(): void {
// outputs `I am span`
console.log(this.tref.nativeElement.textContent);
}
}
The fundamental syntax for the ViewChild decorator is structured as:
@ViewChild([reference from template], {read: [reference type]});
Notice in the snippet that I defined tref as the template reference name in the markup, which allows me to retrieve the corresponding ElementRef. The read parameter is often optional because Angular can deduce the reference type based on the DOM element. For instance, a standard element like a span yields an ElementRef, while a template element provides a TemplateRef. Certain references, such as ViewContainerRef, cannot be inferred and must be explicitly requested via the read option. Conversely, types like ViewRef are never returned directly from the DOM and need to be created programmatically.
Now that we've established how to query for these references, let's examine each one in detail.
ElementRef
This is the most fundamental abstraction in the list. Looking at its structure, you'll find it simply encapsulates the native element it corresponds to. This is useful for accessing the raw DOM element directly, as demonstrated here:
// outputs `I am span`
console.log(this.tref.nativeElement.textContent);
However, the Angular team advises against this kind of usage. It not only introduces security vulnerabilities but also creates a strong coupling between your application and the rendering layer, complicating cross-platform support. In my view, the issue isn't accessing the nativeElement itself, but rather invoking platform-specific DOM APIs like textContent. As we'll see shortly, Angular's approach to DOM manipulation rarely necessitates this level of low-level interaction.
The ViewChild decorator can return an ElementRef for any DOM element. Additionally, because every component is nested within a host element and every directive is applied to an element, both component and directive classes can acquire their host element's ElementRef through Dependency Injection (DI):
@Component({
selector: 'sample',
...
export class SampleComponent{
constructor(private hostElement: ElementRef) {
//outputs <sample>...</sample>
console.log(this.hostElement.nativeElement.outerHTML);
}
Thus, while a component can access its host element via DI, the ViewChild decorator is typically used to reference elements within its own view or template. For directives, the situation is reversed—they don't possess views and generally operate directly on the element they are attached to.
TemplateRef
The concept of a template is well-known among web developers—it represents a collection of DOM elements that can be reused across different views. Prior to the HTML5 template tag, templates were often delivered inside a script tag with various type attributes:
<script id="tpl" type="text/template">
<span>I am span in template</span>
</script>
This method had several downsides, including poor semantics and the manual effort required to build DOM structures. With the template tag, the browser parses the html and creates a DOM tree while refraining from rendering it. This content can then be accessed via the content property:
<script>
let tpl = document.querySelector('#tpl');
let container = document.querySelector('.insert-after-me');
insertAfter(container, tpl.content);
</script>
<div class="insert-after-me"></div>
<ng-template id="tpl">
<span>I am span in template</span>
</ng-template>
Angular embraces this pattern and provides the TemplateRef class for working with templates. Here's how you can utilize it:
@Component({
selector: 'sample',
template: `
<ng-template #tpl>
<span>I am span in template</span>
</ng-template>
`
})
export class SampleComponent implements AfterViewInit {
@ViewChild("tpl") tpl: TemplateRef<any>;
ngAfterViewInit() {
let elementRef = this.tpl.elementRef;
// outputs `template bindings={}`
console.log(elementRef.nativeElement.textContent);
}
}
Serving as a placeholder, Angular extracts the template element from the DOM and leaves a comment in its position. This is how it appears when rendered:
<sample>
<!--template bindings={}-->
</sample>
On its own, the TemplateRef class is fairly simple. It stores a reference to its host element in the elementRef property and offers a single method: createEmbeddedView. This method, however, is highly powerful as it allows the creation of a view, returning a reference to it as ViewRef.
ViewRef
This abstraction represents an Angular View. Within the Angular ecosystem, a View is a core building block of the application's UI, representing the smallest set of elements that are created and destroyed together. Angular's philosophy encourages viewing the interface as a composition of Views rather than a collection of independent HTML tags.
Angular supports two distinct types of views:
- Embedded Views connected to a Template
- Host Views connected to a Component
Generating an embedded view
A template acts merely as a blueprint for a view. You can materialize a view from a template using the createEmbeddedView method mentioned earlier:
ngAfterViewInit() {
let view = this.tpl.createEmbeddedView(null);
}
Generating a host view
Host views come into existence when a component is instantiated dynamically. This is accomplished using ComponentFactoryResolver:
constructor(private injector: Injector,
private r: ComponentFactoryResolver) {
let factory = this.r.resolveComponentFactory(ColorComponent);
let componentRef = factory.create(injector);
let view = componentRef.hostView;
}
In Angular, every component is linked to a specific injector instance, so the current injector is passed during component creation. Also, keep in mind that dynamically created components must be registered in the EntryComponents of their module or hosting component.
We've now witnessed how both embedded and host views can be formed. After a view is instantiated, it can be placed into the DOM using ViewContainer. The following section delves into its capabilities.
ViewContainerRef
ViewContainerRef denotes a container where one or more views can be attached.
An important point to note is that any DOM element is eligible to serve as a view container. Interestingly, Angular does not place views inside the element; instead, it appends them after the element associated with the ViewContainer. This behavior is akin to how router-outlet incorporates components.
For marking a location where a ViewContainer should reside, the ng-container element is an excellent choice. It renders as a comment, thereby avoiding extra HTML elements in the DOM. Below is an example of establishing a ViewContainer at a designated point in a component template:
@Component({
selector: 'sample',
template: `
<span>I am first span</span>
<ng-container #vc></ng-container>
<span>I am last span</span>
`
})
export class SampleComponent implements AfterViewInit {
@ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef;
ngAfterViewInit(): void {
// outputs `template bindings={}`
console.log(this.vc.element.nativeElement.textContent);
}
}
Like other DOM abstractions, ViewContainer is associated with a particular DOM element via the element property. In the preceding example, it's connected to the ng-container element, which manifests as a comment, hence the output is template bindings={}.
Managing views
ViewContainer offers a straightforward API for view manipulation:
class ViewContainerRef {
...
clear() : void
insert(viewRef: ViewRef, index?: number) : ViewRef
get(index: number) : ViewRef
indexOf(viewRef: ViewRef) : number
detach(index?: number) : ViewRef
move(viewRef: ViewRef, currentIndex: number) : ViewRef
}
We've previously explored how to manually generate both embedded and host views. Once a view is available, it can be added to the DOM using the insert method. Here's an illustration of creating an embedded view from a template and inserting it at a location marked by an ng-container element:
@Component({
selector: 'sample',
template: `
<span>I am first span</span>
<ng-container #vc></ng-container>
<span>I am last span</span>
<ng-template #tpl>
<span>I am span in template</span>
</ng-template>
`
})
export class SampleComponent implements AfterViewInit {
@ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef;
@ViewChild("tpl") tpl: TemplateRef<any>;
ngAfterViewInit() {
let view = this.tpl.createEmbeddedView(null);
this.vc.insert(view);
}
}
With this setup, the resulting html structure looks like this:
<sample>
<span>I am first span</span>
<!--template bindings={}-->
<span>I am span in template</span>
<span>I am last span</span>
<!--template bindings={}-->
</sample>
To remove a view from the DOM, the detach method comes into play. The remaining methods serve clear purposes—retrieving a view by its index, relocating a view, or clearing all views from the container.
Creating views
ViewContainer also includes an API for automatic view creation:
class ViewContainerRef {
element: ElementRef
length: number
createComponent(componentFactory...): ComponentRef<C>
createEmbeddedView(templateRef...): EmbeddedViewRef<C>
...
}
These serve as convenient shortcuts for the manual processes described previously. They generate a view from either a template or a component and place it at a specified position.
ngTemplateOutlet and ngComponentOutlet
While grasping the underlying mechanics is valuable, having a convenient shortcut is often preferable. These shortcuts come in the form of two directives: ngTemplateOutlet and ngComponentOutlet. As of this writing, both are experimental, and ngComponentOutlet arrives with version 4. However, given what you've absorbed above, their functionality should be apparent.
Discover more about ngTemplateOutlet!
ngTemplateOutlet
This directive designates a DOM element as a ViewContainer and inserts an embedded view derived from a template, sparing you from explicit instantiation in the component class. Consequently, our earlier example that manually created and inserted a view into a #vc DOM element can be simplified as follows:
@Component({
selector: 'sample',
template: `
<span>I am first span</span>
<ng-container [ngTemplateOutlet]="tpl"></ng-container>
<span>I am last span</span>
<ng-template #tpl>
<span>I am span in template</span>
</ng-template>
`
})
export class SampleComponent {}
As observed, no view instantiation code is needed in the component class. Quite convenient!
ngComponentOutlet
This directive parallels ngTemplateOutlet, with the distinction that it generates a host view (by instantiating a component) rather than an embedded view. Its usage looks like this:
<ng-container *ngComponentOutlet="ColorComponent"></ng-container>
Conclusion
I recognize that this information is substantial. Yet, it forms a clear and coherent framework for DOM manipulation through views. You acquire references to Angular's DOM abstractions via a ViewChild query paired with template variable references. ElementRef serves as the simplest wrapper around a DOM element. For templates, TemplateRef enables embedded view creation. Host views are available through componentRef, produced via ComponentFactoryResolver. All views can be managed using ViewContainerRef. Finally, two directives streamline this manual process: ngTemplateOutlet for embedded views and ngComponentOutlet for host views (dynamic components).
