Pass inputs to ngComponentOutlet in Angular

Reading Time: 4 minutes

Loading

Introduction

In this blog post, I am going to describe how to use the new syntax to pass inputs to ngComponentOutlet to create dynamic components in Angular. Prior to Angular 1.16.2, Angular allows injector input in ngComponentOutlet to provide values. Developers define injection token and useValue to provide Object/primitive value in providers array. With the new way to pass input to ngComponentOutlet, developers can write less boilerplate codes to achieve the same results.

let's go

The Pokemon Tab component with Injector

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

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

// pokemon.injector.ts
import { inject, Injector } from "@angular/core";
import { POKEMON_TOKEN } from "../constants/pokemon.constant";
import { FlattenPokemon } from "../interfaces/pokemon.interface";

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

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

// pokemon-tab.component.ts

import { NgComponentOutlet, NgFor } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { PokemonAbilitiesComponent } from '../pokemon-abilities/pokemon-abilities.component';
import { PokemonStatsComponent } from '../pokemon-stats/pokemon-stats.component';

@Component({
  selector: 'app-pokemon-tab',
  standalone: true,
  imports: [
    PokemonStatsComponent, PokemonAbilitiesComponent, NgFor, AsyncPipe, NgComponentOutlet
  ],
  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>
    <ng-container *ngFor="let component of components$ | async">
      <ng-container *ngComponentOutlet="component; injector: myInjector"></ng-container>
    </ng-container>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonTabComponent implements AfterViewInit, OnChanges {
  @Input()
  pokemon: FlattenPokemon;

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

  // create a method to choose components based on enum values
  componentMap = {
    'statistics': [PokemonStatsComponent],
    'abilities': [PokemonAbilitiesComponent],
    'all': [PokemonStatsComponent, PokemonAbilitiesComponent],
  }

  components$!: Observable<DynamicComponentArray>;
  myInjector!: Injector;
  createPokemonInjector = createPokemonInjectorFn();
  markForCheck = inject(ChangeDetectorRef).markForCheck;

  ngAfterViewInit(): void {
    // create injector to inject pokemon input
    this.myInjector = this.createPokemonInjector(this.pokemon);
    // I need to add this line or the components do not render in first-load
    this.markForCheck();

    ...omitted RxJS codes because they are not important in this example...
  }

  ngOnChanges(changes: SimpleChanges): void {
    // create new injector when pokemon input updates 
    this.myInjector = this.createPokemonInjector(changes['pokemon'].currentValue);
  }
}

// pokemon-abilities.component.ts

import { NgFor, NgTemplateOutlet } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { POKEMON_TOKEN } from '../constants/pokemon.constant';

@Component({
  selector: 'app-pokemon-abilities',
  standalone: true,
  imports: [NgFor, NgTemplateOutlet],
  template: `
    <div style="padding: 0.5rem;">
      <p>Abilities</p>
      <div *ngFor="let ability of pokemon.abilities" class="abilities-container">
        <ng-container *ngTemplateOutlet="abilities; context: { $implicit: ability.name, isHidden: ability.is_hidden }"></ng-container>
      </div>
    </div>
    <ng-template #abilities let-name let-isHidden="isHidden">
      <label><span style="font-weight: bold; color: #aaa">Name: </span>
        <span>{{ name }}</span>
      </label>
      <label><span style="font-weight: bold; color: #aaa">Is hidden? </span>
        <span>{{ isHidden ? 'Yes' : 'No' }}</span>
      </label>
    </ng-template>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonAbilitiesComponent {
  pokemon = inject(POKEMON_TOKEN);
}

Prior to 16.2.0, this is the outline to pass pokemon input to ngComponentOutlet

  • Define an injection token (POKEMON_TOKEN) to inject a Pokemon object
  • Create a function (createPokemonInjectorFn) to instantiate an injector that provides the value of POKEMON_TOKEN in providers array
  • In PokemonTabComponent, invoke createPokemonInjectorFn and assign the injector to myInjector
  • In inline template of PokemonTabComponent, assign myInjector to the injector input of ngComponentOutlet
  • In PokemonAbilitiesComponent, inject POKEMON_TOKEN to find the Pokemon object from providers array. Then, iterate the abilities array to display individual ability in the inline template

In 16.2.0, Angular simplifies the step to pass inputs to ngComponentOutlet and I will show the new changes in the next section.

Pass inputs to ngComponentOutlet without injector

// pokemon-tab.component.ts

import { NgComponentOutlet, NgFor } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { PokemonAbilitiesComponent } from '../pokemon-abilities/pokemon-abilities.component';
import { PokemonStatsComponent } from '../pokemon-stats/pokemon-stats.component';
import { PokemonService } from '../services/pokemon.service';

@Component({
  selector: 'app-pokemon-tab',
  standalone: true,
  imports: [
    PokemonStatsComponent, PokemonAbilitiesComponent, 
    NgFor, 
    NgComponentOutlet
  ],
  template: `
    <div style="padding: 0.5rem;">
      <div>
        <div>
          <input id="all" name="type" type="radio" (click)="selectComponents('all')" checked />
          <label for="all">All</label>
        </div>
        <div>
          <input id="stats" name="type" type="radio" (click)="selectComponents('statistics')" />
          <label for="stats">Stats</label>
        </div>
        <div>
          <input id="ability" name="type" type="radio" (click)="selectComponents('abilities')" />
          <label for="ability">Abilities</label>
        </div>
      </div>
    </div>
    <ng-container *ngFor="let componentType of dynamicComponents">
      <ng-container *ngComponentOutlet="componentType;inputs: { pokemon: pokemon() }"></ng-container>
    </ng-container>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonTabComponent {
  pokemon = inject(PokemonService).pokemon;

  componentMap = {
    'statistics': [PokemonStatsComponent],
    'abilities': [PokemonAbilitiesComponent],
    'all': [PokemonStatsComponent, PokemonAbilitiesComponent],
  }

  dynamicComponents = this.componentMap['all'];
  
  selectComponents(type: string) {
    const components = this.componentMap[type];
    if (components !== this.dynamicComponents) {
      this.dynamicComponents = components;
    }
  }
}

First, I modified PokemonTabComponent to inject PokemonService and get the reference to pokemon signal. Next, I assigned the signal to pokemon member variable and deleted @Input() decorator

<ng-container *ngFor="let componentType of dynamicComponents">
      <ng-container *ngComponentOutlet="componentType; inputs: { pokemon: pokemon() }"></ng-container>
</ng-container>

In the inner ng-container, I replaced injector input with inputs and the value is a pokemon object, { pokemon: pokemon() }.

Then, I deleted boilerplate codes such as injection token, create injector function, ngAfterViewInit and ngOnChanges methods. The end results are less files and less code in PokemonTabComponent.

Read Pokemon Input in PokemonStatsComponent and PokemonAbilitiesComponent

// pokemon-stats.component.ts

export class PokemonStatsComponent {
  @Input({ required: true })
  pokemon!: DisplayPokemon;
}

Modify inline template to obtain statistics from pokemon input

<div style="padding: 0.5rem;">
      <p>Stats</p>
      <div *ngFor="let stat of pokemon.stats" class="stats-container">
        <label>
          <span style="font-weight: bold; color: #aaa">Name: </span>
          <span>{{ stat.name }}</span>
        </label>
        <label>
          <span style="font-weight: bold; color: #aaa">Base Stat: </span>
          <span>{{ stat.baseStat }}</span>
        </label>
        <label>
          <span style="font-weight: bold; color: #aaa">Effort: </span>
          <span>{{ stat.effort }}</span>
        </label>
      </div>
 </div>
// pokemon-abilities.component.ts

export class PokemonAbilitiesComponent {
  @Input({ required: true })
  pokemon!: DisplayPokemon;
}

Modify inline template to obtain abilities from pokemon signal

<div style="padding: 0.5rem;">
      <p>Abilities</p>
      <div *ngFor="let ability of pokemon.abilities" class="abilities-container">
        <label>
          <span style="font-weight: bold; color: #aaa">Name: </span>
          <span>{{ ability.name }}</span>
        </label>
        <label>
          <span style="font-weight: bold; color: #aaa">Is hidden? </span>
          <span>{{ ability.isHidden ? 'Yes' : 'No' }}</span>
        </label>
      </div></div>

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-signal/tree/main/projects/pokemon-signal-demo-10
  2. Stackblitz: https://stackblitz.com/edit/angular-wnq9xy?file=src%2Fpokemon%2Fpokemon-tab%2Fpokemon-tab.component.ts
  3. PokeAPI: https://pokeapi.co/