Interested in latest HTTP response? Use RxJS SwitchMap operator

Reading Time: 3 minutes

 76 total views,  2 views today

Introduction

This post wants to illustrate how to use switchMap operator to cancel previous HTTP requests and retrieve the latest Pokemon by an id. It is an important technique to preserve scarce network resource on a device such as mobile phone that has limited data plan. It also improves performance because the application does not have to wait for previous requests to return and display until the last request comes back with the final result.

let's go

Bootstrap AppComponent

// main.ts

import 'zone.js/dist/zone';
import { provideHttpClient } from '@angular/common/http';
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 {
  name = 'Angular';
}

bootstrapApplication(AppComponent, {
  providers: [provideHttpClient()]
});

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 display information of a Pokemon. The second argument of bootstrapApplication is an array of providers and provideHttpClient makes HttpClient available for injection in components.

Retrieve Pokemon by HTTP

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,
      }))
    );
}

retrievePokemonFn is a high order function that returns a function that calls Pokemon endpoint to retrieve the data by id.

Use switchMap to make latest HTTP Request

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.

const 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),
  );

btnPokemonId$ is an Observable that emits the Pokemon id

this.pokemon$ = btnPokemonId$.pipe(switchMap((id) => this.retrievePokemon(id)));

When btnPokemonId$ emits a Pokemon id, I use switchMap to cancel previous HTTP requests and make a new request to retrieve the Pokemon. The result is then assigned to this.pokemon$ that is an Observable of Pokemon.

Render Pokemon in inline template

<ng-container *ngIf="pokemon$ | async as pokemon">
    <div class="pokemon-container">
       <label>Id:
          <span>{{ pokemon.id }}</span>
       </label>
       <label>Name:
          <span>{{ pokemon.name }}</span>
       </label>
       <label>Height:
          <span>{{ pokemon.height }}</span>
       </label>
       <label>Weight:
          <span>{{ pokemon.weight }}</span>
       </label>
    </div>
    <div class="container">
       <img [src]="pokemon.front_shiny" />
       <img [src]="pokemon.back_shiny" />
    </div>
 </ng-container>

Inline template uses async pipe to resolve pokemon$ Observable and displays the details of the Pokemon.

This is it and I have added HTTP capability to call Pokemon API. Moreover, I leverage switchMap to end earlier requests and trigger a new one whenever new pokemon id is emitted.

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:

  1. Github Repo: https://github.com/railsstudent/ng-pokemon/tree/main/projects/pokemon-demo-3
  2. Stackblitz: https://stackblitz.com/edit/angular-6qkuog?file=src/pokemon/pokemon/pokemon.component.ts
  3. Youtube: https://youtu.be/Y-AUQEYj7qg
  4. PokeAPI: https://pokeapi.co/

Reactive user interface in Angular with RxJS

Reading Time: 4 minutes

 47 total views

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.

let's go

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$)
  • Merge 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)
  • User 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.
  • 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.

Resources:

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

Fun with speech detection using RxJS and Angular standalone components

Reading Time: 4 minutes

 43 total views

Introduction

This is day 20 of Wes Bos’s JavaScript 30 challenge where I build a speech detection application using RxJS, Angular standalone components and Web Speech API. Angular not only can call it’s own APIs to render components but it can also interact with Web Speech API to guess what I spoke in English and outputted its confidence level. The API amazed me due to its high accuracy and confidence level as if a real human being was listening to me and understood my accent.

let's go

Create a new Angular project

ng generate application day20-speech-detection-standalone

Bootstrap AppComponent

First, I convert AppComponent into standalone component such that I can bootstrap AppComponent and inject providers in main.ts.

// app.component.ts

import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { SpeechDetectionComponent } from './speech-detection/speech-detection/speech-detection.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [SpeechDetectionComponent],
  template: '<app-speech-detection></app-speech-detection>',
  styles: [
    `
      :host {
        display: block;
      }
    `,
  ],
})
export class AppComponent {
  title = 'Day20 Speech Detection Standalone';

  constructor(titleService: Title) {
    titleService.setTitle(this.title);
  }
}

In Component decorator, I put standalone: true to convert AppComponent into a standalone component.

Instead of importing SpeechDetectionComponent in AppModule, I import SpeechDetectionComponent (that is also a standalone component) in the imports array because the inline template references it. It is because main.ts uses bootstrapApplication to render AppComponent as the root component of the application. When compiler sees <app-speech-detection> in the inline template and AppComponent does not import SpeechDetectionComponent, the application fails to compile.

// main.ts

import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent).catch((err) => console.error(err));

Next, I can delete AppModule that has no use now.

Declare speech detection component

I declare standalone component, SpeechDetectionComponent, to start the speech detection process. To verify the component is standalone, standalone: true is specified in the Component decorator.

src/app/
├── app.component.spec.ts
├── app.component.ts
└── speech-detection
    ├── helpers
    │   └── speech-detection.helper.ts
    ├── interfaces
    │   └── speech-recognition.interface.ts
    └── speech-detection
        ├── speech-detection.component.spec.ts
        └── speech-detection.component.ts

The application use this component to do everything and the tag is <app-speech-detection>.

// speech-detection.component.ts

import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import {
  createRecognition,
  createRecognitionSubscription,
  createWordListObservable,
} from '../helpers/speech-detection.helper';

@Component({
  selector: 'app-speech-detection',
  standalone: true,
  imports: [AsyncPipe, NgFor, NgIf],
  template: ` <div class="words" contenteditable>
    <ng-container *ngIf="wordList$ | async as wordList">
      <p *ngFor="let word of wordList">{{ word.transcript }}, confidence: {{ word.confidencePercentage }}%</p>
    </ng-container>
  </div>`,
  styles: [`...omitted due to bervity...`],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SpeechDetectionComponent implements OnInit, OnDestroy {
  recognition = createRecognition();
  subscription = createRecognitionSubscription(this.recognition);
  wordList$ = createWordListObservable(this.recognition);

  ngOnInit(): void {
    this.recognition.start();
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
}

SpeechDetectionComponent imports AsyncPipe, NgFor and NgIf in the imports array because the inline template makes use of ngIf, ngFor and async.

Add speech detection helper to define observable and subscription

I moved speech detection observable and subscription to helper file to maintain small component file and good project structure.

// speech-recognition.interface
export interface SpeechRecognitionInfo {
  transcript: string;
  confidence: number;
  isFinal: boolean;
}

export type Transcript = Omit<SpeechRecognitionInfo, 'isFinal' | 'confidence'> & { confidencePercentage: string };
// speech-detection.helper.ts

import { fromEvent, tap, map, filter, scan } from 'rxjs';
import {
  SpeechRecognitionInfo,
  Transcript,
} from '../interfaces/speech-recognition.interface';

declare var webkitSpeechRecognition: any;
declare var SpeechRecognition: any;

export const createRecognition = () => {
  const recognition = new webkitSpeechRecognition() || new SpeechRecognition();
  recognition.interimResults = true;
  recognition.lang = 'en-US';

  return recognition;
};

export const createRecognitionSubscription = (recognition: any) =>
  fromEvent(recognition, 'end')
    .pipe(tap(() => recognition.start()))
    .subscribe();

export const createWordListObservable = (recognition: any) => {
  const percent = 100;
  return fromEvent(recognition, 'result').pipe(
    map((e: any): SpeechRecognitionInfo => {
      const transcript = Array.from(e.results)
        .map((result: any) => result[0].transcript)
        .join('');
      const poopScript = transcript.replace(/poop|poo|shit|dump/gi, '💩');
      const firstResult = e.results[0];

      return {
        transcript: poopScript,
        confidence: firstResult[0].confidence,
        isFinal: firstResult.isFinal,
      };
    }),
    filter(({ isFinal }) => isFinal),
    scan(
      (acc: Transcript[], { transcript, confidence }) =>
        acc.concat({
          transcript,
          confidencePercentage: (confidence * percent).toFixed(2),
        }),
      [],
    ),
  );
};

Explain createRecognition

createRecognition function instantiates a SpeechRecognition service, sets the language to English and interimResult to true.

Explain createRecognitionSubscription

createRecognitionSubscription function restarts speech recognition and subscribes the Observable after the previous speech recognition ends.

  • fromEvent(recognition, ‘end’) – Emit value when end event of speech recognition service occurs
  • tap(() => recognition.start()) – restart speech recognition to recognize the next sentence
  • subscribe() – subscribe observable

Explain createWordListObservable

createWordListObservable function emits a list of phrases recognized by Web Speech API.

  • fromEvent(recognition, ‘result’) – Speech recognition emits a word or phrase
  • map() – replace foul languages with emoji and return the phrase, confidential level and the isFinal flag
  • filter(({ isFinal }) => isFinal) – emit value when the final result is ready
  • scan() – append the phrase and confidence level to the word list

Demystifies RxJS logic in SpeechDetectionComponent

SpeechDetectionComponent class is succinct now after refactoring Observable and Subscription codes to speech-detection.helper.ts. They are extracted to functions in helper file

recognition = createRecognition();
subscription = createRecognitionSubscription(this.recognition);
wordList$ = createWordListObservable(this.recognition);

The component declares this.recognition that is a SpeechRecognition service.

this.wordList$ is an Observable that renders a list of words or phrases.

<ng-container *ngIf="wordList$ | async as wordList">
    <p *ngFor="let word of wordList">
       {{ word.transcript }}, confidence: {{ word.confidencePercentage }}%
    </p>
</ng-container>

this.subscription is a subscription that unsubscribe in ngOnDestroy

ngOnDestroy(): void {
  this.subscription.unsubscribe();
}

This is it and I have built a speech recognition application with RxJS, Angular standalone components and Web Speech API.

Final Thoughts

In this post, I show how to use RxJS, Angular standalone components and web speech API to build speech detection. The application has the following characteristics after using Angular 15’s new features:

  • The application does not have NgModules and constructor boilerplate codes.
  • The standalone component is very clean because I moved as many RxJS codes to separate helper file as possible.

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:

  1. Github Repo: https://github.com/railsstudent/ng-rxjs-30/tree/main/projects/day20-speech-detection-standalone
  2. Live demo: https://railsstudent.github.io/ng-rxjs-30/day20-speech-detection-standalone/
  3. Wes Bos’s JavaScript 30 Challenge: https://github.com/wesbos/JavaScript30

Build a countdown timer using RxJS and Angular standalone components

Reading Time: 7 minutes

 49 total views

Introduction

This is day 29 of Wes Bos’s JavaScript 30 challenge where I build a countdown timer using RxJS, standalone components and standalone directive in Angular 15.

let's go

Create a new Angular project

ng generate application day19-countdown-timer-standalone

Bootstrap AppComponent

First, I convert AppComponent into standalone component such that I can bootstrap AppComponent and inject providers in main.ts.

// app.component.ts

import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { TimerComponent } from './timer/timer/timer.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    TimerComponent
  ],
  template: '<app-timer></app-timer>',
  styles: [`
    :host {
      display: block;
    }
  `]
})
export class AppComponent {
  title = 'Day 29 Standalone Countdown Timer';

  constructor(titleService: Title) {
    titleService.setTitle(this.title);
  }
}

In Component decorator, I put standalone: true to convert AppComponent into a standalone component.

Instead of importing TimerComponent in AppModule, I import TimerComponent (that is also a standalone component) in the imports array because the inline template references it.

// main.ts

import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent).catch(err => console.error(err));

Next, I can delete AppModule that is now useless.

Declare Timers components to build a countdown timer

I declare standalone components, TimerComponent, TimerControlsComponent and TimerPaneComponent to build a reactive countdown timer. To verify the components are standalone, standalone: true is specified in the Component decorator.

src/app
├── app.component.ts
└── timer
    ├── directive
    │   └── timer-button.directive.ts
    ├── helpers
    │   ├── timer-controls.helper.ts
    │   └── timer-pane.helper.ts
    ├── services
    │   └── timer.service.ts
    ├── timer
    │   └── timer.component.ts
    ├── timer-controls
    │   └── timer-controls.component.ts
    └── timer-pane
        └── timer-pane.component.ts

TimerComponent acts like a shell that encloses TimerControlsComponent and TimerPaneComponent. For your information, <app-timer> is the tag of TimerComponent.

// timer.component.ts

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { TimerControlsComponent } from '../timer-controls/timer-controls.component';
import { TimerPaneComponent } from '../timer-pane/timer-pane.component';

@Component({
  selector: 'app-timer',
  standalone: true,
  imports: [
    TimerControlsComponent, 
    TimerPaneComponent
  ],
  template: `
    <div class="timer">
      <app-timer-controls></app-timer-controls>
      <app-timer-pane></app-timer-pane>
    </div>
  `,
  styles: [`
    :host {
      display: block;
    }
    
    .timer {
        display: flex;
        min-height: 100vh;
        flex-direction: column;
    }
  `],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimerComponent {}

TimerComponent imports TimerControlsComponent and TimerPaneComponent in the imports array. TimerControlsComponent is included such that I can use <app-timer-controls> in the inline template. Similarly, TimerPaneComponent provides <app-timer-pane> selector that is also seen in the inline template.

TimerControlsComponent encapsulates buttons and input field to emit selected seconds whereas TimePaneComponent subscribes to the emitted value to initiate count down and render time left.

// timer-controls.component.ts

import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, OnDestroy, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Subscription, filter, fromEvent, map, tap } from 'rxjs';
import { TimerButtonDirective } from '../directive/timer-button.directive';
import { createButtonObservablesFn, timerInputSubscriptionFn } from '../helpers/timer-controls.helper';

@Component({
  selector: 'app-timer-controls',
  standalone: true,
  imports: [
    FormsModule,
    TimerButtonDirective,
  ],
  template: `
    <div class="timer__controls">
      <button class="timer__button" data-seconds="20" appTimerButton>20 Secs</button>
      <button class="timer__button" data-seconds="300" appTimerButton>Work 5</button>
      <button class="timer__button" data-seconds="900" appTimerButton>Quick 15</button>
      <button class="timer__button" data-seconds="1200" appTimerButton>Snack 20</button>
      <button class="timer__button" data-seconds="3600" appTimerButton>Lunch Break</button>
      <form name="customForm" id="custom" #myForm="ngForm">
        <input type="text" name="minutes" placeholder="Enter Minutes" [(ngModel)]="customMinutes">
      </form>
  </div>
  `,
  styles: [`...omitted due to brevity...`],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimerControlsComponent implements OnDestroy, AfterViewInit {
  @ViewChild('myForm', { static: true, read: ElementRef })
  myForm!: ElementRef<HTMLFormElement>;

  @ViewChildren(TimerButtonDirective)
  timers!: QueryList<ElementRef<HTMLButtonElement>>;

  customMinutes = '';
  subscriptions!: Subscription;

  createTimerObservables = createButtonObservablesFn();
  timerInputSubscription = timerInputSubscriptionFn();

  ngAfterViewInit(): void {
    const timers$ = this.createTimerObservables(this.timers.map(({ nativeElement }) => nativeElement));
    const myForm$ = fromEvent(this.myForm.nativeElement, 'submit')
      .pipe(
        filter(() => !!this.customMinutes),
        map(() => parseFloat(this.customMinutes)),
        map((customMinutes) => Math.floor(customMinutes * 60)),
        tap(() => this.myForm.nativeElement.reset())
      );
    this.subscriptions = this.timerInputSubscription([...timers$, myForm$]);
  }

  ngOnDestroy(): void {
    this.subscriptions.unsubscribe();
  }
}

TimerControlsComponent is consisted of a template form and a list of buttons; therefore, I import FormsModule and a standalone directive, TimerButtonDirective. I define the directive in order to query the button elements with ViewChildren decorator.

// timer-button.directive.ts

import { Directive, ElementRef } from '@angular/core';

@Directive({
  selector: '[appTimerButton]',
  standalone: true
})
export class TimerButtonDirective {
  nativeElement!: HTMLButtonElement;

  constructor(el: ElementRef<HTMLButtonElement>) {
    this.nativeElement = el.nativeElement;
  }
}

Next, I add appTimerButton to the button elements in the inline template

// timer-controls.component.ts 

<button class="timer__button" data-seconds="20" appTimerButton>20 Secs</button>
<button class="timer__button" data-seconds="300" appTimerButton>Work 5</button>
<button class="timer__button" data-seconds="900" appTimerButton>Quick 15</button>
<button class="timer__button" data-seconds="1200" appTimerButton>Snack 20</button>
<button class="timer__button" data-seconds="3600" appTimerButton>Lunch Break</button>
// timer-pane.components

import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { displayEndTimeFn, displayTimeLeftFn, nowToFn } from '../helpers/timer-pane.helper';

@Component({
  selector: 'app-timer-pane',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    <div class="display">
      <h1 class="display__time-left">{{ displayTimeLeft$ | async }}</h1>
      <p class="display__end-time">{{ displayEndTime$ | async }}</p>
    </div>
  `,
  styles: [`...omitted due to brevity...`],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimerPaneComponent {
  nowTo$ = nowToFn();
  displayEndTime$ = displayEndTimeFn(this.nowTo$);
  displayTimeLeft$ = displayTimeLeftFn(this.nowTo$); 
}

I declare all the components that require to build the countdown timer. Next section is going to describe TimerService that shares specified seconds between components.

Add timer service to share RxJS subjects and observables

TimerService stores Subjects and Observables that the components subscribe to receive specified seconds.

// timer.service.ts

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class TimerService {
  private readonly secondsSub = new Subject<number>(); 
  readonly seconds$ = this.secondsSub.asObservable();

  updateSeconds(seconds: number) {
    this.secondsSub.next(seconds);
  }
}

Demystifies RxJS logic in TimerControlsComponent

TimerControlsComponent encapsulates all RxJS observables and subscription in the following lines:

createTimerObservables = createButtonObservablesFn();
timerInputSubscription = timerInputSubscriptionFn();

Moreover, I wrote both functions in helper file to maintain small component file and good project structure.

// timer-controls.helper.ts

import { inject } from '@angular/core';
import { Observable, fromEvent, map, merge } from 'rxjs';
import { TimerService } from '../services/timer.service';
  
export const createButtonObservablesFn = () => {
    return (timers: HTMLButtonElement[]) => {
        return timers.map((nativeElement) => { 
            const totalSeconds = +(nativeElement.dataset['seconds'] || '0');
            return fromEvent(nativeElement, 'click').pipe(map(() => totalSeconds))
        });
    }
}

export const timerInputSubscriptionFn = () => {
    const timerService = inject(TimerService);
    return (observables: Observable<number>[]) => merge(...observables).subscribe((seconds) => {
      timerService.updateSeconds(seconds);
      console.log(`${seconds} seconds`);
    });
}

Explain createButtonObservableFn

createButtonObservablesFn is a high order function that returns a function. The function is ultimately executed in ngAfterViewInit to construct timer button observables.

  • const totalSeconds = +(nativeElement.dataset[‘seconds’] || ‘0’) – extract the value of data-seconds and convert to a number
  • fromEvent(nativeElement, ‘click’).pipe(map(() => totalSeconds)) – emit the specified seconds when user clicks button element

Explain timerInputSubscriptionFn

timerInputSubscriptionFn is also a high order function that returns a function. The function returns a subscription that updates the Subject in TimerService.

  • inject(TimerService) – inject TimerService in function instead of constructor
  • merge(…observables) – merge all observables to create a new observable that emits second
  • subscribe((seconds) => { timerService.updateSeconds(seconds); }) – subscribe observable and update TimerService with the selected seconds

Demystifies RxJS logic in TimerPaneComponent

TimerPaneComponent class is so succinct that you wonder where the Observable codes go. They are extracted to functions in helper file

// timer-pane.helpe.ts

import { inject } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { shareReplay, Observable, map, switchMap, timer, take, withLatestFrom, tap } from 'rxjs';
import { TimerService } from '../services/timer.service';

const oneSecond = 1000;

export const nowToFn = () => {
    const timerService = inject(TimerService);
    return timerService.seconds$.pipe(shareReplay(1));
}
  
const displayEndTime = (now: number, seconds: number): string => {
    const timestamp = now + seconds * oneSecond;
  
    const end = new Date(timestamp);
    const hour = end.getHours();
    const amPm = hour >= 12 ? 'PM': 'AM';
    const adjustedHour = hour > 12 ? hour - 12 : hour;
    const minutes = end.getMinutes();
    return `Be Back At ${adjustedHour}:${minutes < 10 ? '0' : ''}${minutes} ${amPm}`;
}
  
export const displayEndTimeFn = (nowTo$: Observable<number>) => 
    nowTo$.pipe(map((seconds) => displayEndTime(Date.now(), seconds)));
  
const displayTimeLeft = (seconds: number) => {
    const numSeconds = 60;
    const minutes = Math.floor(seconds / numSeconds);
    const remainderSeconds = seconds % numSeconds;
    return `${minutes}:${remainderSeconds < 10 ? '0' : '' }${remainderSeconds}`;
}
  
export const displayTimeLeftFn = (nowTo$: Observable<number>) => {
    const titleService = inject(Title);
    const countDown$ = nowTo$.pipe(switchMap((seconds) => 
        timer(0, oneSecond).pipe(take(seconds + 1))
    ));
   
    return countDown$
      .pipe(
        withLatestFrom(nowTo$),
        map(([countdown, secondsLeft]) => secondsLeft - countdown),
        map((secondsLeft) => displayTimeLeft(secondsLeft)),
        tap((strTimeLeft) => titleService.setTitle(strTimeLeft))
      );
}

nowTo function observes timeService.second$, caches the value with shareReplay and return the Observable.

displayEndTimeFn is a function that returns the end time of the the timer in an Observable. displayEndTime function performs DateTime manipulation and prints the result in a message.

displayTimeLeftFn simulates the count down effect reactively.

const countDown$ = nowTo$.pipe(switchMap((seconds) => 
    timer(0, oneSecond).pipe(take(seconds + 1))
));

When nowTo$ emits seconds, (let’s say N), I have to cancel the previous timer and create a new timer that emits (N + 1) values (0, 1, 2, ….N). Therefore, I use switchMap to return a timer observable

When countDown$ emits a value, 1 second has elapsed and time remained should also decrement.

  • withLatestFrom(nowTo$) obtains the selected seconds
  • map(([countdown, secondsLeft]) => secondsLeft – countdown) derives the remaining seconds
  • map((secondsLeft) => displayTimeLeft(secondsLeft)) displays the remaining seconds in mm:ss format
  • tap((strTimeLeft) => titleService.setTitle(strTimeLeft)) updates the document title to display the remaining time

This is it and I have built a reactive countdown timer using standalone components and directive only.

Final Thoughts

In this post, I show how to use RxJS, Angular standalone components and directive to build a countdown timer. The application has the following characteristics after using Angular 15’s new features:

  • The application does not have NgModules and constructor boilerplate codes.
  • The standalone components are very clean because I moved as many RxJS codes and dependency injections to separate helper files as possible.
  • Using inject offers flexibility in code organization. Initially, I constructed Observables functions outside of component class and later moved those functions to helper files. Pre-Angular 15, TimeService and Title must inject in constructor and I implement additional statements and methods inside the component to access them.

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:

  1. Github Repo: https://github.com/railsstudent/ng-rxjs-30/tree/main/projects/day29-countdown-timer-standalone
  2. Live demo: https://railsstudent.github.io/ng-rxjs-30/day29-countdown-timer-standalone/
  3. Youtube playlist: https://www.youtube.com/watch?v=IzbMAiUJhS8&list=PLfiShkI8VaKj0YeJf-xr17dxR1S4_vPTO
  4. Wes Bos’s JavaScript 30 Challenge: https://github.com/wesbos/JavaScript30

Discover the Power of Real-Time Search wit RxJS and Angular standalone components

Reading Time: 5 minutes

 61 total views

Introduction

This is day 6 of Wes Bos’s JavaScript 30 challenge where I create real-time search input box to filter out cities and states in the USA. This is a challenge for me because I had to rewrite the original real-time search using Angular and adopting the styles of the framework. In the tutorial, I created the components using RxJS, custom operators, Angular standalone components and removed the NgModules. Moreover, Angular HTTP Client is responsible for retrieving the JSON data from GitHub gist.

In this blog post, I define a function that injects HttpClient, retrieves USA cities from external JSON file and caches the response. Next, I create an observable that emits search input to filter out USA cities and states. Finally, I use async pipe to resolve the observable in the inline template to render the matching results.

let's go

Create a new Angular project

ng generate application day6-ng-type-ahead

Bootstrap AppComponent

First, I convert AppComponent into standalone component such that I can bootstrap AppComponent and inject providers in main.ts.

// app.component.ts

import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { TypeAheadComponent } from './type-ahead/type-ahead/type-ahead.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    TypeAheadComponent
  ],
  template: '<app-type-ahead></app-type-ahead>',
  styles: [`
    :host {
      display: block;
    }
  `]
})
export class AppComponent {
  title = 'Day 6 NG Type Ahead';

  constructor(titleService: Title) {
    titleService.setTitle(this.title);
  }
}

In Component decorator, I put standalone: true to convert AppComponent into a standalone component.

Instead of importing TypeAheadComponent in AppModule, I import TypeAheadComponent (that is also a standalone component) in the imports array because the inline template references it. It is because main.ts uses bootstrapApplication to render AppComponent as the root component of the application. When compiler sees <app-type-ahead> in the inline template and AppComponent does not import TypeAheadComponent, the application fails to compile.

// main.ts

import { provideHttpClient } from '@angular/common/http';
import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

bootstrapApplication(AppComponent, 
  {
    providers: [provideHttpClient()]
  })
  .catch(err => console.error(err));

provideHttpClient is a function that configures HttpClient to be available for injection.

Next, I delete AppModule because it is not used anymore.

Declare Type Ahead component

I declare standalone component, TypeAheadComponent, to create a component with search box. To verify the component is a standalone, standalone: true is specified in the Component decorator.

src/app
├── app.component.ts
└── type-ahead
    ├── custom-operators
    │   └── find-cities.operator.ts
    ├── interfaces
    │   └── city.interface.ts
    ├── pipes
    │   ├── highlight-suggestion.pipe.ts
    │   └── index.ts
    └── type-ahead
        ├── type-ahead.component.scss
        └── type-ahead.component.ts

find-cities.ts is a custom RxJS operator that receives search value and filters out USA cities and states by it.

// type-ahead.component.ts

import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { ChangeDetectionStrategy, Component, OnInit, ViewChild, inject } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';
import { Observable, shareReplay } from 'rxjs';
import { findCities } from '../custom-operators/find-cities.operator';
import { City } from '../interfaces/city.interface';
import { HighlightSuggestionPipe } from '../pipes/highlight-suggestion.pipe';

const getCities = () => {
  const httpService = inject(HttpClient);
  const endpoint = 'https://gist.githubusercontent.com/Miserlou/c5cd8364bf9b2420bb29/raw/2bf258763cdddd704f8ffd3ea9a3e81d25e2c6f6/cities.json';
  return httpService.get<City[]>(endpoint).pipe(shareReplay(1));
}

@Component({
  selector: 'app-type-ahead',
  standalone: true,
  imports: [
    HighlightSuggestionPipe,
    FormsModule,
    CommonModule,
  ],
  template: `
    <form class="search-form" #searchForm="ngForm">
      <input type="text" class="search" placeholder="City or State" [(ngModel)]="searchValue" name="searchValue">
      <ul class="suggestions" *ngIf="suggestions$ | async as suggestions">
        <ng-container *ngTemplateOutlet="suggestions?.length ? hasSuggestions : promptFilter; context: { suggestions, searchValue }"></ng-container>
      </ul>
    </form>

    <ng-template #promptFilter>
      <li>Filter for a city</li>
      <li>or a state</li>
    </ng-template>

    <ng-template #hasSuggestions let-suggestions="suggestions" let-searchValue="searchValue">
      <li *ngFor="let suggestion of suggestions">
        <span [innerHtml]="suggestion | highlightSuggestion:searchValue"></span>
        <span class="population">{{ suggestion.population | number }}</span>
      </li>
    </ng-template>
  `,
  styleUrls: ['./type-ahead.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TypeAheadComponent implements OnInit {

  @ViewChild('searchForm', { static: true })
  searchForm!: NgForm;

  searchValue = ''
  suggestions$!: Observable<City[]>;
  cities$ = getCities();

  ngOnInit(): void {
    this.suggestions$ = this.searchForm.form.valueChanges.pipe(findCities(this.cities$));
  }
}

TypeAheadComponent imports CommonModule, FormsModule and HighlightSuggestionPipe in the imports array. CommonModule is included to make ngIf and async pipe available in the inline template. After importing FormsModule, I can build a template form to accept search value. Finally, HighlightSuggestionPipe highlights the search value in the search results for aesthetic purpose.

cities$ is an observable that fetches USA cities from external JSON file. Angular 15 introduces inject that simplifies HTTP request logic in a function. Thus, I don’t need to inject HttpClient in the constructor and perform the same logic.

const getCities = () => {
  const httpService = inject(HttpClient);
  const endpoint = 'https://gist.githubusercontent.com/Miserlou/c5cd8364bf9b2420bb29/raw/2bf258763cdddd704f8ffd3ea9a3e81d25e2c6f6/cities.json';
  return httpService.get<City[]>(endpoint).pipe(shareReplay(1));
}

cities$ = getCities();

suggestions$ is an observable that holds the matching cities and states after search value changes. It is subsequently resolved in inline template to render in a list.

Create RxJS custom operator

It is a matter of taste but I prefer to refactor RxJS operators into custom operators when observable has many lines of code. For suggestion$, I refactor the chain of operators into findCities custom operator and reuse it in TypeAheadComponent.

// find-cities.operator.ts

const findMatches = (formValue: { searchValue: string }, cities: City[]) => {
    const wordToMatch = formValue.searchValue;

    if (wordToMatch === '') {
        return [];
    }

    const regex = new RegExp(wordToMatch, 'gi');
    // here we need to figure out if the city or state matches what was searched
    return cities.filter(place => place.city.match(regex) || place.state.match(regex));
}

export const findCities = (cities$: Observable<City[]>) => {
    return (source: Observable<{ searchValue: string }>) =>
        source.pipe(
            skip(1),
            debounceTime(300),
            distinctUntilChanged(),
            withLatestFrom(cities$),
            map(([formValue, cities]) => findMatches(formValue, cities)),
            startWith([]),
        );
}
  • skip(1) – The first valueChange emits undefined for unknown reason; therefore, skip is used to discard it
  • debounceTime(300) – emit search value after user stops typing for 300 milliseconds
  • distinctUntilChanged() – do nothing when search value is unchanged
  • withLatestFrom(cities$) – get the cities returned from HTTP request
  • map(([formValue, cities]) => findMatches(formValue, cities)) – call findMatches to filter cities and states by search value
  • startWith([]) – initially, the search result is an empty array

Finally, I use findCities to compose suggestion$ observable.

Use RxJS and Angular to implement observable in type ahead component

// type-ahead.component.ts

this.suggestions$ = this.searchForm.form.valueChanges
.pipe(findCities(this.cities$));
  • this.searchForm.form.valueChanges – emit changes in template form
  • findCities(this.cities$) – apply custom operator to find matching cities and states

This is it, we have created a real-time search that filters out USA cities and states by search value.

Final Thoughts

In this post, I show how to use RxJS and Angular standalone components to create real-time search of the USA cities and states. The application has the following characteristics after using Angular 15’s new features:

  • The application does not have NgModules and constructor boilerplate codes.
  • In main.ts, the providers array provides the HttpClient by invoking providerHttpClient function
  • In TypeAheadComponent, I inject HttpClient in a function to make http request and obtain the results. In construction phase, I assign the function to cities$ observable
  • Using inject to inject HttpClient offers flexibility in code organization. I can define getCities function in the component or move it to a utility file. Pre-Angular 15, HttpClient is usually injected in a service and the service has a method to make HTTP request and return 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.

Resources:

  1. Github Repo: https://github.com/railsstudent/ng-rxjs-30/tree/main/projects/day6-ng-type-ahead
  2. Live demo: https://railsstudent.github.io/ng-rxjs-30/day6-ng-type-ahead/
  3. Wes Bos’s JavaScript 30 Challenge: https://github.com/wesbos/JavaScript30

Create an analog clock using RxJS and Angular standalone components

Reading Time: 4 minutes

 61 total views

Introduction

This is day 2 of Wes Bos’s JavaScript 30 challenge where I create an analog clock that displays the current time of the day. In the tutorial, I created the components using RxJS, custom operators, Angular standalone components and removed the NgModules.

In this blog post, I describe how to create an observable that draws the hour, minute and second hands of an analog clock. The clock component creates a timer that emits every second to get the current time, calculates the rotation angle of the hands and set the CSS styles to perform line rotation.

let's go

Create a new Angular project

ng generate application day2-ng-and-css-clock

Bootstrap AppComponent

First, I convert AppComponent into standalone component such that I can bootstrap AppComponent and inject providers in main.ts.

// app.component.ts

import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ClockComponent } from './clock';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    ClockComponent
  ],
  template: '<app-clock></app-clock>',
  styles: [`
    :host {
      display: block;
    }
  `],
})
export class AppComponent {
  constructor(titleService: Title) {
    titleService.setTitle('Day 2 NG and CSS Clock');
  }
}

In Component decorator, I put standalone: true to convert AppComponent into a standalone component.

Instead of importing ClockComponent in AppModule, I import ClockComponent (that is also a standalone component) in the imports array because the inline template references it.

// main.ts

import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

bootstrapApplication(AppComponent).catch(err => console.error(err));

Second, I delete AppModule because it is not used anymore.

Declare Clock component

I declare standalone component, ClockComponent, to create an analog clock. To verify the component is a standalone, standalone: true is specified in the Component decorator.

src/app
├── app.component.ts
└── clock
    ├── clock.component.ts
    ├── clock.interface.ts
    ├── custom-operators
    │   └── clock.operator.ts
    └── index.ts

clock-operators.ts encapsulates two custom RxJS operators that help draw the clock hands of the analog clock. currentTime operator returns seconds, minutes and hours of the current time. rotateClockHands receives the results of currentTime and calculate the rotation angle of the hands.

// clock.component.ts

import { AsyncPipe, NgIf } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { timer } from 'rxjs';
import { currentTime, rotateClockHands } from './custom-operators/clock.operator';

@Component({
  selector: 'app-clock',
  standalone: true,
  imports: [
    AsyncPipe,
    NgIf,
  ],
  template: `
    <div class="clock" *ngIf="clockHandsTransform$ | async as clockHandsTransform">
      <div class="clock-face">
        <div class="hand hour-hand" [style.transform]="clockHandsTransform.hourHandTransform"></div>
        <div class="hand min-hand" [style.transform]="clockHandsTransform.minuteHandTransform"></div>
        <div class="hand second-hand" [style.transform]="clockHandsTransform.secondHandTransform"></div>
      </div>
    </div>
  `,
  styles: [...omitted due to brevity...],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ClockComponent {
  readonly oneSecond = 1000;

  clockHandsTransform$ = timer(0, this.oneSecond)
    .pipe(
      currentTime(),
      rotateClockHands(),
    );
}

ClockComponent imports NgIf and AsyncPipe because the components uses ngIf and async keywords to resolve clockHandsTransform$ observable. clockHandsTransform$ is an observable that is consisted of the CSS styles to draw hour, minute and second hands. The observable is succinct because the currentTime and rotateClockHands custom operators encapsulate the logic.

Create RxJS custom operators

It is a matter of taste but I prefer to refactor RxJS operators into custom operators when observable has many lines of code. For clockHandsTransform$, I refactor map into custom operators and reuse them in ClockComponent.

// clock.operator.ts

export function currentTime() {
    return map(() => { 
        const time = new Date();
        return { 
            seconds: time.getSeconds(),
            minutes: time.getMinutes(),
            hours: time.getHours()
        }
    });
}

currentTime operator gets the current time and calls the methods of the Date object to return the current second, minutes and hours.

// clock.operator.ts

function rotateAngle (seconds: number, minutes: number, hours: number): HandTransformations { 
    const secondsDegrees = ((seconds / 60) * 360) + 90;
    const minsDegrees = ((minutes / 60) * 360) + ((seconds / 60) * 6) + 90;
    const hourDegrees = ((hours / 12) * 360) + ((minutes / 60) * 30) + 90;

    return { 
        secondHandTransform: `rotate(${secondsDegrees}deg)`,
        minuteHandTransform: `rotate(${minsDegrees}deg)`,
        hourHandTransform: `rotate(${hourDegrees}deg)`,
    }
}

export function rotateClockHands() {
    return function (source: Observable<{ seconds: number, minutes: number, hours: number }>) {
        return source.pipe(map(({ seconds, minutes, hours }) => rotateAngle(seconds, minutes, hours)));
    }
}

currentTime emits the results to rotateClockHands and the rotateClockHands operator invokes a helper function, rotateAngle, to derive the CSS styles of the hands.

Finally, I use both operators to compose clockHandsTransform$ observable.

Use RxJS and Angular to implement observable in clock component

// clock.component.ts
clockHandsTransform$ = timer(0, this.oneSecond)
    .pipe(
      currentTime(),
      rotateClockHands(),
    );
  • timer(0, this.oneSecond) – emits an integer every second
  • currentTime() – return the current second, minute and hour
  • rotateClockHands() – calculate the rotation angle of second, minute and hour hands

This is it, we have created a functional analog clock that displays the current time.

Final Thoughts

In this post, I show how to use RxJS and Angular standalone components to create an analog clock. The application has the following characteristics after using Angular 15’s new features:

  • The application does not have NgModules and constructor boilerplate codes.
  • In ClockComponent, I import NgIf and AsyncPipe rather than CommonModule, only the minimum parts that the component requires.

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:

  1. Github Repo: https://github.com/railsstudent/ng-rxjs-30/tree/main/projects/day2-ng-and-css-clock
  2. Live demo: https://railsstudent.github.io/ng-rxjs-30/day2-ng-and-css-clock/
  3. Wes Bos’s JavaScript 30 Challenge: https://github.com/wesbos/JavaScript30

Create a drum kit using RxJS and Angular standalone components

Reading Time: 7 minutes

 55 total views

Introduction

This is day 1 of Wes Bos’s JavaScript 30 challenge where I create a drum kit to play sounds when keys are pressed. In the tutorial, I created the components using RxJS, Angular standalone components and removed the NgModules.

In this blog post, I describe how the drum component (parent) uses RxJS fromEvent to listen to keydown event, discard unwanted keys and play sound when “A”, “S”, “D”, “F”, “G”, “H”, “J”, “K” or “L” is hit. When the correct key is pressed, the parent updates the subject that drum kit components (children) subscribe to. Then, the child with the matching key plays the corresponding sound to make a tune.

let's go

Create a new Angular project

ng generate application day1-javascript-drum-kit

Bootstrap AppComponent

First, I convert AppComponent into standalone component such that I can bootstrap AppComponent and inject providers in main.ts.

// app.component.ts

import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { DrumComponent } from './drum';

@Component({
  selector: 'app-root',
  imports: [
    DrumComponent,
  ],
  template: '<app-drum></app-drum>',
  styles: [`
    :host {
      display: block;
      height: 100vh;
    }
  `],
  standalone: true,
})
export class AppComponent {
  title = 'RxJS Drum Kit';

  constructor(private titleService: Title) {
    this.titleService.setTitle(this.title);
  }
}

In Component decorator, I put standalone: true to convert AppComponent into a standalone component.

Instead of importing DrumComponent in AppModule, I import DrumComponent (that is also a standalone component) in the imports array because the inline template references it.

// main.ts

import { enableProdMode, inject } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';

import { APP_BASE_HREF, PlatformLocation } from '@angular/common';
import { AppComponent } from './app/app.component';
import { browserWindowProvider, windowProvider } from './app/core/services';
import { environment } from './environments/environment';

bootstrapApplication(AppComponent, {
  providers: [
    {
      provide: APP_BASE_HREF,
      useFactory: () => inject(PlatformLocation).getBaseHrefFromDOM(),
    },
    browserWindowProvider,
    windowProvider,
  ]
})
  .catch(err => console.error(err));

browserWindowProvider and windowProvider are providers from core folder and I will show the source codes later.

Second, I delete AppModule because it is not used anymore.

Add window service to listen to keydown event

In order to detect key down on native Window, I write a window service to inject to ScrollComponent to listen to keydown event. The sample code is from Brian Love’s blog post here.

// core/services/window.service.ts

import { isPlatformBrowser } from "@angular/common";
import { ClassProvider, FactoryProvider, InjectionToken, PLATFORM_ID } from '@angular/core';

/* Create a new injection token for injecting the window into a component. */
export const WINDOW = new InjectionToken('WindowToken');

/* Define abstract class for obtaining reference to the global window object. */
export abstract class WindowRef {
  get nativeWindow(): Window | Object {
    throw new Error('Not implemented.');
  }
}

/* Define class that implements the abstract class and returns the native window object. */
export class BrowserWindowRef extends WindowRef {
  constructor() {
    super();
  }

  override get nativeWindow(): Object | Window {
    return window;    
  }
}

/* Create an factory function that returns the native window object. */
function windowFactory(browserWindowRef: BrowserWindowRef, platformId: Object): Window | Object {
  if (isPlatformBrowser(platformId)) {
    return browserWindowRef.nativeWindow;
  }
  return new Object();
}

/* Create a injectable provider for the WindowRef token that uses the BrowserWindowRef class. */
export const browserWindowProvider: ClassProvider = {
  provide: WindowRef,
  useClass: BrowserWindowRef
};

/* Create an injectable provider that uses the windowFactory function for returning the native window object. */
export const windowProvider: FactoryProvider = {
  provide: WINDOW,
  useFactory: windowFactory,
  deps: [ WindowRef, PLATFORM_ID ]
};

I export browserWindowProvider and windowProvider to inject both providers in main.ts.

Declare Drum and DrumKey components

I declare standalone components, DrumComponent and DrumKeyComponent, to create a drum kit. To verify they are standalone, standalone: true is specified in the Component decorator.

src/app
├── app.component.ts
├── core
│   └── services
│       ├── index.ts
│       └── window.service.ts
├── drum
│   ├── drum.component.ts
│   └── index.ts
├── drum-key
│   ├── drum-key.component.ts
│   └── index.ts
├── helpers
│   ├── get-asset-path.ts
│   ├── get-host-native-element.ts
│   └── index.ts
├── interfaces
│   ├── index.ts
│   └── key.interface.ts
└── services
    ├── drum.service.ts
    └── index.ts
// get-asset-path.ts

import { APP_BASE_HREF } from '@angular/common';
import { inject } from '@angular/core';

export const getFullAssetPath = () => {
    const baseHref = inject(APP_BASE_HREF);
    const isEndWithSlash = baseHref.endsWith('/');
    return `${baseHref}${isEndWithSlash ? '' : '/'}assets/`;
}
// get-host-native-element.ts

import { ElementRef, inject } from '@angular/core';

export const getHostNativeElement = 
() => inject<ElementRef<HTMLElement>>(ElementRef<HTMLElement>).nativeElement;

getFullAssetPath and getHostNativeElement are helper functions that inject application base href and host native element in the construction phase of the components.

// drum.component.ts

import { NgFor } from '@angular/common';
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, inject } from '@angular/core';
import { filter, fromEvent, map } from 'rxjs';
import { WINDOW } from '../core/services';
import { DrumKeyComponent } from '../drum-key/drum-key.component';
import { getFullAssetPath, getHostNativeElement } from '../helpers';
import { DrumService } from '../services';

const getImageUrl = () => { 
  const imageUrl = `${getFullAssetPath()}images/background.jpg`;
  return `url('${imageUrl}')`;
}

const getEntryStore = () => { 
  const getEntryStore = inject(DrumService); 
  return getEntryStore.getEntryStore();
};

const windowKeydownSubscription = () => {
  const drumService = inject(DrumService);
  const allowedKeys = getEntryStore().allowedKeys;
  return fromEvent(inject<Window>(WINDOW), 'keydown')
    .pipe(
      filter(evt => evt instanceof KeyboardEvent),
      map(evt => evt as KeyboardEvent),
      map(({ key }) => key.toUpperCase()),
      filter(key => allowedKeys.includes(key)),
    ).subscribe((key) => drumService.playSound(key));
}

@Component({
  imports: [
    NgFor,
    DrumKeyComponent,
  ],
  standalone: true,
  selector: 'app-drum',
  template: `
    <div class="keys">
      <app-drum-key *ngFor="let entry of entries" [entry]="entry" class="key"></app-drum-key>
    </div>
  `,
  styles: [`...omitted due to brevity...`],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DrumComponent implements OnInit, OnDestroy {
  entries = getEntryStore().entries;
  hostElement = getHostNativeElement();
  imageUrl = getImageUrl();
  subscription = windowKeydownSubscription();

  ngOnInit(): void {
    this.hostElement.style.backgroundImage = this.imageUrl;
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
}

DrumComponent imports DrumKeyComponent and NgFor to render different drum keys. NgFor is required because inline template uses ng-for directive to create a drum kit. windowKeydownSubscription uses RxJS to create an Observable to observe keydown event and subscribe the Observable to return an instance of Subscription.

// drum-key.component.ts

import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostBinding, Input, OnDestroy, ViewChild, inject } from '@angular/core';
import { filter, fromEvent, map } from 'rxjs';
import { getFullAssetPath, getHostNativeElement } from '../helpers';
import { Key } from '../interfaces';
import { DrumService } from '../services';

const getSoundFileFn = () => {
  const assetPath = getFullAssetPath();
  return (description: string) => `${assetPath}sounds/${description}.wav`;
}

const drumKeyTranstionEnd = () => 
  fromEvent(getHostNativeElement(), 'transitionend')
    .pipe(
      filter(evt => evt instanceof TransitionEvent),
      map(evt => evt as TransitionEvent),
      filter(evt => evt.propertyName === 'transform')
    );

@Component({
  standalone: true,
  selector: 'app-drum-key',
  template: `
    <ng-container>
      <kbd>{{ entry.key }}</kbd>
      <span class="sound">{{ entry.description }}</span>
      <audio [src]="soundFile" #audio></audio>
    </ng-container>
  `,
  styles: [`...omitted due to brevity...`],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DrumKeyComponent implements OnDestroy {
  @Input() 
  entry!: Key;

  @ViewChild('audio', { static: true })
  audio: ElementRef<HTMLAudioElement> | undefined;

  @HostBinding('class.playing') isPlaying = false;

  cdr = inject(ChangeDetectorRef);
  playSoundSubscription = inject(DrumService).playDrumKey$
    .pipe(filter(key => key === this.entry.key))
    .subscribe(() => this.playSound());
  transitionSubscription = drumKeyTranstionEnd()
    .subscribe(() => {
      this.isPlaying = false;
      this.cdr.markForCheck();
    });
  getSoundFile = getSoundFileFn();

  get soundFile() {
    return this.getSoundFile(this.entry.description);
  }

  playSound() {
    if (!this.audio) {
      return;
    }

    const nativeElement = this.audio.nativeElement;
    nativeElement.currentTime = 0;
    nativeElement.play();
    this.isPlaying = true;
    this.cdr.markForCheck();
  }

  ngOnDestroy(): void {
    this.playSoundSubscription.unsubscribe();
    this.transitionSubscription.unsubscribe();
  }
}

DrumKeyComponent constructs playSoundSubscription and transitionSubscription subscriptions to play the actual sound and display a yellow border until the sound ends. Using inject operator, I construct these subscriptions outside of constructor and ngOnInit.

Declare drum service to pass data from Drum to DrumKey component

When DrumComponent observes the correct key is pressed, the key must emit to DrumKeyComponent to perform CSS animation and play sound. The data is emit to Subject that is encapsulated in DrumService.

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class DrumService {
  private readonly playDrumKey = new Subject<string>();
  readonly playDrumKey$ = this.playDrumKey.asObservable();

  playSound(key: string) {
    this.playDrumKey.next(key);
  }

  getEntryStore() {
    const entries: Key[] = [
      {
        key: 'A',
        description: 'clap'
      },
      {
        key: 'S',
        description: 'hihat'
      },
      {
        key: 'D',
        description: 'kick'
      },
      {
        key: 'F',
        description: 'openhat'
      },
      {
        key: 'G',
        description: 'boom'
      },
      {
        key: 'H',
        description: 'ride'
      },
      {
        key: 'J',
        description: 'snare'
      },
      {
        key: 'K',
        description: 'tom'
      },
      {
        key: 'L',
        description: 'tink'
      }
    ];

    return {
      entries,
      allowedKeys: entries.map(entry => entry.key),
    }
  }
}

Use RxJS and Angular to implement key down observable

Declare subscription instance member, assign the result of windowKeydownSubscription to it and unsubscribe in ngDestroy()

subscription = windowKeydownSubscription();

ngOnDestroy(): void {
    this.subscription.unsubscribe();
}
// drum.component.ts

const windowKeydownSubscription = () => {
  const drumService = inject(DrumService);
  const allowedKeys = getEntryStore().allowedKeys;
  return fromEvent(inject<Window>(WINDOW), 'keydown')
    .pipe(
      filter(evt => evt instanceof KeyboardEvent),
      map(evt => evt as KeyboardEvent),
      map(({ key }) => key.toUpperCase()),
      filter(key => allowedKeys.includes(key)),
    ).subscribe((key) => drumService.playSound(key));
}
  • fromEvent(inject<Window>(WINDOW), ‘keydown’) – observe keydown event on native window
  • filter(evt => evt instanceof KeyboardEvent) – filter event is an instance of KeyboardEvent
  • map(evt => evt as KeyboardEvent) – cast event to KeyboardEvent
  • map(({ key }) => key.toUpperCase()) – convert key to uppercase
  • filter(key => allowedKeys.includes(key)) – validate key can play sound
  • subscribe((key) => drumService.playSound(key)) – subscribe the observable to play the wav file

Use RxJS and Angular to implement play sound file

// drum-key.component.ts

const drumKeyTranstionEnd = () => 
  fromEvent(getHostNativeElement(), 'transitionend')
    .pipe(
      filter(evt => evt instanceof TransitionEvent),
      map(evt => evt as TransitionEvent),
      filter(evt => evt.propertyName === 'transform')
    );

playSoundSubscription = inject(DrumService).playDrumKey$
    .pipe(filter(key => key === this.entry.key))
    .subscribe(() => this.playSound());

transitionSubscription = drumKeyTranstionEnd()
    .subscribe(() => {
      this.isPlaying = false;
      this.cdr.markForCheck();
    });

Let’s demystify playSoundSubscription

  • inject(DrumService).playDrumKey$ – observe playDrumKey$ observable of DrumService
  • filter(key => key === this.entry.key) – compare component’s key and the key pressed, and they are the same
  • subscribe(() => this.playSound()) – play the wav file

Let’s demystify drumKeyTranstionEnd and transitionSubscription

  • fromEvent(getHostNativeElement(), ‘transitionend’)- observe transition event of the host element
  • filter(evt => evt instanceof TransitionEvent) – filter event is an instance of TransitionEvent
  • map(evt => evt as TransitionEvent) – cast event to TransitionEvent
  • filter(evt => evt.propertyName === ‘transform’) – filter the event property is transform
  • subscribe(() => { this.isPlaying = false; this.cdr.markForCheck(); }) – subscribe the observable to update host class to display yellow border until the sound stops

This is it, we have created a drum kit that plays sound after pressing key.

Final Thoughts

In this post, I show how to use RxJS and Angular standalone components to create a drum kit. The application has the following characteristics after using Angular 15’s new features:

  • The application does not have NgModules and constructor boilerplate codes.
  • Apply inject operator to inject services in const functions outside of component classes. The component classes are shorter and become easy to comprehend.
  • In DrumKeyComponent, I assign subscriptions to instance members directly and don’t have to implement OnInit lifecycle hook.

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:

  1. Github Repo: https://github.com/railsstudent/ng-rxjs-30/tree/main/projects/day1-javascript-drum-kit
  2. Live demo: https://railsstudent.github.io/ng-rxjs-30/day1-javascript-drum-kit/
  3. Wes Bos’s JavaScript 30 Challenge: https://github.com/wesbos/JavaScript30