Connie Leung
Posted on August 16, 2023
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);
}
Prior to 1.16.2, 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 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;
}
}
}
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:
Posted on August 16, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.