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

13,255 reads

Create Angular components dynamically like a pro

If you’ve been programming with AngularJS you probably got used to generating HTML strings on the fly, running them through $compile service and linking to a data model (scope) to get two-way data binding.

reactions

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

In AngularJS a directive can modify DOM in any way possible and the framework has no clue what the modifications will be. But the problem with such approach is the same as with any dynamic environment — it’s hard to optimize for speed. Dynamic template evaluation is of course not the main culprit of AngularJS being viewed as a slow framework, but it certainly contributed to the reputation.

reactions

After studying Angular internals for quite some time it seems to be that the newer framework design was very much driven by the need for speed. You’ll find many comments like this in the sources:

reactions

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.

So Angular guys in the newer framework decided to provide less flexibility in return for a much greater speed. And introduced a JIT and AOT compilers and static templates. And factories. And factory resolver. And many other things that look hostile and unfamiliar to AngularJS community. But no worries. If you’ve come across these concepts before and is wondering what these are read on and achieve enlightenment.

reactions

Component factory and compiler

In Angular every component is created from a factory. And factories are generated by the compiler using the data you supply in the @Component decorator. If after reading many article on the web you’re still not sure what this decorator does read Implementing custom component decorator.

reactions

Under the hood Angular uses a concept of a View. The running framework is essentially a tree of views. Each view is composed of different types of nodes: element nodes, text nodes and so on. Each node is narrowly specialized in its purpose so that processing of such nodes takes as little time as possible. There are various providers associated with each node — like ViewContainerRef and TemplateRef. And each node knows how to respond to queries like ViewChildren and ContentChildren.

reactions

That’s a lot of information for each node. Now, to optimize for speed all this information has to be available when the node is constructed and cannot be changed later. This is what compilation process does — collects all the required information and encapsulates it in the form of a component factory.

reactions

Suppose you define a component and its template like this:

reactions

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

Using this data the compiler generates the following component factory:

reactions

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

It describes the structure of a component view and is used when instantiating the component. The first node is element definition and the second one is text definition. You can see that each node gets the information it needs when being instantiated through parameters list. It’s a job of a compiler to resolve all the required dependencies and provide them at the runtime.

reactions

If you have access to a factory you can easily create a component instance from it and insert into a DOM using viewContainerRef. I’ve written about it in Exploring Angular DOM manipulations. This is how it would look:

reactions

export class SampleComponent implements AfterViewInit { @ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef; ngAfterViewInit() { this.vc.createComponent(componentFactory); } }

So the question now is how to get access of a component factory and we will see shortly.

reactions

Angular modules and ComponentFactoryResolver

Although AngularJS also had modules it lacked true namespaces for directives. There was always a potential for conflicts and no way to encapsulate utility directives inside a particular module. Luckily, Angular learnt its lessons and now provides proper namespacing for declarative types: directives, components and pipes.

reactions

Just as in AngularJS every component in the newer framework is part of some module. Components don’t exist by themselves and if you want to use a component from a different module you have to import that module:

reactions

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

In turn, if a module wants to provide some components to be used by other module components it has to export these components. Here is how CommonModule does that:

reactions

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

So each component is bound to a particular module and you can’t declare the same component in different modules. If you do that you’ll get an error:

reactions

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

When Angular compiles an application, it takes components that are defined in entryComponents of a module or found in components templates and generates component factories for them. You can see those factories in the Sources tab:

reactions

In previous section we identified that if we had an access to a component factory we could then use it to create a component and insert into a view. Each module provides a convenient service for all its components to get a component factory. This service is ComponentFactoryResolver. So, if you define a BComponent on the module and want to get a hold of its factory you can use this service from a component belonging to this module:

reactions

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

This only works if both components are defined in the same module or if a module with a resolved component factory is imported.

reactions

Dynamic module loading and compilation

But what if your components are defined on the other module that you don’t want to load until the components in it are actually required? We can do that. This will be something similar to what router is doing with loadChildren configuration option.

reactions

There are two options how to load a module during runtime. The first one is to use the SystemJsNgModuleLoader provided by Angular. It is used by the router to load child routes if you’re using SystemJS as a loader. It has one public method load, which loads a module to a browser and compile the module and all components declared in it. This method takes a path to a file with a module and export name and returns ModuleFactory:

reactions

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

If you don’t specify export name, the loaded will use the default export name. The other thing to note is that SystemJsNgModuleLoader requires a DI setup with some injections so you should define it as a provider like that:

reactions

providers: [ { provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader } ]

You can of course specify any token for provide, but the router module uses NgModuleFactoryLoader so it’s probably a good thing to use the same approach.

reactions

So, the here is the full code to load a module and get a component factory:

reactions

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

But there is a one problem with using SystemJsNgModuleLoader. Under the hood it uses compileModuleAsync method of the compiler. This method creates factories only for components declared in entryComponents of a module or found in components templates. But what if you don’t want to declare your components as entry components? There is a solution — load the module yourself and use compileModuleAndAllComponentsAsync method. It generates factories for all components on the module and returns them as an instance of ModuleWithComponentFactories:

reactions

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

Here is the full code that shows how to load a module yourself and get access to all component factories:

reactions

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); }) }) }

Keep in mind that this approach makes use of a compiler which is not supported as a Public API. Here is what the docs say:

reactions

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.

Creating components on the fly

From the previous sections you found out how the dynamic components can be created in Angular. You know that this process requires an access to component factories which are placed on a module. Until now I’ve used modules that are defined before the runtime and can be loaded eagerly or lazily. But the good thing is that you don’t have to define modules beforehand and then load them. You can create a module and a component on the fly just like in AngularJS.

reactions

Let’s take the example I showed in the beginning and see how we can achieve the same in Angular. So, here is the example again:

reactions

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

The general flow to create and attach a dynamic content to the view is the following:

reactions

Define a component class and its properties and decorate the class Define a module class, add the component to module declarations and decorate the module class Compile module and all components to get hold of a component factory

The module is simply a class with a decorator applied to it. The same holds for a component. Since decorators are simple functions and available during runtime we can use them to decorate classes whenever we want. Here is the how to create and attach component dynamically on the fly:

reactions

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 = f.create(this._injector, [], null, this._m); cmpRef.instance.name = 'dynamic'; this.vc.insert(cmpRef.hostView); }) }

You may want to replace anonymous class with a named class in decorators for better debugging information.

reactions

Destroying components

The last thing is that if you’ve added components manually, don’t forget to destroy them when parent component is destroyed:

reactions

ngOnDestroy() { if(this.cmpRef) { this.cmpRef.destroy(); } }

Did you find the information in the article helpful?

Tags