Pass inputs to ngComponentOutlet in Angular
Tags: angular, tutorial, frontend
Published: 2023-08-16 14:12:13

##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
```typescript
// 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 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
```typescript
// 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

```html
<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

```typescript
// pokemon-stats.component.ts

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

Modify inline template to obtain statistics from pokemon input

```typescript
<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>
```

```typescript
// pokemon-abilities.component.ts

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

Modify inline template to obtain abilities from pokemon signal

```typescript
<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:

{% embed https://stackblitz.com/edit/angular-wnq9xy?file=src%2Fpokemon%2Fpokemon-tab%2Fpokemon-tab.component.ts %}

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:

- Github Repo: https://github.com/railsstudent/ng-pokemon-signal/tree/main/projects/pokemon-signal-demo-10
- Stackblitz: https://stackblitz.com/edit/angular-wnq9xy?file=src%2Fpokemon%2Fpokemon-tab%2Fpokemon-tab.component.ts
- PokeAPI: https://pokeapi.co/