Pass inputs to ngComponentOutlet in Angular

railsstudent

Connie Leung

Posted on August 16, 2023

Pass inputs to ngComponentOutlet in Angular

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 eliminate boilerplate codes to achieve the same results.

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);
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Define an injection token (POKEMON_TOKEN) to inject a Pokemon object
  2. Create a function (createPokemonInjectorFn) to instantiate an injector that provides the value of POKEMON_TOKEN in providers array
  3. In PokemonTabComponent, invoke createPokemonInjectorFn and assign the injector to myInjector
  4. In inline template of PokemonTabComponent, assign myInjector to the injector input of ngComponentOutlet
  5. 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 1.16.2, 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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
// pokemon-abilities.component.ts

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

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>
Enter fullscreen mode Exit fullscreen mode

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:

💖 💪 🙅 🚩
railsstudent
Connie Leung

Posted on August 16, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related