Dynamic Component Loader

Component templates are not always fixed. An application may need to load new components at runtime.

In this cookbook we show how to use ComponentFactoryResolver to add components dynamically.

Table of contents

Dynamic Component Loading

Where to load the component

Loading components

Dynamic Component Loading

The following example shows how to build a dynamic ad banner.

The hero agency is planning an ad campaign with several different ads cycling through the banner.

New ad components are added frequently by several different teams. This makes it impractical to use a template with a static component structure.

Instead we need a way to load a new component without a fixed reference to the component in the ad banner's template.

Angular comes with its own API for loading components dynamically. In the following sections you will learn how to use it.

Where to load the component

Before components can be added we have to define an anchor point to mark where components can be inserted dynamically.

The ad banner uses a helper directive called AdDirective to mark valid insertion points in the template.

src/app/ad.directive.ts

import { Directive, ViewContainerRef } from '@angular/core'; @Directive({ selector: '[ad-host]', }) export class AdDirective { constructor(public viewContainerRef: ViewContainerRef) { } }

AdDirective injects ViewContainerRef to gain access to the view container of the element that will become the host of the dynamically added component.

Loading components

The next step is to implement the ad banner. Most of the implementation is in AdBannerComponent.

We start by adding a template element with the AdDirective directive applied.

import { Component, Input, AfterViewInit, ViewChild, ComponentFactoryResolver, OnDestroy } from '@angular/core'; import { AdDirective } from './ad.directive'; import { AdItem } from './ad-item'; import { AdComponent } from './ad.component'; @Component({ selector: 'add-banner', template: ` <div class="ad-banner"> <h3>Advertisements</h3> <template ad-host></template> </div> ` }) export class AdBannerComponent implements AfterViewInit, OnDestroy { @Input() ads: AdItem[]; currentAddIndex: number = -1; @ViewChild(AdDirective) adHost: AdDirective; subscription: any; interval: any; constructor(private _componentFactoryResolver: ComponentFactoryResolver) { } ngAfterViewInit() { this.loadComponent(); this.getAds(); } ngOnDestroy() { clearInterval(this.interval); } loadComponent() { this.currentAddIndex = (this.currentAddIndex + 1) % this.ads.length; let adItem = this.ads[this.currentAddIndex]; let componentFactory = this._componentFactoryResolver.resolveComponentFactory(adItem.component); let viewContainerRef = this.adHost.viewContainerRef; viewContainerRef.clear(); let componentRef = viewContainerRef.createComponent(componentFactory); (<AdComponent>componentRef.instance).data = adItem.data; } getAds() { this.interval = setInterval(() => { this.loadComponent(); }, 3000); } } import { Injectable } from '@angular/core'; import { HeroJobAdComponent } from './hero-job-ad.component'; import { HeroProfileComponent } from './hero-profile.component'; import { AdItem } from './ad-item'; @Injectable() export class AdService { getAds() { return [ new AdItem(HeroProfileComponent, {name: 'Bombasto', bio: 'Brave as they come'}), new AdItem(HeroProfileComponent, {name: 'Dr IQ', bio: 'Smart as they come'}), new AdItem(HeroJobAdComponent, {headline: 'Hiring for several positions', body: 'Submit your resume today!'}), new AdItem(HeroJobAdComponent, {headline: 'Openings in all departments', body: 'Apply today'}), ]; } } import { Type } from '@angular/core'; export class AdItem { constructor(public component: Type<any>, public data: any) {} } import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { HeroJobAdComponent } from './hero-job-ad.component'; import { AdBannerComponent } from './ad-banner.component'; import { HeroProfileComponent } from './hero-profile.component'; import { AdDirective } from './ad.directive'; import { AdService } from './ad.service'; @NgModule({ imports: [ BrowserModule ], providers: [AdService], declarations: [ AppComponent, AdBannerComponent, HeroJobAdComponent, HeroProfileComponent, AdDirective ], entryComponents: [ HeroJobAdComponent, HeroProfileComponent ], bootstrap: [ AppComponent ] }) export class AppModule { constructor() {} } import { Component, OnInit } from '@angular/core'; import { AdService } from './ad.service'; import { AdItem } from './ad-item'; @Component({ selector: 'my-app', template: ` <div> <add-banner [ads]="ads"></add-banner> </div> ` }) export class AppComponent implements OnInit { ads: AdItem[]; constructor(private adService: AdService) {} ngOnInit() { this.ads = this.adService.getAds(); } }

The template element decorated with the ad-host directive marks where dynamically loaded components will be added.

Using a template element is recommended since it doesn't render any additional output.

src/app/ad-banner.component.ts (template)

template: ` <div class="ad-banner"> <h3>Advertisements</h3> <template ad-host></template> </div> `

Resolving Components

AdBanner takes an array of AdItem objects as input. AdItem objects specify the type of component to load and any data to bind to the component.

The ad components making up the ad campaign are returned from AdService.

Passing an array of components to AdBannerComponent allows for a dynamic list of ads without static elements in the template.

AdBannerComponent cycles through the array of AdItems and loads the corresponding components on an interval. Every 3 seconds a new component is loaded.

ComponentFactoryResolver is used to resolve a ComponentFactory for each specific component. The component factory is need to create an instance of the component.

ComponentFactories are generated by the Angular compiler.

Generally the compiler will generate a component factory for any component referenced in a template.

With dynamically loaded components there are no selector references in the templates since components are loaded at runtime. In order to ensure that the compiler will still generate a factory, dynamically loaded components have to be added to their NgModule's entryComponents array.

src/app/app.module.ts (entry components)

entryComponents: [ HeroJobAdComponent, HeroProfileComponent ],

Components are added to the template by calling createComponent on the ViewContainerRef reference.

createComponent returns a reference to the loaded component. The component reference can be used to pass input data or call methods to interact with the component.

In the Ad banner, all components implement a common AdComponent interface to standardize the api for passing data to the components.

Two sample components and the AdComponent interface are shown below:

import { Component, Input } from '@angular/core'; import { AdComponent } from './ad.component'; @Component({ template: ` <div class="job-ad"> <h4>{{data.headline}}</h4> {{data.body}} </div> ` }) export class HeroJobAdComponent implements AdComponent { @Input() data: any; } import { Component, Input } from '@angular/core'; import { AdComponent } from './ad.component'; @Component({ template: ` <div class="hero-profile"> <h3>Featured Hero Profile</h3> <h4>{{data.name}}</h4> <p>{{data.bio}}</p> <strong>Hire this hero today!</strong> </div> ` }) export class HeroProfileComponent implements AdComponent { @Input() data: any; } export interface AdComponent { data: any; }

The final ad banner looks like this:

Ads