Original link:
Exploring Angular DOM manipulation techniques using ViewContainerRef

To learn more about Angular manipulating DOM using Renderer and View Containers techniques, check out the YouTube video My Talk at NgVikings.

Every time I read articles about How Angular works with the DOM, I find references to ElementRef, TemplateRef, ViewContainerRef, and other classes. Although these classes are covered in official Angular documentation or related articles, the overall idea is rarely described, and there are few examples of how these classes work together, which is what this article focuses on.

If you come from the angular.js world, it’s easy to understand how to manipulate the DOM using Angular. js. Angular. js injects DOM Element into the link function. You can query any node in the component template, add or remove nodes, modify styles, and so on. However, this approach has a major drawback: tight coupling to the browser platform.

The new Angular version needs to run on different platforms, such as Browser, Mobile or Web Worker, so it needs a layer of abstraction between the platform specific API and framework interface. This layer of abstraction in Angular includes the reference types: ElementRef, TemplateRef, ViewRef, ComponentRef, and ViewContainerRef. This article explains each reference type in detail and how that reference type manipulates the DOM.

@ViewChild

Before exploring DOM abstract classes, take a look at how to get them in components/directives. Angular provides a technique called DOM Query, mainly derived from @ViewChild and @ViewChildren decorators. The basic function is the same, except that @ViewChild returns a single reference and @ViewChildren returns multiple references wrapped around QueryList objects. The examples in this article focus on ViewChild and omit the @ from the description.

These two decorators are usually used with template reference variables, which are simply named references to DOM elements in the template. Similar to the ID attribute of an HTML element. You can mark a DOM element with a Template reference and use the ViewChild decorator to query it in a component/directive class, for 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); }}Copy the code

The basic syntax for ViewChild decorator is:

@ViewChild([reference from template], {read: [reference type]});Copy the code

As you can see from the previous example, I used tref as the template reference name and associated ElementRef with that element. The second argument read is optional because Angular infers the reference type based on the type of the DOM element. For example, if it (#tref) mounts a simple HTML element like span, Angular returns ElementRef; Angular returns TemplateRef if it mounts a template element. Some reference types, such as ViewContainerRef, cannot be inferred by Angular and must be explicitly declared in the read argument. Others, such as viewrefs, cannot be mounted in DOM elements, so they must be manually encoded in constructors.

Now, let’s see how to get these references and explore them.

ElementRef

This is the most basic abstract class, and if you look at its class structure, you’ll see that it contains only the element objects it mounts. This is useful for accessing native DOM elements, such as:

// outputs `I am span`
console.log(this.tref.nativeElement.textContent);Copy the code

However, the Angular team discourels this approach because it exposes security risks and makes your application tightly coupled to the rendering layers, making it difficult to run on multiple platforms. I don’t think this problem is caused by using nativeElement but by using a specific DOM API, such as textContent. But as you’ll see later, Angular implements the overall thinking model for manipulating the DOM, eliminating the need for lower-level apis such as textContent.

DOM elements decorated with ViewChild return ElementRef, but since all components are mounted to custom DOM elements, all directives apply to DOM elements, So both components and directives can retrieve the ElementRef object of the host element via DI (Dependency Injection). Such as:

@Component({
    selector: 'sample',
    ...
export class SampleComponent{
      constructor(private hostElement: ElementRef) {
          //outputs <sample>...</sample>
             console.log(this.hostElement.nativeElement.outerHTML);
      }
    ...Copy the code

So a component can access its host element via DI (Dependency Injection), but the ViewChild decorator is often used to get DOM elements from template views. Directives, on the other hand, do not have a view template, so they are mainly used to get host elements that they mount.

TemplateRef

The concept of a template will be familiar to most developers, as a collection of DOM elements in a cross-program view. Before HTML5 introduced the template tag, browsers introduced templates by setting the type attribute inside script tags, such as:

<script id="tpl" type="text/template">
  <span>I am span in template</span>
</script>Copy the code

This approach has semantic drawbacks and requires manual creation of the DOM model. However, with the Template tag, the browser can parse the HTML and create a DOM tree, but not render it. The DOM tree can be accessed via the Content property, for example:

<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>Copy the code

Angular uses the Template tag to implement the TemplateRef abstract class to work with the template tag and see how it works. Ng-template is an Angular native HTML tag similar to template) :

@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); }}Copy the code

The Angular framework removes the template element from the DOM and inserts a comment in its place. This is what it looks like after rendering:

<sample> <! --template bindings={}--> </sample>Copy the code

TemplateRef is a simple abstract class whose elementRef attribute is a reference to its host element and a createEmbeddedView method. The createEmbeddedView method is useful, however, because it creates a view and returns the view’s reference object, ViewRef.

ViewRef

This abstraction represents an Angular View. In the Angular world, a View is a collection of elements that are created and destroyed together and are the building blocks of a program’s UI. Angular encourages developers to think of the UI as a collection of views, not just a tree of HTML tags.

Angular supports two types of views:

  • Embedded View, byTemplateprovide
  • The Host View is created byComponentprovide

Creating an embedded view

A template is simply a blueprint for a view, which can be created using the createEmbeddedView method mentioned earlier, for example:

ngAfterViewInit() {
    let view = this.tpl.createEmbeddedView(null);
}Copy the code

Creating a host view

The host view is created when the component is dynamically instantiated. A Dynamic Component can be created by ComponentFactoryResolver:

constructor(private injector: Injector,
            private r: ComponentFactoryResolver) {
    let factory = this.r.resolveComponentFactory(ColorComponent);
    let componentRef = factory.create(injector);
    let view = componentRef.hostView;
}Copy the code

Angular has an Injector instance tied to each component, so the ColorComponent was created by passing in the Injector of the current component (SampleComponent). Also, don’t forget that dynamically creating a component requires adding the created component to the EntryComponents property of the module or host component.

Now that we have seen how the embedded view and host view are created, once the view is created, it can be inserted into the DOM tree using the ViewContainer. This feature is explored below.

ViewContainerRef

A view container is a container that holds one or more views.

The first thing to note is that any DOM element can be used as a ViewContainer. Interestingly, Angular does not insert a view inside a DOM element bound to a ViewContainer, but appends it to the element. This is similar to how router-outlet inserts components.

In general, it is a good idea to bind ViewContainer to the ng-Container element, because the ng-Container element will be rendered as an annotation and will not introduce extra HTML elements into the DOM. The following example describes how to create a ViewContainer in a build 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); }}Copy the code

Like other abstract classes, ViewContainer binds DOM elements via the Element property, such as the ng-Container element that is rendered as an annotation, so the output will also be Template Bindings ={}.

The operational view

ViewContainer provides several action view apis:

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 }Copy the code

We already know how to create two types of views from templates and components: embedded views and component views. Once you have a view, you can insert it into the DOM using the INSERT method. The following example describes how to create an embedded view from a template and insert it in place of the Ng-Container tag.

@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); }}Copy the code

With the above implementation, the final HTML 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>Copy the code

The DOM can be removed from a view using the detach method, and other methods can be known by the method name, such as retrieving view reference objects by index, moving the view, or removing all views from the view container.

Create a view

ViewContainer also provides an API for manually creating views:

class ViewContainerRef { element: ElementRef length: number createComponent(componentFactory...) : ComponentRef<C> createEmbeddedView(templateRef...) : EmbeddedViewRef<C> ... }Copy the code

The above two methods are a good wrapper to create a view by passing in a template reference object or component factory object and inserting that view at a specific location in the view container.

NgTemplateOutlet and ngComponentOutlet

While it would be nice to know how Angular manipulates the DOM internally, it would be nice to have a shortcut. Yes, Angular provides two shortcut directives: ngTemplateOutlet and ngComponentOutlet. Both directives are experimental at the time of writing, and ngComponentOutlet will be available in version 4 as well. If you have read the above, it is easy to know what these two instructions do.

ngTemplateOutlet

This directive marks the DOM element as a ViewContainer and inserts the embedded view created by the template, eliminating the need to explicitly create the embedded view in the component class. Thus, the code for creating an embedded view and inserting the #vc DOM element in the above example can be rewritten:

@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 {}Copy the code

You can see from the example above that we don’t need to write any code in the component class to instantiate the view. Very convenient, isn’t it?

ngComponentOutlet

This directive is similar to ngTemplateOutlet, except that ngComponentOutlet creates a host view generated by component instantiation, not an embedded view. You can use it like this:

<ng-container *ngComponentOutlet="ColorComponent"></ng-container>Copy the code

conclusion

It may seem like a lot of new knowledge to digest, but Angular’s model of thinking about manipulating the DOM through views is clear and coherent. You can use ViewChild to query template reference variables to get Angular DOM abstract classes. The simplest wrapper for a DOM element is ElementRef; For templates, you can use TemplateRef to create embedded views; For components, you can use ComponentRef to create the host view, and you can use ComponentFactoryResolver to create ComponentRef. The two created views (the embedded view and the host view) are in turn managed by the ViewContainerRef. Finally, Angular provides two more shortcut directives to automate the process: the ngTemplateOutlet directive uses templates to create embedded views; NgComponentOutlet uses dynamic components to create hosted views.