Introduction
In this blog post, I want to describe how to perform UI customization using ViewContainerRef and Angular signals. Originally, I have a PokemonTabComponent that renders dynamic components using ViewContainerRef and the components are assigned a pokemon input. After rewrite, I refactor these components to inject Pokemon service in order to access the pokemon signal. After obtaining the signal in the component, the inline template can invoke signal function to render data subsequently.
Create a service using “Signals in a Service”
// pokemon.service.ts
// ...omitted import statements ...
const initialValue: DisplayPokemon = {
id: -1,
name: '',
height: -1,
weight: -1,
backShiny: '',
frontShiny: '',
abilities: [],
stats: [],
};
const pokemonTransformer = (pokemon: Pokemon): DisplayPokemon => {
const { id, name, height, weight, sprites, abilities: a, stats: statistics } = pokemon;
const abilities: Ability[] = a.map(({ ability, is_hidden }) => ({
name: ability.name,
isHidden: is_hidden
}));
const stats: Statistics[] = statistics.map(({ stat, effort, base_stat }) => ({
name: stat.name,
effort,
baseStat: base_stat,
}));
return {
id,
name,
height,
weight,
backShiny: sprites.back_shiny,
frontShiny: sprites.front_shiny,
abilities,
stats,
}
}
@Injectable({
providedIn: 'root'
})
export class PokemonService {
private readonly pokemonIdSub = new BehaviorSubject(1);
private readonly httpClient = inject(HttpClient);
private readonly pokemon$ = this.pokemonIdSub
.pipe(
switchMap((id) => this.httpClient.get<Pokemon>(`https://pokeapi.co/api/v2/pokemon/${id}`)),
map((pokemon) => pokemonTransformer(pokemon))
);
pokemon = toSignal(this.pokemon$, { initialValue });
personalData = computed(() => {
const { id, name, height, weight } = this.pokemon();
return [
{ text: 'Id: ', value: id },
{ text: 'Name: ', value: name },
{ text: 'Height: ', value: height },
{ text: 'Weight: ', value: weight },
];
});
updatePokemonId(input: PokemonDelta | number) {
if (typeof input === 'number') {
this.pokemonIdSub.next(input);
} else {
const potentialId = this.pokemonIdSub.getValue() + input.delta;
const newId = Math.min(input.max, Math.max(input.min, potentialId));
this.pokemonIdSub.next(newId);
}
}
}
First, I define PokemonService
to create pokemon
signal and use computed
keyword to compute personalData
signal based on it. When pokemonIdSub
BehaviorSubject emits an id in updatePokemonId
method, a HTTP request is made to retrieve the specified Pokemon from the API. However, HttpClient
returns an Observable; therefore, I convert it to signal using toSignal
with an initial value.
After PokemonService
defines pokemon
and personalData
signals, components can inject the service to access the signals and call the signal functions within their inline templates.
The skeleton code of Pokemon Tab component
// pokemon-tab.component.ts
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentRef, OnInit, ViewChild, ViewContainerRef, inject } from '@angular/core';
type SelectionType = 'ALL' | 'STATISTICS' | 'ABILITIES';
@Component({
selector: 'app-pokemon-tab',
standalone: true,
template: `
<div style="padding: 0.5rem;">
<div>
<div>
<input id="all" name="type" type="radio" (click)="renderComponentsBySelection('ALL')" checked />
<label for="all">All</label>
</div>
<div>
<input id="stats" name="type" type="radio" (click)="renderComponentsBySelection('STATISTICS')" />
<label for="stats">Stats</label>
</div>
<div>
<input id="ability" name="type" type="radio" (click)="renderComponentsBySelection('ABILITIES')" />
<label for="ability">Abilities</label>
</div>
</div>
</div>
<ng-container #vcr></ng-container>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonTabComponent {
@ViewChild('vcr', { static: true, read: ViewContainerRef })
vcr!: ViewContainerRef;
selection: SelectionType = 'ALL';
componentRefs: ComponentRef<any>[] = [];
cdr = inject(ChangeDetectorRef);
async renderComponentsBySelection(selection: SelectionType) {}
}
In PokemonTabComponent
standalone component, nothing happens when clicking the radio button. However, the behaviour will change when I add new logic to call ViewContainerRef
API to create components and trigger change detection. Therefore, I inject ChangeDetectorRef
and assign the reference to cdr
for change detection purpose.
Determine component types based on radio selection
// pokemon-tab.component.ts
private async getComponenTypes() {
const { PokemonStatsComponent } = await import('../pokemon-stats/pokemon-stats.component');
const { PokemonAbilitiesComponent } = await import('../pokemon-abilities/pokemon-abilities.component');
if (this.selection === 'ALL') {
return [PokemonStatsComponent, PokemonAbilitiesComponent];
} else if (this.selection === 'STATISTICS') {
return [PokemonStatsComponent];
}
return [PokemonAbilitiesComponent];
}
First, I define getComponenTypes
method that loads PokemonStatsComponent
and PokemonAbilitiesComponent
from component files. When selection is ALL, both components are returned. When selection is STATISTICS, the method returns PokemonStatsComponent
only. Otherwise, the method returns PokemonAbilitiesComponent
.
Leverage ViewContainerRef API for customization in PokemonTabComponent
async renderComponentsBySelection(selection: SelectionType) {
this.selection = selection;
await this.renderDynamicComponents();
}
When user clicks a radio button, inline template invokes renderComponentsBySelection
to set the selection and delegate to renderDynamicComponents
to call ViewContainerRef API.
destroyComponentRefs(): void {
// release component refs to avoid memory leak
for (const componentRef of this.componentRefs) {
if (componentRef) {
componentRef.destroy();
}
}
}
private async renderDynamicComponents() {
const componentTypes = await this.getComponenTypes();
// clear dynamic components shown in the container previously
this.vcr.clear();
this.destroyComponentRefs();
for (const componentType of componentTypes) {
const newComponentRef = this.vcr.createComponent(componentType);
// store component refs created
this.componentRefs.push(newComponentRef);
// run change detection in the component and child components
this.cdr.detectChanges();
}
}
When using ViewContainerRef API to create components, it is important to call clear
to remove all components from the container and destroy all instances of ComponentRef
to free memory. Next, ViewContainerRef
iterates componentTypes to create components, keep track of ComponentRef
and trigger change detection manually to display them.
If you ask where this.vcr
comes from, it comes from the template variable, vcr
, in the inline template.
<ng-container #vcr></ng-container>
@ViewChild('vcr', { static: true, read: ViewContainerRef })
vcr!: ViewContainerRef;
ViewChild
selects the template variable and this.vcr
has a valid ViewContainerRef
object after Angular instantiates PokemonTabComponent
component.
The application does not work at this point because dynamic components do not receive pokemon input during component creation. It is done on purpose. Next, I will rewrite other components to inject PokemonService
and display signal value in their inline template.
Inject PokemonService to PokemonStatsComponent and PokemonAbilitiesComponent
// pokemon-stats.component.ts
export class PokemonStatsComponent {
pokemon = inject(PokemonService).pokemon;
}
Modify inline template to obtain statistics from pokemon signal
<div style="padding: 0.5rem;">
<p>Stats</p>
<ng-container *ngTemplateOutlet="content; context: { $implicit: pokemon().stats }"></ng-container>
</div>
<ng-template #content let-stats>
<div *ngFor="let stat of 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>
</ng-template>
// pokemon-abilities.component.ts
export class PokemonAbilitiesComponent {
pokemon = inject(PokemonService).pokemon;
}
Modify inline template to obtain abilities from pokemon signal
<div style="padding: 0.5rem;">
<p>Abilities</p>
<ng-container *ngTemplateOutlet="content; context: { $implicit: pokemon().abilities }"></ng-container>
</div>
<ng-template #content let-abilities>
<div *ngFor="let ability of 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>
</ng-template>
Use computed signal in PokemonPersonalComponent
// pokemon-personal.component.ts
export class PokemonStatsComponent {
personalData = inject(PokemonService).personalData;
}
Modify inline template to display personal data in personalData signal
<div class="pokemon-container" style="padding: 0.5rem;">
<ng-container *ngTemplateOutlet="details; context: { $implicit: personalData() }"></ng-container>
</div>
<ng-template #details let-personalData>
<label *ngFor="let data of personalData">
<span style="font-weight: bold; color: #aaa">{{ data.text }}</span>
<span>{{ data.value }}</span>
</label>
</ng-template>
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.