Reactive user interface in Angular with RxJS
Tags: rxjs, angular, typescript, webdev
Published: 2023-03-21 04:16:29

##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

```typescript
// 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.  

```typescript
// 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
```typescript
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.  

```typescript
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),
   );
```

```typescript
merge(btnMinusTwo$, btnMinusOne$, btnAddOne$, btnAddTwo$)
```
merges button observables to emit the delta that is either positive or negative

```typescript
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

```typescript
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. 

```html
<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.

##Resources:

- Github Repo: https://github.com/railsstudent/ng-pokemon/tree/main/projects/pokemon-demo-1
- Stackblitz: https://stackblitz.com/edit/angular-utbmf3?file=src/main.ts
- PokeAPI: https://pokeapi.co/