In-depth Angular: (Re/Translation) Working with DOM in Angular: unexpected consequences and optimization techniques

Summary of premise:

This article introduces a clever optimization technique that can be applied to scenarios commonly used by ngFor. You'll learn what an embedded view is and how to reuse it instead of destroying it on every iteration.

Maxim koretskyi delivered a seminar on advanced DOM operations in Angular at NgConf. From basics like using template references and DOM queries to access DOM elements, to using view containers to dynamically render templates and components.

Original video:https://www.youtube.com/watch?v=qWmqiYDrnDc

I summarize the key concepts in this article. I'll start by explaining the tools and methods for working with the DOM in Angular, and then move on to more advanced optimization techniques that I didn't touch on during the workshop.

You can find the examples used in the talk in this github repository.

InDepthApp link:IndepthApp

Prerequisite knowledge:

<ng-container> is used to perform logical operations in a template without adding extra DOM elements. It is an invisible container that can be used to organize and control structure and behavior in templates, but does not generate any additional markup in the final rendered HTML.

<ng-template> is used to define a template fragment for reuse in components. It can contain any number of HTML elements and Angular directives, but nothing will be displayed when rendered. It is often used with structural directives (such as *ngIf and *ngFor) to dynamically insert or remove template content as needed.

View view engine

Suppose you have a task that removes a child component from the DOM. This is the template of the parent component, which containsAthe child components that need to be deleted:

@Component({
  ...
  template: `
    <button (click)="remove()">Remove child component</button>
    <a-comp></a-comp>
  `
})
export class AppComponent {}

Work around this taskby using the renderer or the native DOM API<a-comp>to delete the DOM element directly:

@Component({...})
export class AppComponent {
  ...
  remove() {
    this.renderer.removeChild(
       this.hostElement.nativeElement,      // parent App comp node
       this.childComps.first.nativeElement  // child A comp node
     );
  }
}

You can view the complete solution here. If you inspect the generated HTML in the tab Elements after deleting the node, you will see that the child A component no longer exists in the DOM:

However, if you then check the console, Angular still reports the number of child components as 1not0. To make matters worse, change detectionstillruns against childAcomponents and their subcomponents. This is the log from the console:

Why?​ 

This happens because Angular internally uses what is often called a View or Component View The data structure represents the component. Here is a diagram representing the relationship between the view and the DOM:

This happens because Angular internally uses what is often called a View or Component View The data structure represents the component. Here is a diagram representing the relationship between the view and the DOM:

Each view consists of view nodes that hold references to corresponding DOM elements. So when we change the DOM directly, the view nodes that are inside the view and hold references to that DOM element are not affected. AThe following diagram shows the state of the view and DOM after we have removed the component element from the DOM:

Since all change detection operations (including ViewChildren) run on the View and not the DOM, Angular detects the one corresponding to the component. View and report number  instead of as expected. Additionally, since the viewcorresponding to the component exists, it also runs change detection on thecomponent and all of its subcomponents. A10AA

This shows that you cannot simply remove the subcomponent directly from the DOM. In fact, you should avoid deleting any HTML elements created by the framework and only delete elements that Angular doesn't know about. These may be elements created by your code or some 3rd party plugin.

To solve this task correctly we need a tool that works directly with views, such a tool in Angular isView Container .

View container 

View containers can safely change the DOM hierarchy and are used by all built-in structural directives in Angular. It is a special kind of view node that sits inside a view and acts as a container for other views:

As you can see, it can accommodate two types of views:embedded views and hosted views.

These are the only view types that exist in Angular, and they differ primarily depending on the input data used to create them. Additionally, embedded views can only be attached to a view container, whereas host views can be attached to any DOM element (often called a host element).

In Angular, embedded views and host views refer to the relationship between components.

**Host View** refers to a component that contains one or more embedded views. It is a component with its own template and logic, and can contain other components as its child components.

**Embedded views** refer to components contained by host views. It is created and managed by the host view and is used to display specific content or functionality. Embedded views can be reused by multiple host views.

The difference is:
- The host view is an independent component with its own template and logic, and can contain other components as subcomponents.
- An embedded view is a component included by the host view, created and managed by the host view, and used to display specific content or functionality.

You can think of the host view as acontainer,while the embedded view is placed inside The contents of the container. The host view is responsible for creating and managing embedded views, and providing context and data for embedded views to use.

What this text means is that in Angular, there are two types of views: embedded views and host views. They differ mainly in the input data they use and where they can be attached.

Embedded views can only be attached to a view container, whereas host views can be attached to any DOM element (often called a host element). This means that host views can be rendered at different locations throughout the application, while embedded views are constrained inside the container.

The following is a simple code example that shows how to create and use embedded and hosted views:

import { Component, ViewContainerRef, ViewChild, TemplateRef } from '@angular/core';

@Component({
  selector: 'app-example',
  template: `
    <div>
      <h1>嵌入视图</h1>
      <ng-template #embeddedView>
        <p>这是一个嵌入视图。</p>
      </ng-template>
      <ng-container *ngTemplateOutlet="embeddedView"></ng-container>
    </div>
    <div>
      <h1>宿主视图</h1>
      <ng-container #hostView></ng-container>
    </div>
  `
})
export class ExampleComponent {
  @ViewChild('hostView', { read: ViewContainerRef }) hostViewContainer: ViewContainerRef;
  @ViewChild('embeddedView', { read: TemplateRef }) embeddedViewTemplate: TemplateRef<any>;

  constructor() {}

  ngAfterViewInit() {
    // 创建宿主视图
    this.hostViewContainer.createEmbeddedView(this.embeddedViewTemplate);
  }
}

In the above example, we define a component `ExampleComponent`, which contains an embedded view and a host view. Using the `ViewChild` decorator, we obtain references to the host view container and the embedded view template.

In the `ngAfterViewInit` lifecycle hook, we attach the embedded view template to the host view container by calling the `createEmbeddedView` method.

In this way, when we use `ExampleComponent` in our application, the contents of both the embedded view and the host view will be displayed.

Embedded views are created from templates using TemplateRef, while host views are created using view (component) factories. For example, the main component ( AppComponent) used to bootstrap the application is represented internally as a host view attached to the component's host element ( <app-comp>).

View containers provide APIs to create, manipulate, and delete dynamic views. I call them dynamic views instead of static views created by the framework for static components in templates. Angular does not use a view container for static views, instead it holds references to subviews within nodes specific to the subcomponent. The diagram below illustrates this idea:

As you can see, there is no view container node here, the reference to the subview is attached directly to theA component view node.


Manipulating dynamic views

Before you start creating a view and attaching it to a view container, you need to introduce the container into the component's template and initialize it. Any element within a template can act as a view container, but the most common candidate for this role is <ng-container> because it is rendered as an annotation node and therefore does not introduce redundant elements to the DOM.

To convert any element into a view container, we use{read: ViewContainerRef}view query option:

@Component({
   template: `<ng-container #vc></ng-container>`
})
export class AppComponent implements AfterViewChecked {
   @ViewChild('vc', {read: ViewContainerRef}) viewContainer: ViewContainerRef;
}

Once Angular evaluates the view query and assigns a reference to the view container to a class property, you can use that reference to create a dynamic view.

Create an embedded view

To createembed a view, you need atemplate . In Angular, we use <ng-template>element to wrap any DOM element and define the structure of the template. We can then simply use a view query with parameters{read: TemplateRef} to get a reference to the template:

@Component({
  ...
  template: `
    <ng-template #tpl>
        <!-- any HTML elements can go here -->
    </ng-template>
  `
})
export class AppComponent implements AfterViewChecked {
    @ViewChild('tpl', {read: TemplateRef}) tpl: TemplateRef<null>;
}

You should implement the logic within the lifecycle hook,ngAfterViewInitbecause this is when the view query is initialized. Additionally, for embedded views, you can define a context object that contains values ​​for in-template binding.

Create a host view

To createhost a view, you need a componentfactory. To learn more about factories and dynamic components, check outHere's what you need to know about dynamic components in Angular.

In Angular, we use this[componentFactoryResolver](https://angular.io/api/core/ComponentFactoryResolver)service to get a reference to the component factory:

@Component({ ... })
export class AppComponent implements AfterViewChecked {
       
       
  ...
  constructor(private r: ComponentFactoryResolver) {}
  ngAfterViewInit() {
       
       
    const factory = this.r.resolveComponentFactory(ComponentClass);
  }
 }
}

Once we have the component's factory, we can use it to initialize the component, create the host view and attach that view to the view container. To do this, we simply call the createComponent method and pass in the component factory:

@Component({ ... })
export class AppComponent implements AfterViewChecked {
       
       
    ...
    ngAfterViewInit() {
       
       
        this.viewContainer.createComponent(this.factory);
    }
}

You can find a complete example of creating a host view here.

Delete view

Any view attached to a view container can be removed using the remove or detach method. Both methods remove the view from the view container and the DOM. However, while the removemethod destroys the view so that it cannot be reattached later, the detachmethod preserves the view for future reuse, which is useful for me to follow. The optimization techniques shown below are very important.

Therefore, in order to properly solve the task of removing a child component or any DOM element, it is necessary to first create an embedded view or a host view and attach it to the view container. Once this is done, you will be able to remove it from the view container and DOM using any available API method.

Optimization technology

Sometimes you may need to repeatedly render and hide the same component or HTML defined by a template. In the example below, by clicking a different button, we will switch components to display:

This can be achieved if we simply use the method we learned above and put the knowledge into the following code:

@Component({...})
export class AppComponent {
  show(type) {
    ...
    // a view is destroyed
    this.viewContainer.clear();
    
    // a view is created and attached to a view container      
    this.viewContainer.createComponent(factory);
  }
}

Every time the button is clicked and the show method is executed, we have the undesirable effect of destroying and recreating the view.

createComponentIn this particular example, since we are using component factories and methods, the host view is destroyed and recreated. If we use the createEmbeddedView method with TemplateRef, the embedded view will be destroyed and recreated:

show(type) {
    ...
    // a view is destroyed
    this.viewContainer.clear();
    
    // a view is created and attached to a view container
    this.viewContainer.createEmbeddedView(this.tpl);
}

Ideally, we need to create the view once and then reuse it when needed. The view container API provides a way to attach an existing view to a view container and later remove it without destroying it.

ViewRef

Both ComponentFactory and TemplateRef implement view creation methods that can be used to create views. In fact, the view container uses these methods behind the scenes when you call its createEmbeddedView or createComponent method and pass in the input data. The good news is that we can call these methods ourselves to create an embedded or hosted view and get a reference to the view. In Angular, views are referenced using the ViewRef type and its subtypes.

Create a host view

This is how you use a component factory to create a host view and get a reference to it:

aComponentFactory = resolver.resolveComponentFactory(AComponent);
aComponentRef = aComponentFactory.create(this.injector);
view: ViewRef = aComponentRef.hostView;

In the case of a hosted view, the view associated with the component can be retrieved from the ComponentRef returned by the create method. It is exposed through the property of the same name hostView.
Once we have the view, we can attach it to the view container using the insert method. Other views that are no longer required to be displayed can be removed using the detach method and retained. Therefore, an optimized solution to the task of switching components should be implemented like this:

showView2() {
    ...
    // Existing view 1 is removed from a view container and the DOM
    this.viewContainer.detach();
    // Existing view 2 is attached to a view container and the DOM
    this.viewContainer.insert(view);
}

Note again that we use the detach method instead of clearorremove to preserve the view for later reuse. You can find the complete implementation here.

Create an embedded view

If the view is created based on a templateembedded, the view is returned directly through the methodcreateEmbeddedView: a>

view1: ViewRef;
view2: ViewRef;
ngAfterViewInit() {
    this.view1 = this.t1.createEmbeddedView(null);
    this.view2 = this.t2.createEmbeddedView(null);
}

Then, similar to the previous example, you can remove a view from the view container and reattach another view. Once again you can find the complete implementation here.

Interestingly, both the view creation methodcreateEmbeddedViewandcreateComponentthe view container return a reference to the view created.

Guess you like

Origin blog.csdn.net/dongnihao/article/details/134626128