Introduction
I extended my Pokemon application to call an API to retrieve a Pokemon by id. The HTTP request returned an Observable that required ngIf and async pipe to resolve in order to render the results in inline template. In this blog post, I want to demonstrate how to convert HTTP response to Signal with toSignal
. toSignal
returns T | undefined
but we can provide an initial value to the function to get rid of the undefined type.
Old Pokemon Component with RxJS codes
// pokemon.component.ts
...omitted import statements for brevity...
const retrievePokemonFn = () => {
const httpClient = inject(HttpClient);
return (id: number) => httpClient.get<Pokemon>(`https://pokeapi.co/api/v2/pokemon/${id}`)
.pipe(
map((pokemon) => ({
id: pokemon.id,
name: pokemon.name,
height: pokemon.height,
weight: pokemon.weight,
back_shiny: pokemon.sprites.back_shiny,
front_shiny: pokemon.sprites.front_shiny,
}))
);
}
@Component({
selector: 'app-pokemon',
standalone: true,
imports: [AsyncPipe, FormsModule, NgIf, NgTemplateOutlet],
template: `
<h1>
Display the first 100 pokemon images
</h1>
<div>
<ng-container *ngIf="pokemon$ | async as pokemon">
<div class="pokemon-container">
<ng-container *ngTemplateOutlet="details; context: { $implicit: 'Id: ', value: pokemon.id }"></ng-container>
<ng-container *ngTemplateOutlet="details; context: { $implicit: 'Name: ', value: pokemon.name }"></ng-container>
<ng-container *ngTemplateOutlet="details; context: { $implicit: 'Height: ', value: pokemon.height }"></ng-container>
<ng-container *ngTemplateOutlet="details; context: { $implicit: 'Weight: ', value: pokemon.weight }"></ng-container>
</div>
<div class="container">
<img [src]="pokemon.front_shiny" />
<img [src]="pokemon.back_shiny" />
</div>
</ng-container>
</div>
<div class="container">
<button class="btn" #btnMinusTwo>-2</button>
<button class="btn" #btnMinusOne>-1</button>
<button class="btn" #btnAddOne>+1</button>
<button class="btn" #btnAddTwo>+2</button>
<form #f="ngForm" novalidate>
<input type="number" [(ngModel)]="searchId" [ngModelOptions]="{ updateOn: 'blur' }"
name="searchId" id="searchId" />
</form>
</div>
<ng-template #details let-name let-value="value">
<label><span style="font-weight: bold; color: #aaa">{{ name }}</span>
<span>{{ value }}</span>
</label>
</ng-template>
`,
styles: [`...omitted due to brevity...`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonComponent implements OnInit {
@ViewChild('btnMinusTwo', { static: true, read: ElementRef })
btnMinusTwo: ElementRef<HTMLButtonElement>;
@ViewChild('btnMinusOne', { static: true, read: ElementRef })
btnMinusOne: ElementRef<HTMLButtonElement>;
@ViewChild('btnAddOne', { static: true, read: ElementRef })
btnAddOne: ElementRef<HTMLButtonElement>;
@ViewChild('btnAddTwo', { static: true, read: ElementRef })
btnAddTwo: ElementRef<HTMLButtonElement>;
@ViewChild('f', { static: true, read: NgForm })
myForm: NgForm;
pokemon$!: Observable<FlattenPokemon>;
searchId = 1;
retrievePokemon = retrievePokemonFn();
ngOnInit() {
const btnMinusTwo$ = this.createButtonClickObservable(this.btnMinusTwo, -2);
const btnMinusOne$ = this.createButtonClickObservable(this.btnMinusOne, -1);
const btnAddOne$ = this.createButtonClickObservable(this.btnAddOne, 1);
const btnAddTwo$ = this.createButtonClickObservable(this.btnAddTwo, 2);
const inputId$ = this.myForm.form.valueChanges
.pipe(
debounceTime(300),
distinctUntilChanged((prev, curr) => prev.searchId === curr.searchId),
filter((form) => form.searchId >= 1 && form.searchId <= 100),
map((form) => form.searchId),
map((value) => ({
value,
action: POKEMON_ACTION.OVERWRITE,
}))
);
const btnPokemonId$ = merge(btnMinusTwo$, btnMinusOne$, btnAddOne$, btnAddTwo$, inputId$)
.pipe(
scan((acc, { value, action }) => {
... derive pokemon id....
}, 1),
startWith(1),
shareReplay(1),
);
this.pokemon$ = btnPokemonId$.pipe(switchMap((id) => this.retrievePokemon(id)));
}
createButtonClickObservable(ref: ElementRef<HTMLButtonElement>, value: number) {
return fromEvent(ref.nativeElement, 'click').pipe(
map(() => ({ value, action: POKEMON_ACTION.ADD }))
);
}
}
retrievePokemonFn()
returns a function that accepts an id to retrieve a Pokemon. When btnPokemonId$
Observable emits an id, the stream invokes this.retrievePokemon
and assigns the results to this.pokemon$
Observable. Then, this.pokemon$
is resolved in inline template to display image URLs and details. My goals are to refactor ngOnInit
and convert this.pokemon$
Observable to Angular signal. Then, inline template renders the signal value instead of the resolved Observable.
Store Reactive results into signal
First, I create a signal to store current Pokemon id
// pokemon-component.ts
pokemonId = signal(1);
Then, I modify inline template to add click
event to the button elements to update pokemonId
signal.
Before (RxJS)
<div class="container">
<button class="btn" #btnMinusTwo>-2</button>
<button class="btn" #btnMinusOne>-1</button>
<button class="btn" #btnAddOne>+1</button>
<button class="btn" #btnAddTwo>+2</button>
</div>
After (Signal)
<button class="btn" *ngFor="let delta of [-2, -1, 1, 2]" (click)="updatePokemonId(delta)">{{delta < 0 ? delta : '+' + delta }}</button>
In signal version, I remove template variables such that the component does not require ViewChild
to query HTMLButtonElement
readonly min = 1;
readonly max = 100;
updatePokemonId(delta: number) {
this.pokemonId.update((value) => {
const potentialId = value + delta;
return Math.min(this.max, Math.max(this.min, potentialId));
});
}
When button is clicked, updatePokemonId
updates pokemonId
to a value between 1 and 100.
In Imports array, I include NgFor
to use ngFor directive
imports: [..., NgFor],
Now, I declare searchId
signal to react to changes to number input field
// pokemon.component.ts
searchId = signal(1);
searchId
emits search value, streams to subsequent RxJS operators and subscribes to update pokemonId
signal.
Before (RxJS)
ngOnInit() {
const inputId$ = this.myForm.form.valueChanges
.pipe(
debounceTime(300),
distinctUntilChanged((prev, curr) => prev.searchId === curr.searchId),
filter((form) => form.searchId >= 1 && form.searchId <= 100),
map((form) => form.searchId),
map((value) => ({
value,
action: POKEMON_ACTION.OVERWRITE,
}))
);
}
After (Signal)
<input type="number" [ngModel]="searchId()"
(ngModelChange)="searchId.set($event)"
name="searchId" id="searchId" />
[(ngModel)]
is decomposed to [ngModel]
and (ngModelChange)
to get my solution to work. NgModel input is bounded to searchId()
and (ngModelChange)
updates the signal when input value changes.
constructor() {
toObservable(this.searchId)
.pipe(
debounceTime(300),
distinctUntilChanged(),
filter((value) => value >= this.min && value <= this.max),
map((value) => Math.floor(value)),
takeUntilDestroyed(),
).subscribe((value) => this.pokemonId.set(value));
}
Angular 16 introduces takeUntilDestroyed
that completes Observable; therefore, I donโt have to implement OnDestroy
interface to unsubscribe subscription manually.
Convert Observable to Angular Signal with toSignal
import { toSignal } from '@angular/core/rxjs-interop';
const initialValue: DisplayPokemon = {
id: 0,
name: '',
height: -1,
weight: -1,
back_shiny: '',
front_shiny: '',
};
pokemon = toSignal(
toObservable(this.pokemonId).pipe(switchMap((id) => this.retrievePokemon(id))), { initialValue });
When the codes update pokemonId
, toObservable(this.pokemonId)
emits the id to switchMap
operator to retrieve the specific Pokemon. The result of the stream is a Pokemon Observable that is passed to toSignal
to convert to an Angular signal.
pokemon
is a signal, I use it to compute rowData signal and pass that signal value to the context object of ngTemplateOutlet
.
rowData = 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 },
]
});
<div class="pokemon-container">
<ng-container
*ngTemplateOutlet="details; context: { $implicit: rowData() }"></ng-container>
</div>
I modify ngTemplate to iterate the rowData array to display the label and actual value.
<ng-template #details let-rowData>
<label *ngFor="let data of rowData">
<span style="font-weight: bold; color: #aaa">{{ data.text }}</span>
<span>{{ data.value }}</span>
</label>
</ng-template>
I remove NgIf
and AsyncPipe
from the imports array because the inline template does not need them anymore. The final array is consisted of FormsModule, NgTemplateOutlet and NgFor.
imports: [FormsModule, NgTemplateOutlet, NgFor],
New Pokemon Component using toSignal
// retrieve-pokemon.ts
// ...omitted import statements due to brevity...
export const retrievePokemonFn = () => {
const httpClient = inject(HttpClient);
return (id: number) => httpClient.get<Pokemon>(`https://pokeapi.co/api/v2/pokemon/${id}`)
.pipe(
map((pokemon) => pokemonTransformer(pokemon))
);
}
const pokemonTransformer = (pokemon: Pokemon): DisplayPokemon => ({
id: pokemon.id,
name: pokemon.name,
height: pokemon.height,
weight: pokemon.weight,
back_shiny: pokemon.sprites.back_shiny,
front_shiny: pokemon.sprites.front_shiny,
});
// pokemon.component.ts
...omitted import statements for brevity...
const initialValue: DisplayPokemon = {
id: 0,
name: '',
height: -1,
weight: -1,
back_shiny: '',
front_shiny: '',
};
@Component({
selector: 'app-pokemon',
standalone: true,
imports: [FormsModule, NgTemplateOutlet, NgFor],
template: `
<h2>
Display the first 100 pokemon images
</h2>
<div>
<ng-container>
<div class="pokemon-container">
<ng-container *ngTemplateOutlet="details; context: { $implicit: rowData() }"></ng-container>
</div>
<div class="container">
<img [src]="pokemon().front_shiny" />
<img [src]="pokemon().back_shiny" />
</div>
</ng-container>
</div>
<div class="container">
<button class="btn" *ngFor="let delta of [-2, -1, 1, 2]" (click)="updatePokemonId(delta)">
{{delta < 0 ? delta : '+' + delta }}
</button>
<input type="number" [ngModel]="searchId()" (ngModelChange)="searchId.set($event)"
name="searchId" id="searchId" />
</div>
<ng-template #details let-rowData>
<label *ngFor="let data of rowData">
<span style="font-weight: bold; color: #aaa">{{ data.text }}</span>
<span>{{ data.value }}</span>
</label>
</ng-template>
`,
styles: [`...omitted due to brevity...`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonComponent {
readonly min = 1;
readonly max = 100;
searchId = signal(1);
retrievePokemon = retrievePokemonFn();
pokemonId = signal(1);
pokemon = toSignal(
toObservable(this.pokemonId).pipe(switchMap((id) => this.retrievePokemon(id))), { initialValue });
rowData = 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(delta: number) {
this.pokemonId.update((value) => {
const potentialId = value + delta;
return Math.min(this.max, Math.max(this.min, potentialId));
});
}
constructor() {
toObservable(this.searchId)
.pipe(
debounceTime(300),
distinctUntilChanged(),
filter((value) => value >= this.min && value <= this.max),
map((value) => Math.floor(value)),
takeUntilDestroyed(),
).subscribe((value) => this.pokemonId.set(value));
}
}
The new version uses toSignal
function to convert Pokemon Observable to Pokemon signal with an initial value. After the conversion, I can use computed
to derive rowData signal and pass the signal value to the inline template to render. Thus, the inline template and logic is less verbose than the previous RxJS version.
This is it and I have enhanced the Pokemon application to make HTTP request and convert the HTTP response to signal. Then, signal functions are called within the inline template to render the 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.