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.
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]));
}
- ({ nativeElement }) => fromEvent(nativeElement, 'click') - create Observable that emits value when button is clicked
- 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
injector = inject(Injector);
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.