So I'm sure you've been through the pain of showing different components based on some rules or conditions. A fast and easy solution for that is, of course, rendering them conditional, with *ngIf directive, but this can easily become a royal pain if the logic is complex or we have triggers in different parts of the app. In this article, I'll show how can you dynamically add banners to your web application using Angular. We're going to start from scratch with an empty Angular 12 project containing 4 components.
ng new ng new dynamically-components
ng g c advertisement-banner
ng g c error-banner
ng g c success-banner
ng g c banners-container
We're going to use a service to trigger the load or the removal of the component.
import { Injectable, Type } from "@angular/core";
import { BehaviorSubject } from "rxjs";
@Injectable({
providedIn: "root",
})
export class BannersService {
private _bannersToAdd = new BehaviorSubject<any>(null);
bannersToAdd$ = this._bannersToAdd.asObservable();
private _bannersToRemove = new BehaviorSubject<any>(null);
bannersToRemove$ = this._bannersToRemove.asObservable();
constructor() {}
addBanner(component: Type<any>): void {
this._bannersToAdd.next(component);
}
removeBanner(component: Type<any>): void {
this._bannersToRemove.next(component);
}
}
So from where we want to trigger a component loading,in our case, app.component.ts we'll need to call addBanner() method passing the component we want to load.
import { AdvertisementBannerComponent } from './advertisement-banner/advertisement-banner.component';
import { BannersService } from './banner.service';
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
constructor(private bannersService: BannersService) {}
ngOnInit(): void {
this.bannersService.addBanner(AdvertisementBannerComponent);
}
}
Now, our container component will render the banners we want to show depending on some conditions. This component will need to be rendered at the top of each page, so we'll insert it in app.component.html
<app-banners-container></app-banners-container>
Here is were all the magic is happening. First we'll need to subscribe on bannersToAdd$ observable from our service, in this way we'll know when we need to render a new component.
export class BannersContainerComponent {
constructor(
public bannersService: BannersService,
) {}
ngAfterViewInit(): void {
this.bannersService.bannersToAdd$.subscribe((el) => {
this.loadComponent(el);
});
}
}
The HTML is simple, we're using ng-template element for various reasons:
- It doesn't render any additional output.
- Is a virtual Angular element used to render HTML templates and its content is displayed only when it's needed (conditional).
- It's not a true web element, so if we take a look in the element, we won't see a tag in HTML DOM.
- It is an internal implementation of Angular's structural directive, so we'll have access at two properties: TemplateRef and ViewContainerRef. A structural directive creates the view from ng-template and inserts that in the view container, so the structural directive access the content through TemplateRef and updates the ViewContainerRef.
ViewContainerRef is a container where views can be attached to a component, so basically it's just an element where we can insert our new components (banners).
<ng-template class="full-width full-height" #banners></ng-template>
At this point, we need a reference to the ViewContainerRef in order to call a method directly on it. By using @ViewChild, container variable will be filled with an instance of ViewContainerRef. Important to know is that the value of this injected member is not available at the component construction time. Angular will take care of this property itself, but only AFTER the view initialization is completed. So the earliest point when we can write initialization code that uses the reference injected by @ViewChild decorator is in AfterViewInit lifecycle hook.
@ViewChild('banners', { read: ViewContainerRef })
container!: ViewContainerRef;
ngOnInit(){
console.log(this.container.length); // won't work because container is undefined
}
ngAfterViewInit() {
console.log(this.container.length); // will work
}
Now, how do we load the component?
constructor(
public bannersService: BannersService,
private componentFactoryResolver: ComponentFactoryResolver
) {}
ngAfterViewInit(): void {
this.bannersService.bannersToAdd$.subscribe((el) => {
this.loadComponent(el);
});
}
private loadComponent(el: any): void {
const componentFactory =
this.componentFactoryResolver.resolveComponentFactory(el);
const x = this.container.createComponent(componentFactory);
x.changeDetectorRef.detectChanges();
this.components.push(x);
}
In Angular every component is created from a factory (this is generated by the compiler using the data you provide in the @Component decorator (selector, templateUrl, styleUrl,changeDetection, ecapsulation, etc). So if we want to create a component, we need to do the same thing. First we need to get a factory that can create our components, for this, we inject in our constructor ComponentFactoryResolver. Basically If we have access to a factory, we can create components instances from this and insert them into DOM using viewContainerRef. You can read more details about factories here .
We get a factory using the resolver injected in the constructor:
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(el);
Now that we have a factory, this can be used to create components. Calling createComponent() method will create and insert the component in our container.
const x = this.container.createComponent(componentFactory);
We'll need to keep a handle on the created components, so we can destroy them in ngOnDestroy and also have the posibility to remove one or more components at different triggers.
this.components.push(x);
Finally, we have to make sure that change detection happens once:
x.changeDetectorRef.detectChanges();
And destroy them in ngOnDestroy:
ngOnDestroy(): void {
for (let x = 0; x < this.components.length; ++x) {
this.components[x].destroy();
}
}
Now, for testing purposes, we can go and add banners from app.component.ts (Of course this can be easily replace with some logic that triggers the loading based on rules and conditions).
export class AppComponent implements OnInit {
constructor(private bannersService: BannersService) {}
ngOnInit(): void {
this.bannersService.addBanner(AdvertisementBannerComponent);
setTimeout(() => {
this.bannersService.addBanner(ErrorBannerComponent);
}, 300);
setTimeout(() => {
this.bannersService.addBanner(SuccessfulBannerComponent);
}, 1000);
}
}
Now, It may be a great idea to have the possibility to remove a component(In our case, to manually close the banner or after a timeout). Now it's the time we can actually use the array of the components we kept in components variable.
ngAfterViewInit(): void {
this.bannersService.bannersToRemove$.subscribe((el) => {
if(el){
this.removeComponent(el);
}
});
}
private removeComponent(el: any): void {
const component = this.components.find(
(c: { instance: any }) => c.instance instanceof el
);
const componentIndex = this.components.indexOf(component);
if (componentIndex !== -1) {
this.container.remove(this.container.indexOf(component.hostView as any));
this.components.splice(componentIndex, 1);
}
}
This method is pretty straight forward, first we need to find the component that needs to be removed from the DOM and its index:
const component = this.components.find(
(c: { instance: any }) => c.instance instanceof el
);
const componentIndex = this.components.indexOf(component);
Then we need to remove it from DOM using the reference we have :
this.container.remove(this.container.indexOf(component.hostView as any));
And of course, updating the array of the components we're rendering:
this.components.splice(componentIndex, 1);
To test this, we need to call removeBanner method from our service:
export class AppComponent implements OnInit {
constructor(private bannersService: BannersService) {}
ngOnInit(): void {
this.bannersService.addBanner(AdvertisementBannerComponent);
setTimeout(() => {
this.bannersService.addBanner(ErrorBannerComponent);
}, 300);
setTimeout(() => {
this.bannersService.removeBanner(AdvertisementBannerComponent);
}, 1000);
}
}
So, that's it! It's really useful and there are a lot of occasions when you can use this! You can get the complete code here: