Egor Panok

Full Stack JavaScript Developer

Loading Angular Components Dynamically

January 16, 2019 - 5 min read

Loading Angular Components Dynamically

Application scenarios may require loading new components in runtime. This article shows how to use ComponentFactoryResolver to add components dynamically.

Use Case

Let’s suppose we have an Angular app and we decided to make a survey to identify what the favourite JavaScript framework of our users is. This is how our survey form may look like:

Survey Form

Survey Form

The survey is going to be shown based on meeting some requirements, for instance, we’ll show it only to users having registered more than 3 month or to users having some specific behaviour. Since it’s just a subset of our audience, it sounds reasonable to load this component dynamically.

Prerequisites

First of all, let’s define basic entities for the survey: Framework enum and ISurvey interface.

// framework.enum.ts

export enum Framework {
  ANGULAR = 'Angular',
  REACT = 'React',
  VUE = 'Vue',
  EMBER = 'Ember',
  BACKBONE = 'Backbone',
  EXTJS = 'ExtJS',
  OTHER = 'Other'
}
// survey.interface.ts

import { Framework } from "./framework/framework.enum"

export interface ISurvey {
    experienceInJSInYears: number;
    experienceInDevInYears: number;
    age: number;
    favoriteFramework: Framework;
}

Also we need a SurveyService to collect survey data:

// survey.service.ts

import { Injectable } from "@angular/core"
import { ISurvey } from "../survey.interface"

@Injectable({
    providedIn: "root"
})
export class SurveyService {
    constructor() {}

    add(survey: ISurvey) {
        // Just emulating an http call...
        console.log("Adding a survey...")
        console.log(survey)
    }
}

SurveyComponent

Let’s build it with Bulma and ReactiveForms:

// survey.component.ts

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Framework } from '../framework/framework.enum';
import { SurveyService } from '../service/survey.service';

@Component({
  selector: 'app-survey',
  templateUrl: './survey.component.html',
  styleUrls: ['./survey.component.scss']
})
export class SurveyComponent implements OnInit {

  surveyForm: FormGroup;
  frameworks: Framework[] = [
    Framework.ANGULAR,
    Framework.REACT,
    Framework.VUE,
    Framework.BACKBONE,
    Framework.EMBER,
    Framework.EXTJS,
    Framework.OTHER
  ];

  constructor(
    private formBuilder: FormBuilder,
    private surveySurvice: SurveyService
  ) { }

  ngOnInit() {
    this.surveyForm = this.formBuilder.group({
      experienceInJSInYears: [0, [Validators.required]],
      experienceInDevInYears: [0, [Validators.required]],
      age: [0, [Validators.required]],
      favoriteFramework: [Framework.ANGULAR, [Validators.required]]
    });
  }

  onSubmit() {
    const survey = {
      experienceInJSInYears: this.surveyForm.controls['experienceInJSInYears'].value,
      experienceInDevInYears: this.surveyForm.controls['experienceInDevInYears'].value,
      age: this.surveyForm.controls['age'].value,
      favoriteFramework: this.surveyForm.controls['favoriteFramework'].value
    };

    this.surveySurvice.add(survey);
  }

}
<!-- survey.component.html -->

<div class="card">
    <header class="card-header">
        <p class="card-header-title">
            Survey
        </p>
    </header>
    <div class="card-content">
        <div class="content">
            <form
                [formGroup]="surveyForm"
                (ngSubmit)="onSubmit()"
                class="survey-form"
            >
                <div class="field">
                    <label class="label"
                        >Experience In JavaScript (years)</label
                    >
                    <div class="control">
                        <input
                            class="input"
                            formControlName="experienceInJSInYears"
                            type="number"
                            placeholder="Experience in JavaScript (years)"
                        />
                    </div>
                </div>

                <div class="field">
                    <label class="label"
                        >Experience In Development (years)</label
                    >
                    <div class="control">
                        <input
                            class="input"
                            formControlName="experienceInDevInYears"
                            type="number"
                            placeholder="Experience in Development (years)"
                        />
                    </div>
                </div>

                <div class="field">
                    <label class="label">Age (years)</label>
                    <div class="control">
                        <input
                            class="input"
                            formControlName="age"
                            type="number"
                            placeholder="Age (years)"
                        />
                    </div>
                </div>

                <div class="field">
                    <label class="label">Favorite Framework</label>
                    <div class="control">
                        <div class="select">
                            <select formControlName="favoriteFramework">
                                <option *ngFor="let framework of frameworks"
                                    >{{framework}}</option
                                >
                            </select>
                        </div>
                    </div>
                </div>

                <div class="control">
                    <button class="button is-link" type="submit">Submit</button>
                </div>
            </form>
        </div>
    </div>
</div>

SurveyModule

Now we are ready to bind it all up into one module:

// survey.module.ts

import { NgModule } from "@angular/core"
import { CommonModule } from "@angular/common"
import { ReactiveFormsModule } from "@angular/forms"
import { SurveyComponent } from "./components/survey.component"
import { SurveyService } from "./service/survey.service"

@NgModule({
    declarations: [SurveyComponent],
    imports: [CommonModule, ReactiveFormsModule],
    exports: [SurveyComponent],
    providers: [SurveyService],
    entryComponents: [SurveyComponent]
})
export class SurveyModule {}

Please, notice that we added the SurveyComponent into the entryComponents section of the module - it’s critical to make the injection of the SurveyComponent possible.

Dynamic Loading of the SurveyComponent

So, we are ready to load the SurveyComponent dynamically. Let’s suppose we decided to do it in the AppComponent. Presuming that we’ve imported SurveyModule into the AppModule, to accomplish this we need to do the following:

  1. Specify the exact DOM element in our app where our component will created by setting a template ref (#):
// app.component.html

<div class="section">
    <div #entry></div>
</div>
  1. Store a reference to this DOM element by means of @ViewChild() directive, specifying the ‘entry’ query which should correspond the name of the template ref (#) from the previous step:
// app.component.ts

@ViewChild('entry', {read: ViewContainerRef}) entry: ViewContainerRef;
  1. Resolve the SurveyComponent’s factory by means of ComponentFactoryResolver and then create a component by using #entry ViewComponentReference and the factory. Just note that we do it in the ngAfterContentInit hook verifying preliminarily if the survey required.
// app.component.ts

ngAfterContentInit() {
    // Check requirements to show the survey
    if (this.isSurveyRequired()) {
      // Resolve a factory
      const surveyFormFactory = this.resolver.resolveComponentFactory(SurveyComponent);
      // Create a component
      const component = this.entry.createComponent(surveyFormFactory);
    }
}

Here is the full code of the AppComponent:

// app.component.ts

import { AfterContentInit, Component, ComponentFactoryResolver, ViewChild, ViewContainerRef } from '@angular/core';
import { SurveyComponent } from './survey/components/survey.component';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements AfterContentInit {

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

  constructor(
    private resolver: ComponentFactoryResolver
  ) {}

  isSurveyRequired() {
    // Just an emulation
    return Math.random() >= 0.5;
  }

  ngAfterContentInit() {
    // Check requirements to show the survey
    if (this.isSurveyRequired()) {
      // Resolve a factory
      const surveyFormFactory = this.resolver.resolveComponentFactory(SurveyComponent);
      // Create a component
      const component = this.entry.createComponent(surveyFormFactory);
    }
  }
}

Conclusion

Along with setting components in templates declaratively, we can load them in runtime. We might need it to implement context menus and toolbars, pop-up dialogs and so on. To implement it, firstly we need to obtain the component’s leveraging ComponentFactoryResolver and then we can create needed component at the specific place indicated by the template ref (#).

You’re more than welcomed to play around the code in this repo.

Happy coding!


Written by Egor Panok, full stack JavaScript Developer who loves building useful things. Follow him on Twitter

© 2020, Egor Panok, Full Stack JavaScript Developer