Angular Dynamic Component Lazy Loaded

Angular Dynamic Component Lazy Loaded

Angular Dynamic Component Lazy Loaded


Recently I was faced with the challenge of rendering whatever component inside of a page body based on a predefined configuration.

Angular supports the mechanism of instantiating a component inside a view container using the ViewContainerRef. We just need to call the method createComponent:


@ViewChild('componentContainer', { read: ViewContainerRef, static: true })
componentContainer: ViewContainerRef | undefined;
    
const viewContainerRef = this.adHost.viewContainerRef;
viewContainerRef.clear();

const componentRef = viewContainerRef.createComponent(SomeComponentType);
componentRef.instance.data = someDataToPass;


You can find more info on: https://angular.io/guide/dynamic-component-loader

Now imagine that you could get a component type by config, lazy-load it and render it. That would be pretty awesome right?


On angular v14 with the release of standalone components that is now easier (before we would need to attach the component to a module to do so).


This solution is also quite flexible and performant because we can render whatever component we want and we are just loading the component when we actually need it. This improves the app initial load because we are excluding all this components from the initial chunk files.


To demonstrate this let’s implement a simple app to display vehicles that we can toggle between, in this case we will have car and motorbike. This two vehicles will be displayed using two distinct components that we will lazy-load and display based on the user selection. So let’s see how:


First let’s create two components that we will lazy-load and display based on the user selection / config:

import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'motorbike',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './motorbike.component.html',
  styleUrls: ['./motorbike.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class MotorbikeComponent {
  @Input() title = '';
}
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'car',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './car.component.html',
  styleUrls: ['./car.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CarComponent {
  @Input() title = '';
}

Next let’s implement a service responsible to return the component type based on the key we provide:

import { Injectable, Type } from '@angular/core';
import { Observable, from } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class ComponentResolverService {
  getComponentType(component: string): Observable<Type<unknown>> {
    switch (component) {
      case 'car':
        return from(
          (async () => {
            const { CarComponent } = await import(
              '../components/car/car.component'
            );
            return CarComponent;
          })()
        );
      case 'motorbike':
        return from(
          (async () => {
            const { MotorbikeComponent } = await import(
              '../components/motorbike/motorbike.component'
            );
            return MotorbikeComponent;
          })()
        );
      default:
        throw new Error(
          `ComponentResolverService error. Cannot resolve component of type: ${component}`
        );
    }
  }
}

On app component let’s use this service to lazy load the component and render it:

import { Component, Type, ViewChild, ViewContainerRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';

import { BehaviorSubject, Observable, switchMap, tap } from 'rxjs';

import { ComponentResolverService } from './services/component-resolver.service';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, RouterOutlet],
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  @ViewChild('componentContainer', { read: ViewContainerRef, static: true })
  componentContainer: ViewContainerRef | undefined;

  currentVehicle$ = new BehaviorSubject<'car' | 'motorbike'>('car');
  component$ = this.getComponent();

  constructor(private componentResolverService: ComponentResolverService) {}

  onVehicleToogle(): void {
    const nextVehicle =
      this.currentVehicle$.getValue() === 'car' ? 'motorbike' : 'car';
    this.currentVehicle$.next(nextVehicle);
  }

  getComponent(): Observable<Type<unknown>> {
    return this.currentVehicle$.pipe(
      switchMap((currentVehicle) => {
        return this.componentResolverService.getComponentType(currentVehicle);
      }),
      tap((componentType) => {
        const viewContainerRef = this.componentContainer;
        viewContainerRef?.clear();

        const componentRef =
          viewContainerRef?.createComponent<unknown>(componentType);
        componentRef?.setInput('title', this.currentVehicle$.getValue());
      })
    );
  }
}

App component template:

<header><h1 class="title">Vehicles</h1></header>
<article>
  <header class="bodyHeader">
    <button
      *ngIf="component$ | async"
      type="button"
      class="button"
      (click)="onVehicleToogle()"
    >
      Toogle Vehicle
    </button>
  </header>
  <section><ng-container #componentContainer></ng-container></section>
</article>
<footer></footer>

This might look a bit overwhelming so lets describe a bit more how things work. On .html file the app component uses a ng-container that will be used as a placeholder for the components we want to render. We access and manipulate this container using ViewChild property decorator on .ts file.


Share Article:
  • Facebook
  • Instagram
  • LinkedIn
  • Twitter
  • Recent Posts