Render dynamic components the simple way in Angular – ngComponentOutlet

Reading Time: 4 minutes

Loading

Introduction

In Angular, there are a few ways to render templates and components dynamically. There is ngTemplateOutlet that can render different instances of ng-template conditionally. When we use components, we can apply ngComponentOutlet to render the dynamic components the simple way or ViewContainerRef the complex way.

In this blog post, I created a new component, PokemonTabComponent, that is consisted of hyperlinks and a ng-container element. When clicking a link, the component renders PokemonStatsComponent, PokemonAbilitiesComponent or both dynamically. NgComponentOutlet helps render the dynamic components the simple way and we will see its usage for the rest of the post.

let's go

The skeleton code of Pokemon Tab component

// pokemon-tab.component.ts

@Component({
  selector: 'app-pokemon-tab',
  standalone: true,
  imports: [
    PokemonStatsComponent, PokemonAbilitiesComponent
  ],
  template: `
    <div style="padding: 0.5rem;">
      <ul>
        <li><a href="#" #selection data-type="ALL">All</a></li>
        <li><a href="#" #selection data-type="STATISTICS">Stats</a></li>
        <li><a href="#" #selection data-type="ABILITIES">Abilities</a></li>
      </ul>
    </div>
    <app-pokemon-stats [pokemon]="pokemon"></app-pokemon-stats>
    <app-pokemon-abilities [pokemon]="pokemon"></app-pokemon-abilities>
  `,
  styles: [`...omitted due to brevity...`],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonTabComponent {
  @Input()
  pokemon: FlattenPokemon;
}

In PokemonTabComponent standalone component, nothing happened when clicking the links. However, the behaviour would change when I added new codes and and ngComponentOutlet in the inline template to render the dynamic components.

Compose RxJS code to map mouse click to components

// pokemon-tab.enum.ts

export enum POKEMON_TAB {
    ALL = 'all',
    STATISTICS = 'statistics',
    ABILITIES = 'abilities'
}

First, I defined enum to represent different mouse clicks.

// pokemon-tab.component.ts

componentMap = {
    [POKEMON_TAB.STATISTICS]: [PokemonStatsComponent],
    [POKEMON_TAB.ABILITIES]: [PokemonAbilitiesComponent],
    [POKEMON_TAB.ALL]: [PokemonStatsComponent, PokemonAbilitiesComponent],
}

Then, I defined an object map to map the enum members to the component lists.

@ViewChildren('selection', { read: ElementRef })
selections: QueryList<ElementRef<HTMLLinkElement>>;

Next, I used the ViewChildren() decorator to query the hyperlinks and the building blocks are in place to construct RxJS code in ngAfterViewInit.

// pokemon-tab.component.ts

export class PokemonTabComponent implements AfterViewInit, OnChanges {
  ...
} 
// pokemon-tab.component.ts

components$!: Observable<DynamicComponentArray>;

ngAfterViewInit(): void {
    const clicked$ = this.selections.map(({ nativeElement }) => fromEvent(nativeElement, 'click')
        .pipe(
          map(() => POKEMON_TAB[(nativeElement.dataset['type'] || 'ALL') as keyof typeof POKEMON_TAB]),
          map((value) => this.componentMap[value]),
        )
    );

    // merge observables to emit enum value and look up Component types
    this.components$ = merge(...clicked$)
      .pipe(startWith(this.componentMap[POKEMON_TAB.ALL]));
}
const clicked$ = this.selections.map(({ nativeElement }) => fromEvent(nativeElement, 'click')
   .pipe(
       map(() => POKEMON_TAB[(nativeElement.dataset['type'] || 'ALL') as keyof typeof POKEMON_TAB]),
       map((value) => this.componentMap[value]),
    )
 );
  • map(() => POKEMON_TAB[(nativeElement.dataset[‘type’] || ‘ALL’) as keyof typeof POKEMON_TAB]) – convert the value of type data attribute to POKEMON_TAB enum member
  • map((value) => this.componentMap[value]) – use POKEMON_TAB enum member to look up component list to render dynamically
  • Assign the component list to clicked$ Observable
this.components$ = merge(...clicked$)
    .pipe(startWith(this.componentMap[POKEMON_TAB.ALL]));
  • merge(…clicked$) – merge the Observables to emit the component list
  • startWith(this.componentMap[POKEMON_TAB.ALL]) – both PokemonStatsComponent and PokemonAbilitiesComponent are rendered initially

Apply ngComponentOutlet to PokemonTabComponent

ngComponentOutlet directive has 3 syntaxes and I will use the syntax that expects component and injector. It is because I require to inject Pokemon object to PokemonStatsComponent and PokemonAbilitiesComponent respectively

<ng-container *ngComponentOutlet="componentTypeExpression;
                                  injector: injectorExpression;
                                  content: contentNodesExpression;">
</ng-container>

In the inline template, I replaced <app-pokemon-stats> and <app-pokemon-abilities> with <ng-container> as the host of the dynamic components.

<ng-container *ngFor="let component of components$ | async">
     <ng-container *ngComponentOutlet="component; injector: myInjector"></ng-container>
</ng-container>

In the imports array, import NgFor, AsyncPipe and NgComponentOutlet.

imports: [PokemonStatsComponent, PokemonAbilitiesComponent, NgFor, AsyncPipe, NgComponentOutlet],

In the inline template, myInjector has not declared and I will complete the implementation in ngAfterViewInit and ngOnChange.

Let’s define a new injection token for the Pokemon object

// pokemon.constant.ts

import { InjectionToken } from "@angular/core";
import { FlattenPokemon } from "../interfaces/pokemon.interface";

export const POKEMON_TOKEN = new InjectionToken<FlattenPokemon>('pokemon_token');

createPokemonInjectorFn is a high-order function that returns a function to create an injector to inject an arbitrary Pokemon object.

// pokemon.injector.ts

export const createPokemonInjectorFn = () => {
  const injector = inject(Injector);

  return (pokemon: FlattenPokemon) =>
    Injector.create({
      providers: [{ provide: POKEMON_TOKEN, useValue:pokemon }],
      parent: injector
    });
}

In PokemonTabComponent, I imported POKEMON_TOKEN and createPokemonInjectorFn to assign new injector to myInjector in ngAfterViewInit and ngOnChanges.

// pokemon-tab.component.ts

myInjector!: Injector;
createPokemonInjector = createPokemonInjectorFn();
markForCheck = inject(ChangeDetectorRef).markForCheck;

ngAfterViewInit(): void {
    this.myInjector = this.createPokemonInjector(this.pokemon);
    this.markForCheck();
    ...
}

ngOnChanges(changes: SimpleChanges): void {
    this.myInjector = this.createPokemonInjector(changes['pokemon'].currentValue);
}

At this point, the application would not work because PokemonStatsComponent and PokemonAbilitiesComponent had not updated to inject the Pokemon object. This would be the final step to have a working example.

Inject Pokemon to PokemonStatsComponent and PokemonAbilitiesComponent

// pokemon-stats.component.ts

export class PokemonStatsComponent {
  pokemon = inject(POKEMON_TOKEN);
}
// pokemon-abilities.component.ts

export class PokemonAbilitiesComponent {
  pokemon = inject(POKEMON_TOKEN);
}

The following Stackblitz repo shows the final results:

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.

Resources:

  1. Github Repo: https://github.com/railsstudent/ng-pokemon/tree/main/projects/pokemon-demo-9
  2. Stackblitz: https://stackblitz.com/edit/angular-gxreww?file=src/pokemon/pokemon-tab/pokemon-tab.component.ts
  3. Youtube: https://www.youtube.com/watch?v=jXFsU81C9jI
  4. PokeAPI: https://pokeapi.co/