The original link recently took over a project where a client came up with a lofty request: a single home screen, with all components displayed via tabs. This is not a weird requirement, as customers who don’t like interface hopping are very keen on this form of presentation.

All right, customer first, get it done! This implementation is very simple in traditional HTML applications, just in Angular4.

Let’s take a look at two ways to dynamically load components in NG:

  1. Loading declared components: Render one component instance to another component view using ComponentFactoryResolver;
  2. Dynamically create and load components: Create and render components using ComponentFactory and Compiler

According to our requirements, each component is developed in advance and needs to be displayed on the same component. So the first way meets our requirements.

To use ComponentFactoryResolver to dynamically load components, you need to understand the following concepts:

  1. ViewChild: property decorator, which can be used to obtain the corresponding element on the view;
  2. ViewContainerRef: A view container on which components can be created and deleted;
  3. ComponentFactoryResolver: A component parser that renders one component on the view of another component.

Once you understand the concept, look at the code!

HTML code:

<dynamic-container [componentName] ="'RoleComponent'" >
</dynamic-container>
Copy the code

Ts code:

import {Component, Input, ViewContainerRef, ViewChild,ComponentFactoryResolver,ComponentRef,OnDestroy,OnInit} from '@angular/core';
import {RoleComponent} from "./role/role.component";
@Component({
  selector: 'dynamic-container', entryComponents: [RoleComponent,....] .// Specify the name of the component to be dynamically loaded. Otherwise, an error will be reported
  template: "<ng-template #container></ng-template>"
})
export class DynamicComponent implements OnDestroy,OnInit {
  @ViewChild('container', { read: ViewContainerRef }) container: ViewContainerRef;
    @Input() componentName      // The name of the component to load
    compRef: ComponentRef<any>; // The loaded component instance
    constructor(private resolver: ComponentFactoryResolver) {}
    loadComponent() {
      let factory = this.resolver.resolveComponentFactory(this.componentName);
      if (this.compRef) {
        this.compRef.destroy();
        }
      this.compRef = this.container.createComponent(factory) // Create a component
    }
    ngAfterContentInit() {
      this.loadComponent()
    }
    ngOnDestroy() {
      if(this.compRef){
          this.compRef.destroy(); }}}Copy the code

The code is not complicated!

However, what if the loaded component has passed in parameters, such as modifying a role component that requires a role ID? The solution is to use ReflectiveInjector to inject the required parameters into the component when loading it. Code adjustments are as follows:

Inputs: HTML code for inputs: inputs

<dynamic-container [componentName] ="'RoleComponent'" [inputs] ="{'myName':'dynamic'}" ></dynamic-container>
Copy the code

Ts code:

import { ReflectiveInjector} from '@angular/core'; .export class DynamicComponent implements OnDestroy,OnInit {
  
    @Input() inputs:any         // Load the set of arguments that the component needs to pass in. loadComponent() {let factory = this.resolver.resolveComponentFactory(this.componentName);
      if(!this.inputs)
          this.inputs={}
  
      let inputProviders = Object.keys(this.inputs).map((inputName) = > {
          return {provide: inputName, useValue: this.inputs[inputName]}; });let resolvedInputs = ReflectiveInjector.resolve(inputProviders);
      let injector = ReflectiveInjector.fromResolvedProviders(resolvedInputs, this.container.parentInjector);
      if (this.compRef) {
        this.compRef.destroy();
        }
      this.compRef = factory.create(injector) // Create a component with parameters
      this.container.insert(this.compRef.hostView);// Render the view of the component
  }
  ngAfterContentInit() {
    this.loadComponent()
  }
  ......
}
////RoleComponent code is as follows
export class RoleComponent implements OnInit {
    myName:string.constructor() {//this.myName has the value dynamic}}Copy the code

At this point, the dynamically loaded component’s interface proudly drops on the interface. Wait, something seems wrong! Why is the data obtained from the background not loaded on the interface?

The code to get the data is as follows:

export class RoleComponent implementsOnInit { roleList=[]; .constructor(private _roleService.list:RoleService) {
      this._roleService.list().subscribe(res= >{
          this.roleList=res.roleList; }); }... }Copy the code

After repeated testing, the conclusion is as follows: the data obtained from the background through HTTP has been obtained, but ng is not triggered for change detection, so the interface does not render the data.

Studying ng’s documentation with the belief that it is “pit filled”, you see that NG supports manually triggering change detection, as long as change detection is invoked in the appropriate place. At the same time, NG provides different levels of change detection:

  • Change detection strategy:
    • Default: the detection policy of Default provided by ng triggers detection as long as the input of the component changes.
    • OnPush: The OnPush detection policy is that the detection is not triggered immediately when the input changes. Instead, the detection is triggered when the input reference changes.
  • ChangeDetectorRef. DetectChanges () : can be explicitly change detection, the control of the need to use;
  • Ngzone.run () : Change detection throughout the application
  • Applicationref.tick () : Checks for changes throughout the application and listens for onTurnDone events in the NgZone to trigger checks

According to the documentation, the NG app uses NgZone to detect changes by default, which is fine for normally loaded components, but not for dynamically loaded components. Several trials, only the second method works: explicitly call ChangeDetectorRef. DetectChanges ()

So change the ts code:

interval:any
loadComponent() {
    ......
    this.interval=setInterval((a)= > {
      this.compRef.changeDetectorRef.detectChanges();
    }, 50);  // Detect changes once in 50 milliseconds
}
ngOnDestroy() {
    ......
    clearInterval(this.interval)
}
Copy the code

Given my rudimentary NG skills, THIS clumsy way to solve the data loading problem was like a lump in my throat, and I always thought there was a more elegant way to solve it, which I’ll spend more time working on.

Wordy so far, if there is something wrong in the article, welcome to point out.

As a side note, PrimeNG is highly recommended for its rich front-end components, which can be easily accessible and greatly save interface development speed.

References:

  1. Angular 2 Change Detection, Zones and an example
  2. Angular change detection explained
  3. In-depth understanding of Angular2 change monitoring and ngZone