Introduction
This post wants to illustrate how powerful RxJS is when building a reactive user interface in Angular. The application is consisted of a group of buttons that can increment and decrement Pokemon id. When button click occurs, the observable emits the new id and renders the images of the new Pokemon.
Bootstrap AppComponent
// main.ts
import 'zone.js/dist/zone';
import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { PokemonComponent } from './pokemon/pokemon/pokemon.component';
@Component({
selector: 'my-app',
standalone: true,
imports: [PokemonComponent],
template: `
<app-pokemon></app-pokemon>
`,
})
export class AppComponent {}
bootstrapApplication(AppComponent).catch((err) => console.log(err));
In main.ts
, I bootstrapped AppComponent
as the root element of the application. It is possible because AppComponent
is a standalone component and Component decorator defines standalone: true
option. In the imports array, I imported PokemonComponent
(that also a standalone component) and it is responsible to implement the reactive user interface with RxJS.
// pokemon-component.ts
import { AsyncPipe, NgIf } from '@angular/common';
import { Component, ElementRef, OnInit, ViewChild, ChangeDetectionStrategy } from '@angular/core';
import { fromEvent, map, merge, Observable, scan, shareReplay, startWith } from 'rxjs';
@Component({
selector: 'app-pokemon',
standalone: true,
imports: [AsyncPipe],
template: `
<h1>
Display the first 100 pokemon images
</h1>
<div>
<label>Pokemon Id:
<span>{{ btnPokemonId$ | async }}</span>
</label>
<div class="container" *ngIf="images$ | async as images">
<img [src]="images.frontUrl" />
<img [src]="images.backUrl" />
</div>
</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>
</div>
`,
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>;
btnPokemonId$!: Observable<number>;
images$!: Observable<{ frontUrl: string, backUrl: string }>;
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);
this.btnPokemonId$ = merge(btnMinusTwo$, btnMinusOne$, btnAddOne$, btnAddTwo$)
.pipe(
scan((acc, value) => {
const potentialValue = acc + value;
if (potentialValue >= 1 && potentialValue <= 100) {
return potentialValue;
} else if (potentialValue < 1) {
return 1;
}
return 100;
}, 1),
startWith(1),
shareReplay(1),
);
const pokemonBaseUrl = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon';
this.images$ = this.btnPokemonId$.pipe(
map((pokemonId: number) => ({
frontUrl: `${pokemonBaseUrl}/shiny/${pokemonId}.png`,
backUrl: `${pokemonBaseUrl}/back/shiny/${pokemonId}.png`
}))
);
}
createButtonClickObservable(ref: ElementRef<HTMLButtonElement>, value: number) {
return fromEvent(ref.nativeElement, 'click').pipe(map(() => value));
}
}
PokemonComponent
imports AsyncPipe
and NgIf
in the imports array because the inline template makes use of async
and ngIf
.
Implement reactive user interface with rxJS
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);
createButtonClickObservable(ref: ElementRef<HTMLButtonElement>, value: number) {
return fromEvent(ref.nativeElement, 'click').pipe(map(() => value));
}
}
createButtonClickObservable
creates an Observable that emits a number when button click occurs. When button text is +1 or +2, the Observables emit 1 or 2 respectively. When button text is -1 or -2, the Observables emit -1 or -2 respectively.
this.btnPokemonId$ = merge(btnMinusTwo$, btnMinusOne$, btnAddOne$, btnAddTwo$)
.pipe(
scan((acc, value) => {
const potentialValue = acc + value;
if (potentialValue >= 1 && potentialValue <= 100) {
return potentialValue;
} else if (potentialValue < 1) {
return 1;
}
return 100;
}, 1),
startWith(1),
shareReplay(1),
);
merge(btnMinusTwo$, btnMinusOne$, btnAddOne$, btnAddTwo$)
merges button observables to emit the delta that is either positive or negative
scan((acc, value) => {
const potentialValue = acc + value;
if (potentialValue >= 1 && potentialValue <= 100) {
return potentialValue;
} else if (potentialValue < 1) {
return 1;
}
return 100;
}, 1)
Use scan
operator to update the Pokemon id. When Pokemon id is less than 1, reset the id to 1. When Pokemon id is greater than 100, reset the id to 100.
Then, the application displays the first 100 Pokemons only
startWith(1)
Set the initial Pokemon id to 1 to display the first Pokemon
shareReplay(1)
Cache the Pokemon id
const pokemonBaseUrl = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon';
this.images$ = this.btnPokemonId$.pipe(
map((pokemonId: number) => ({
frontUrl: `${pokemonBaseUrl}/shiny/${pokemonId}.png`,
backUrl: `${pokemonBaseUrl}/back/shiny/${pokemonId}.png`
}))
);
When this.btnPokemonId$
emits a new Pokemon id, it is mapped to front and back image URLs and assigned to this.images$
Observable.
<div class="container" *ngIf="images$ | async as images">
<img [src]="images.frontUrl" />
<img [src]="images.backUrl" />
</div>
Inline template uses async pipe to resolve images$ Observable and the image tags update the source to display the new Pokemon.
This is it and I have built a reactive user interface with RxJS and Angular
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.