As I’m working on my current project, I have come across the need to load custom components based on a value in a select. These components include inputs that validate data in several different formats. My go to for example code nearly always being Angular’s own site, I read through their example. Finding myself a bit confused, and unable to pull their example down into its simplest elements to borrow for my own use case, I continued my search. Unfortunately after quite some time searching, reading articles, and attempting to reproduce and then refactor examples down to their simplest terms, I came up empty handed. Though, not all was lost. With all of the information I have gleaned from the various articles read, I began to construct my own example, which I now share with you in hopes that it might help to understand this not-nearly-so-complicated concept.
The setup
As is with most build in Angular, we need to set up our module.ts file. I have added the MatSelectModule for our dropdown, our two injected components, InjectedComponent and AnotherInjectedComponent, as well as the BrowserAnimationsModule (a mainstay of Material imports). Because we’re using our custom components dynamically, we need to add the entryComponents property, and plop them in to it.
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { MatSelectModule } from '@angular/material/select'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { TestDirective } from './test.directive'; import { InjectedComponent } from './injected/injected.component'; import { AnotherInjectedComponent } from './another-injected/another-injected.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @NgModule({ declarations: [ AppComponent, TestDirective, InjectedComponent, AnotherInjectedComponent ], imports: [ BrowserModule, AppRoutingModule, BrowserAnimationsModule, MatSelectModule ], providers: [], bootstrap: [AppComponent], entryComponents: [ InjectedComponent, AnotherInjectedComponent ] }) export class AppModule { }
Next up is building the ultimate in simple directives. Our test directive is there to give us a reference to our view container, which we will use to show our custom components. This directive is injected into our DOM using the ng-template element, and accessed in our host component with a @ViewChild. These will be discussed in a moment.
import { Directive, ViewContainerRef } from '@angular/core'; @Directive({ selector: '[appTest]' }) export class TestDirective { constructor(public viewContainerRef: ViewContainerRef) { } }
Next we’ll set up our main HTML file. Here we have a simple mat-select. It uses two way binding (though we really can use one way binding here) to share the current value of the select with our component. We also trigger the changeComponent() method, which we’ll see in a moment, via the selectionChange directive. Our dropdown is populated from a list of string values.
Of note is our ng-template element. Here we are injecting the directive via its selector. This is where our custom component will render in the ViewContainerRef gained through the appTest directive.
<mat-select [(value)]="selected" (selectionChange)="changeComponent()"> <mat-label>Shown Component</mat-label> <mat-option *ngFor="let item of list" [value]="item">{{ item }}</mat-option> </mat-select> <div> <ng-template appTest></ng-template> </div>
Next we get into the meat of the example. I’ll break this down piece by piece, and include the full code at the end. What I am not showing here are the two custom components. In this example they contain no real functionality, only different html files, to make it simple.
The first part of our code is standard Angular, importing our bits and pieces to make the entire solution work.
Our next section includes our code to grab the TestDirective, using @ViewChild. We make the view area, that portion the directive holds for us, available to our code with the someDirective variable, which we set as the same type as our custom directive. The other variables will be used along the way.
@ViewChild(TestDirective, { static: true }) someDirective: TestDirective; title = 'test-dyncomp'; ourComponent; selected; // this is what we display in our dropdown list = []; mainList = [ { name: 'Injected', comp: InjectedComponent }, { name: 'Another Injected', comp: AnotherInjectedComponent } ];
Two variables of note from above are our list and mainList. MainList contains the text that will show in our mat-select element, as well as holding the component that it refers to. We use a map on this list, which we can just as easily populate from a service as hard code it here, to get the names of the options that will appear in our mat-select element. This is done in the constructor. We also set up a ComponentFactoryResolver which is the key to our dynamic loading functionality.
constructor(private componentFactoryResolver: ComponentFactoryResolver) { this.list = this.mainList.map(item => item.name); }
In our ngOnInit method, we use the first item in our mainList as the default selection. If we didn’t do this, no harm, nothing would be shown. We then call our changeComponent() method, which will be discussed next, to show our first component.
ngOnInit() { this.selected = this.mainList[0].name; this.changeComponent(); }
Here’s where the magic happens to show our components dynamically, based on the current option in our mat-select element. First we use Array.filter to grab all of the information about the component we want to show, from the text value of our selected option. We could do this just as easily through an index, or some other lookup. This works because the values of our options are taken from this mainList initially.
const result = this.mainList.filter(item => item.name === this.selected);
We then grab the reference to the component from our result value, and store it in the class property ourComponent. With this reference, we can then use our component factory resolver method resolveComponentFactory to get the factory object to create the component of our choosing, which is stored in the ourComponent property.
this.ourComponent = result[0].comp; const componentFactory = this.componentFactoryResolver.resolveComponentFactory( this.ourComponent );
We then want to get the view container reference that we will inject our component into when we create it. This is the variable that we make available to the class through the @ViewChild call. In our case we calld that someDirective. And just to make sure we start fresh, we call clear() on our current view container reference to destroy everything in that view.
const viewContainerRef = this.someDirective.viewContainerRef; viewContainerRef.clear();
Finally we call the createComponent method on our view container reference, passing it the component factory object that holds the component we want to create and render.
viewContainerRef.createComponent(componentFactory);
Here is the entire method for changeComponent().
changeComponent() { const result = this.mainList.filter(item => item.name === this.selected); this.ourComponent = result[0].comp; const componentFactory = this.componentFactoryResolver.resolveComponentFactory( this.ourComponent ); const viewContainerRef = this.someDirective.viewContainerRef; viewContainerRef.clear(); viewContainerRef.createComponent(componentFactory); }
And our full component.
import { Component, ViewChild, ComponentFactoryResolver, OnInit } from '@angular/core'; import { InjectedComponent } from './injected/injected.component'; import { AnotherInjectedComponent } from './another-injected/another-injected.component'; import { TestDirective } from './test.directive'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit { @ViewChild(TestDirective, { static: true }) someDirective: TestDirective; title = 'test-dyncomp'; ourComponent; selected; list = []; mainList = [ { name: 'Injected', comp: InjectedComponent }, { name: 'Another Injected', comp: AnotherInjectedComponent } ]; constructor(private componentFactoryResolver: ComponentFactoryResolver) { this.list = this.mainList.map(item => item.name); } ngOnInit() { this.selected = this.mainList[0].name; this.changeComponent(); } changeComponent() { const result = this.mainList.filter(item => item.name === this.selected); this.ourComponent = result[0].comp; const componentFactory = this.componentFactoryResolver.resolveComponentFactory( this.ourComponent ); const viewContainerRef = this.someDirective.viewContainerRef; viewContainerRef.clear(); viewContainerRef.createComponent(componentFactory); } }
Now we can generate our target components, add them to the mainList property and from our select element change components on the fly! This is the simplest example I could write to do this magic, and have used the same in real world projects.
The full code for this example can be found on my github page here.