Here is what you need to know about dynamic components in Angular

This article explains how to create components dynamically in Angular (note: components used in templates can be called statically created components).

If you were programming with AngularJS before, you might have used the $compile service to generate HTML and connect it to the data model for bi-directional binding:

const template = '<span>generated on the fly: {{name}}</span>'
const linkFn = $compile(template);
const dataModel = $scope.$new(a); dataModel.name ='dynamic';

// link data model to a template
linkFn(dataModel);
Copy the code

AngularJS directives can modify the DOM, but there’s no way to know what they’re modifying. The problem with this approach is the same as with dynamic environments, which makes it difficult to optimize performance. Dynamic templates are certainly not the main culprit for AngularJS’s slow performance, but they are an important one.

After looking at Angular internal code for a while, I found that this newly designed framework is very performance-oriented. You’ll often find these lines in Angular source code:

Attention: Adding fields to this is performance sensitive!

Note: We use one type for all nodes so that loops that loop over all nodes of a ViewDefinition stay monomorphic!

For performance reasons, we want to check and update the list every five seconds.
Copy the code

So Angular designers decided to sacrifice flexibility for huge performance gains, such as JIT and AOT Compiler, static templates, instruction/module Factory, ComponentFactoryResolver These concepts are unfamiliar and even hostile to the AngularJS community, but don’t worry, if you’ve only heard of them before and now want to know what they are, read on and you’ll find out.

Note: The JIT/AOT Compiler is actually the same Compiler, but it is used in the building time or running time phase.

As for factory, Angular Compiler compiles components you write like a.component.ts to a.com ponent.ngFactory.js. That is, Compiler uses the @Component decorator as the raw material to compile your Component/directive class into another view factory class.

Back to JIT/AOT Compiler. If a.component.ngFactory.js is generated during build, it is AOT Compiler. If the Compiler is generated during the Run phase, it needs to be packaged into a dependency package, downloaded locally by the user, and then compiled into the component/instruction classes to generate the corresponding view factory classes at runtime. Here’s a look at what the *.ngFactory.js file code looks like.

The Factory resolver is even simpler. It’s an object that gets those compiled Factory objects.

Component factories and compilers

Every Component in Angular is created by a Component factory, which in turn is compiled by the compiler from the metadata in the @Component decorator you wrote. If you are a little confused by the number of decorator articles you have read on the Web, refer to my Medium article Implementing Custom Component Decorator.

Angular uses the concept of views internally, or the framework as a view tree. Each view is made up of a number of different types of nodes: element nodes, text nodes, and so on. Each node has a specific role, so that each node takes very little time to process, and each node has services such as ViewContainerRef and TemplateRef to use, You can also use ViewChild/ViewChildren DOM querying and ContentChild/ContentChildren do these nodes.

Note: Simply put, An Angular application is a tree of views. Each view is made up of multiple nodes, each of which provides a template manipulation API for developers to use. These nodes are available through the DOM Query API.

Each node contains a large amount of information and, for performance reasons, takes effect once the node is created and cannot be changed later (note: created nodes are cached). Node generation is the process by which the compiler collects information about the components you write and encapsulates it in a component factory.

Suppose you write a component like this:

@Component({
  selector: 'a-comp',
  template: '<span>A Component</span>'
})
class AComponent {}
Copy the code

Based on the information you write, the compiler generates component factory code like this, which contains only the important parts (note: the entire code below can be interpreted as views, where elementDef2 and jit_textDef3 can be interpreted as nodes) :

function View_AComponent_0(l) {
  return jit_viewDef1(0,[
      elementDef2(0.null.null.1.'span',...). , jit_textDef3(null['My name is '. ] )]Copy the code

The above code basically describes the structure of the component view and is used to instantiate a component. The first node elementDef2 is the element node definition, and the second node jit_textDef3 is the text node definition. You can see that each node has enough parameter information to instantiate, which is generated by the compiler parsing all dependencies and provided by the framework at run time.

As you know from the previous section, if you have access to the component factory, you can use it to instantiate the corresponding component object and insert the component/view into the DOM using the ViewContainerRef API. If you’re interested in ViewContainerRef, explore Angular’s use of ViewContainerRef to manipulate the DOM. How to use this API (note: the following code shows how to use the ViewContainerRef API to insert a view into the view tree) :

export class SampleComponent implements AfterViewInit {
    @ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef;

    ngAfterViewInit() {
        this.vc.createComponent(componentFactory); }}Copy the code

Ok, as you can see from the code above, once you get to the component factory, everything will be solved. Now, the question is how to get the ComponentFactory ComponentFactory object, keep looking.

Modules and ComponentFactoryResolver

Although AngularJS has modules, it lacks the true namespace required for directives, has potential naming conflicts, and can’t encapsulate directives in separate modules. Fortunately, However, Angular has learned its lesson and provides proper namespaces for declarative types such as directives, components, and pipes.

Just like AngularJS, components in Angular are wrapped in modules. Components do not exist on their own. If you want to use a component of another module, you must import the module:

@NgModule({
    // imports CommonModule with declared directives like
    // ngIf, ngFor, ngClass etc.
    imports: [CommonModule],
    ...
})
export class SomeModule {}
Copy the code

Similarly, if a module wants to provide components for other modules to use, it must export those components, which can be found in the Exports property. For example, you can look at CommonModule source code:

constCOMMON_DIRECTIVES: Provider[] = [ NgClass, NgComponentOutlet, NgForOf, NgIf, ... ] ;@NgModule({ declarations: [COMMON_DIRECTIVES, ...] , exports: [COMMON_DIRECTIVES, ...] . })export class CommonModule {
}
Copy the code

So each component is bound to a module, and you can’t declare the same component in different modules. Angular throws an error if you do:

Type X is part of the declarations of 2 modules: ...
Copy the code

When Angular compiles an application, the compiler compiles components registered with the entryComponents property in a module, or used in a template, into a component factory. A good example of a dynamic component in the entryComponents definition is the Angular Material Dialog component, DialogContentComp components can be registered in entryComponents to dynamically load dialog box content. You can see the compiled component factory file in the Sources TAB:

As we know from above, if we can get the component factory, we can use the component factory to create the corresponding component object and insert it into the view. In effect, each module provides a service ComponentFactoryResolver that gets the component factory for all components. So, if you define a BComponent in a module and want to get its component factory, you can inject this service into the component and use it:

export class AppComponent {
  constructor(private resolver: ComponentFactoryResolver) {
    // now the `factory` contains a reference to the BComponent factory
    const factory = this.resolver.resolveComponentFactory(BComponent);
  }
Copy the code

This is only possible if both the AppComponent and the BComponent are defined in a module, or if the module already has a component factory for the BComponent when importing other modules.

Dynamically load and compile modules

But what if the component is defined in another module, and this module is loaded on demand? We can actually get a component factory for a component in much the same way as loading modules on demand using the loadChildren configuration item.

There are two ways to load modules at run time. The first way is to use the SystemJsNgModuleLoader module loader. If you use the SystemJS loader, routes also use the SystemJsNgModuleLoader module loader when loading child routing modules. The SystemJsNgModuleLoader module loader has a load method that loads the module into the browser and compiles the module and all components declared in it. The load method takes the file path argument, along with the name of the exported module, and returns NgModuleFactory:

loader.load('path/to/file#exportName')
Copy the code

Note: The NgModuleFactory source code is in packages/ Core/Linker. The code in this folder is mainly adhesive code, which contains interface classes for core modules. The specific implementation is in other folders.

If no specific exported module name is specified, the loader uses the default keyword default exported module name. Also note that to use SystemJsNgModuleLoader you need to register it like this:

providers: [
    {
      provide: NgModuleFactoryLoader,
      useClass: SystemJsNgModuleLoader
    }
  ]
Copy the code

You can of course use any token in provide, but routing modules use the NgModuleFactoryLoader token, so it’s best to use the same token as well. (NgModuleFactoryLoader)

The complete code for the module to load and get the component factory is as follows:

@Component({
  providers: [
    {
      provide: NgModuleFactoryLoader,
      useClass: SystemJsNgModuleLoader
    }
  ]
})
export class ModuleLoaderComponent {
  constructor(private _injector: Injector,
              private loader: NgModuleFactoryLoader) {
  }

  ngAfterViewInit() {
    this.loader.load('app/t.module#TModule').then((factory) = > {
      const module = factory.create(this._injector);
      const r = module.componentFactoryResolver; const cmpFactory = r.resolveComponentFactory(AComponent); // create a component and attach it to the view const componentRef = cmpFactory.create(this._injector); this.container.insert(componentRef.hostView); }}})Copy the code

But there is a problem with SystemJsNgModuleLoader. Inside the load() function of the code above See L70) uses the compiler’s compileModuleAsync method, which creates component factories only for components registered in entryComponents or used in component templates. But what if you just don’t want to register your component in the entryComponents property? There is still a solution – compileModuleAndAllComponentsAsync method is used to load module. The method for all components generated components factory in the module, and returns the ModuleWithComponentFactories objects:

class ModuleWithComponentFactories<T> {
    componentFactories: ComponentFactory<any> []; ngModuleFactory: NgModuleFactory<T>;Copy the code

The following code completely shows how to use this method to load modules and get component factories for all components (note: this is the second way mentioned above) :

ngAfterViewInit() {
  System.import('app/t.module').then((module) = > {
      _compiler.compileModuleAndAllComponentsAsync(module.TModule)
        .then((compiled) = > {
          const m = compiled.ngModuleFactory.create(this._injector);
          const factory = compiled.componentFactories[0];
          const cmp = factory.create(this._injector, [], null, m); })})}Copy the code

Remember, however, that this method uses the compiler’s proprietary API, as documented in the source code below:

One intentional omission from this list is @angular/compiler, which is currently considered a low level api and is subject to internal changes. These changes will not affect any applications or libraries using the higher-level apis (the command line interface or JIT compilation via @angular/platform-browser-dynamic). Only very specific use-cases require direct access to the compiler API (mostly tooling integration for IDEs, linters, etc). If you are working on this kind of integration, please reach out to us first.

The runtime creates components dynamically

From above we saw how components can be created dynamically through component factories in modules, where modules are defined before runtime and can be loaded early or lazily. However, it is possible to create modules and components at run time, just like AngularJS, without having to define modules in advance.

Take a look at the AngularJS code above to see how it works:

const template = '<span>generated on the fly: {{name}}</span>'
const linkFn = $compile(template);
const dataModel = $scope.$new(a); dataModel.name ='dynamic'

// link data model to a template
linkFn(dataModel);
Copy the code

The code above summarizes the general flow of dynamically creating a view as follows:

  1. Define a component class and its properties, and decorate the component class with a decorator
  2. Define the module class, declare the component class in the module class, and decorate the module class with a decorator
  3. Compile the module and all the components in the module, take all the component factories

Module classes are just ordinary classes with module decorators, as are Component classes, and since decorators are simply functions that are available at run time, we can decorate any class with decorators like @NgModule()/@Component() if we want to. The following code shows how to create a component dynamically in its entirety:

@ViewChild('vc', {read: ViewContainerRef}) vc: ViewContainerRef;

constructor(private _compiler: Compiler,
            private _injector: Injector,
            private _m: NgModuleRef<any>) {
}

ngAfterViewInit() {
  const template = '<span>generated on the fly: {{name}}</span>';

  const tmpCmp = Component({template: template})(class{});const tmpModule = NgModule({declarations: [tmpCmp]})(class{});this._compiler.compileModuleAndAllComponentsAsync(tmpModule)
    .then((factories) = > {
      const f = factories.componentFactories[0];
      const cmpRef = this.vc.createComponent(tmpCmp);
      cmpRef.instance.name = 'dynamic'; })}Copy the code

For better debugging information, you can replace the anonymous classes in the code above with any class.

Ahead-of-Time Compilation

The compiler mentioned above is a just-in-time (JIT) compiler. You may have heard of ahead-of-time (AOT) compiler. Angular has only one compiler, and they Just use different names for different phases of compiler use. If the compiler is downloaded to the browser and used at runtime, it is called a JIT compiler. If it is used at compile time and does not need to be downloaded to the browser, it is called an AOT compiler. Using AOT is officially recommended by Angular, and the documentation explains why — rendering is faster and code packages are smaller.

If you use AOT, which means there is no compiler at runtime, the no-compile example above will still work and you can still use ComponentFactoryResolver, but dynamic compilation requires a compiler and will not run. However, if you have to use dynamic compilation, you have to package the compiler as a development dependency, and then download the code to the browser. This requires a bit of installation, but it’s nothing special.

import { JitCompilerFactory } from '@angular/compiler';

export function createJitCompiler() {
  return new JitCompilerFactory([{
    useDebug: false,
    useJit: true
  }]).createCompiler();
}

import { AppComponent }  from './app.component';

@NgModule({
  providers: [{provide: Compiler, useFactory: createJitCompiler}],
  ...
})
export class AppModule {
}
Copy the code

In the code above, we instantiate a compiler factory using the JitCompilerFactory class of @Angular/Compiler, and then register the compiler factory instance by identifying the compiler. The above is all the code that needs to be modified. It is very simple to modify and add these things.

Component destroyed

If you use dynamically loaded components, the last thing to note is that the dynamically loaded component needs to be destroyed when the parent component is destroyed:

ngOnDestroy() {
  if(this.cmpRef) {
    this.cmpRef.destroy(); }}Copy the code

The code above will remove the dynamically loaded component view from the view container and destroy it.

ngOnChanges

Angular performs change detection for all dynamically loaded components just as it does for statically loaded components, which means ngDoCheck is also called. Check Medium If you think ngDoCheck means your component is being checked — read this article). However, even if a dynamically loaded component declares the @INPUT Input binding, ngOnChanges for that dynamically loaded component will not be triggered if the parent component’s Input binding property changes. This is because the ngOnChanges function, which checks for input changes, is simply compiled and regenerated by the compiler at compile time. It is part of the component factory and was compiled from template information at compile time. Because the dynamically loaded component is not used in the template, this function is not compiled by the compiler.

Github

All the sample code for this article is hosted on Github.

ComponentFactoryResolver (” ComponentFactoryResolver “, “ComponentFactoryResolver”, “ComponentFactoryResolver”, “ComponentFactoryResolver”) If not in the same module, use the SystemJsNgModuleLoader module loader.