Too many ViewChild? Try ViewChildren to query DOM elements

Reading Time: 3 minutes

 39 total views,  3 views today

Introduction

This post describes how to refactor component to use ViewChildren to query DOM elements. It is a common case when inline template has several elements of the same type and the component uses ViewChild to query them. When this pattern occurs, we can consider to render the elements using ngFor and applying ViewChildren to obtain the elements in QueryList.

let's go

Pokemon Controls component applying ViewChild

// pokemon-controls.component.ts

@Component({
  selector: 'app-pokemon-controls',
  standalone: true,
  imports: [FormsModule],
  template: `
    <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 PokemonControlsComponent implements OnInit, OnDestroy {
  @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>;
 
  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.subscription = merge(btnMinusTwo$, btnMinusOne$, btnAddOne$, btnAddTwo$)
      .pipe(... RxJS logic...)
      .subscribe(...subscribe logic...);
  }

  createButtonClickObservable(ref: ElementRef<HTMLButtonElement>, value: number) {
    return fromEvent(ref.nativeElement, 'click').pipe(
      map(() => ({ value, action: POKEMON_ACTION.ADD }))
    );
  }
}

In PokemonControlsComponent standalone component, the inline template has four buttons with different template variable names. In the component, I applied ViewChild decorators to access the button elements and create button click Observables.

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

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

Then, I use merge operator to merge these Observables to emit the current Pokemon id and call Pokemon API to retrieve the current Pokemon

Since these buttons have the same type, I can refactor the template to render them using NgFor and assigning the same template variable name. Moreover, I can simplify all occurrences of ViewChild with a single ViewChildren decorator.

Rather than copy and paste <button> four times, I will render the buttons using ngFor. Before using ngFor, NgFor has to be imported because the component is standalone.

<div class="container">
      <button *ngFor="let delta of [-2, -1, 1, 2]" class="btn" #btn [attr.data-delta]="delta">
        {{ delta < 0 ? delta : '+' + delta }}
      </button>
</div>

The inline template renders all the buttons and assigns #btn template variable name to them. [attr.data-delta]="delta" stores the delta in data attribute named delta and the data attribute will be used when creating button click Observables. In the component, I update the code to apply ViewChildren to query the button elements.

@ViewChildren('btn', { read: ElementRef })
btns: QueryList<ElementRef<HTMLButtonElement>>;

this.btns is undefined in ngOnInit; therefore, I move the RxJS logic to ngAfterViewInit.

ngAfterViewInit(): void {
    const btns$ = this.btns.map(({ nativeElement }) => this.createButtonClickObservable(nativeElement));

    this.subscription = merge(...btns$)
      .pipe(...RxJS logic...)
      .subscribe(...subscribe logic...);
}

this.btns is QueryList and merge expects a spread array. Therefore, I iterate the QueryList to construct an array of Observables and pass the array to merge.

createButtonClickObservable(nativeElement: HTMLButtonElement) {
    const value = +(nativeElement.dataset['delta'] || 0);
    return fromEvent(nativeElement, 'click').pipe(
      map(() => ({ value, action: POKEMON_ACTION.ADD }))
    );
}

In createButtonClickObservable, I look up the delta in the data attribute and continue to create the new Observable.

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/blob/main/projects/pokemon-demo-6
  2. Stackblitz: https://stackblitz.com/edit/angular-kvsoxr?file=src%2Fpokemon%2Fpokemon-controls%2Fpokemon-controls.component.ts
  3. Youtube: https://www.youtube.com/watch?v=N74CSvHkuaQ
  4. PokeAPI: https://pokeapi.co/

Interested in latest HTTP response? Use RxJS SwitchMap operator

Reading Time: 3 minutes

 73 total views

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

 44 total views,  1 views today

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

 48 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

 60 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

 54 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

Hover link and open dropdown using RxJS and Angular

Reading Time: 6 minutes

 57 total views

Introduction

This is day 26 of Wes Bos’s JavaScript 30 challenge where I hover link and open dropdown below it. I am going to use RxJS and Angular to rewrite the challenge from Vanilla JavaScript.

In this blog post, I describe how to use RxJS fromEvent to listen to mouseenter event and emit the event to concatMap operator. In the callback of concatMap, it emits a timer to add another CSS class after 150 milliseconds. When CSS classes are added, a white dropdown appears below the link. The RxJS code creates the effect of hover link and open dropdown. Moreover, the tutorial also uses RxJS fromEvent to listen to mouseleave event to remove the CSS classes to close the dropdown.

let's go

Create a new Angular project

ng generate application day26-strike-follow-along-link

Create Stripe feature module

First, we create a Stripe feature module and import it into AppModule. The feature module encapsulates StripeNavPageComponent that is comprised of three links.

Import StripeModule in AppModule

// stripe.module.ts

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { CoolLinkDirective } from './directives/cool-link.directive';
import { StripeNavPageComponent } from './stripe-nav-page/stripe-nav-page.component';

@NgModule({
  declarations: [
    StripeNavPageComponent,
    CoolLinkDirective
  ],
  imports: [
    CommonModule
  ],
  exports: [
    StripeNavPageComponent
  ]
})
export class StripeModule { }
// app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { StripeModule } from './stripe';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    StripeModule
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Declare Stripe component in feature module

In Stripe feature module, we declare StripeNavPageComponent to build the application. StripeNavPageComponent depends on inline template, encapsulated SCSS file and global styles in order to work properly.

src
├── app
│   ├── app.component.ts
│   ├── app.module.ts
│   └── stripe
│       ├── directives
│       │   └── cool-link.directive.ts
│       ├── index.ts
│       ├── services
│       │   └── stripe.service.ts
│       ├── stripe-nav-page
│       │   ├── stripe-nav-page.component.scss
│       │   └── stripe-nav-page.component.ts
│       └── stripe.module.ts
├── styles.scss
// style.scss

nav {
  position: relative;
  perspective: 600px;
}

.cool > li > a {
  color: yellow;
  text-decoration: none;
  font-size: 20px;
  background: rgba(0,0,0,0.2);
  padding: 10px 20px;
  display: inline-block;
  margin: 20px;
  border-radius: 5px;
}

nav ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  justify-content: center;
}

.cool > li {
  position: relative;
  display: flex;
  justify-content: center;
}

.dropdown {
  opacity: 0;
  position: absolute;
  overflow: hidden;
  padding: 20px;
  top: -20px;
  border-radius: 2px;
  transition: all 0.5s;
  transform: translateY(100px);
  will-change: opacity;
  display: none;
}

.trigger-enter .dropdown {
  display: block;
}

.trigger-enter-active .dropdown {
  opacity: 1;
}

.dropdown a {
  text-decoration: none;
  color: #ffc600;
}

After numerous trials and errors, the CSS styles of navigation bar and dropdowns only work when I put them in the global stylesheet.

// stripe-nav-page.component.ts

import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy } from '@angular/core';
import { CoolLinkDirective } from '../directives/cool-link.directive';
import { StripeService } from '../services/stripe.service';

@Component({
  selector: 'app-stripe-nav-page',
  template: `
    <ng-container>
      <h2>Cool</h2>
      <nav class="top" #top>
        <div class="dropdownBackground" #background>
          <span class="arrow"></span>
        </div>
        <ul class="cool">
          <li class="link">
            <a href="#">About Me</a>
            <ng-container *ngTemplateOutlet="aboutMe"></ng-container>
          </li>
          <li class="link">
            <a href="#">Courses</a>
            <ng-container *ngTemplateOutlet="courses"></ng-container>
          </li>
          <li class="link">
            <a href="#">Other Links</a>
            <ng-container *ngTemplateOutlet="social"></ng-container>
          </li>
        </ul>
      </nav>
    </ng-container>

    <ng-template #aboutMe>
      <div class="dropdown">
        <div class="bio">
          <img src="https://logo.clearbit.com/wesbos.com">
          <p>Wes Bos sure does love web development. He teaches things like JavaScript, CSS and BBQ. Wait. BBQ isn't part of web development. It should be though!</p>
        </div>
      </div>
    </ng-template>

    <ng-template #courses>
      <ul class="dropdown courses">
        <ng-container *ngIf="coursesTaught$ | async as coursesTaught">
          <li *ngFor="let x of coursesTaught; trackBy: trackByIndex">
            <span class="code">{{ x.code }}</span>
            <a [href]="x.link">{{ x.description }}</a>
          </li>
        </ng-container>
      </ul>
    </ng-template>

    <ng-template #social>
      <ul class="dropdown">
        <ng-container *ngIf="socialAccounts$ | async as socialAccounts">
          <li *ngFor="let account of socialAccounts; trackBy: trackByIndex">
            <a class="button" [href]="account.link">{{ account.description }}</a>
          </li>
        </ng-container>
      </ul>
    </ng-template>
  `,
  styleUrls: ['./stripe-nav-page.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class StripeNavPageComponent implements AfterViewInit, OnDestroy {
  socialAccounts$ = this.stripeService.getSocial();
  coursesTaught$ = this.stripeService.getCourses();

  constructor(private stripeService: StripeService) { }

  ngAfterViewInit(): void {}

  trackByIndex(index: number) {
    return index;
  }

  ngOnDestroy(): void {}
}

Next, I delete boilerplate codes in AppComponent and render StripeNavPageComponent in inline template.

// app.component.ts

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

@Component({
  selector: 'app-root',
  template: '<app-stripe-nav-page></app-stripe-nav-page>',
  styles: [`
    :host {
      display: block;
    }
  `],
})
export class AppComponent {
  title = 'Day 26 Stripe Follow Along Nav';

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

Declare stripe service to retrieve data

In an enterprise application, courses and personal information are usually kept in a server. Therefore, I define a service that constructs fake requests to remote server. In the methods, the codes wait 250 – 300 milliseconds before returning the data array in Observable.

import { Injectable } from '@angular/core';
import { map, timer } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class StripeService {
  getSocial() {
    return timer(300)
      .pipe(
        map(() => ([
          {
            link: 'http://twitter.com/wesbos',
            description:  'Twitter'
          },
          ... other social media accounts ...
        ])
      )
    );
  }

  getCourses() {
    return timer(250)
      .pipe(
        map(() => ([
          {
            code: 'RFB',
            link: 'https://ReactForBeginners.com',
            description: 'React For Beginners'
          },
          ... other courses ...
        ])
      )
    );
  }
}

StripeNavPageComponent injects the service to retrieve data, uses async pipe to resolve the Observables in the inline template and iterates the arrays to render the HTML elements.

socialAccounts$ = this.stripeService.getSocial();
<ng-container *ngIf="socialAccounts$ | async as socialAccounts">
    <li *ngFor="let account of socialAccounts; trackBy: trackByIndex">
         <a class="button" [href]="account.link">{{ account.description }}</a>
    </li>
</ng-container>
coursesTaught$ = this.stripeService.getCourses();
<ng-container *ngIf="coursesTaught$ | async as coursesTaught">
    <li *ngFor="let x of coursesTaught; trackBy: trackByIndex">
        <span class="code">{{ x.code }}</span>
        <a [href]="x.link">{{ x.description }}</a>
    </li>
</ng-container>

Refactor RxJS logic to custom operator

In the complete implementation, ngAfterViewInit becomes bulky after putting in the RxJS logic. To make the logic more readable, I refactor the hover link logic to a custom RxJS operator.

import { Observable, concatMap, tap, timer } from 'rxjs';

export function hoverLink<T extends HTMLElement>(nativeElement: T) {
    return function (source: Observable<any>) {
        return source.pipe(
            tap(() => nativeElement.classList.add('trigger-enter')),
            concatMap(() => timer(150)
              .pipe(
                tap(() => {
                  if (nativeElement.classList.contains('trigger-enter')) {
                    nativeElement.classList.add('trigger-enter-active');
                  }
                })
              )
            )
        )
    }
}

hoverLink operator adds trigger-enter class to <li> element and emits the event to concatMap. concatMap emits a timer that waits 150 milliseconds before firing to add the second class, trigger-enter-active to the same element.

Use RxJS and Angular to implement hover link and open dropdown effect

Use ViewChildren to obtain references to <li> elements

@ViewChildren(CoolLinkDirective)
links!: QueryList<CoolLinkDirective>;

I am going to iterate the links to register mouseenter and mouseleave events, toggle CSS classes of the <li> elements and subscribe the observables.

Use ViewChild to obtain references to HTML elements

@ViewChild('top', { static: true, read: ElementRef })
nav!: ElementRef<HTMLElement>;

@ViewChild('background', { static: true, read: ElementRef })
background!: ElementRef<HTMLDivElement>;

Declare subscription instance member and unsubscribe in ngDestroy()

subscription = new Subscription();

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

ngAfterViewInit provides the RxJS logic to emit and subscribe mouseenter and mouseleave events.

// stripe-nav-page.component.ts

private navBarClosure(navCoords: DOMRect) {
  return (dropdownCoords: DOMRect) => {
     const top = dropdownCoords.top - navCoords.top;
     const left = dropdownCoords.left - navCoords.left;

     const backgroundNativeElement = this.background.nativeElement;
     backgroundNativeElement.style.width = `${dropdownCoords.width}px`;
     backgroundNativeElement.style.height = `${dropdownCoords.height}px`;
     backgroundNativeElement.style.transform = `translate(${left}px, ${top}px)`;
     backgroundNativeElement.classList.add('open');
  }
}

ngAfterViewInit(): void {
    const translateBackground = this.navBarClosure(this.nav.nativeElement.getBoundingClientRect());

    this.links.forEach(({ nativeElement }) => {
      const mouseEnterSubscription = fromEvent(nativeElement, 'mouseenter')
        .pipe(hoverLink(nativeElement))
        .subscribe(() => {
          const dropdown = nativeElement.querySelector('.dropdown');
          if (dropdown) {
            translateBackground(dropdown.getBoundingClientRect());
          }
        });

      const mouseLeaveSubscription = fromEvent(nativeElement, 'mouseleave')
        .subscribe(() => {
          nativeElement.classList.remove('trigger-enter-active', 'trigger-enter');
          this.background.nativeElement.classList.remove('open');
        });

      this.subscriptions.add(mouseEnterSubscription);
      this.subscriptions.add(mouseLeaveSubscription);
    });
}

Add CSS classes and open dropdown

const translateBackground = this.navBarClosure(this.nav.nativeElement.getBoundingClientRect());

const mouseEnterSubscription = fromEvent(nativeElement, 'mouseenter')
   .pipe(hoverLink(nativeElement))
   .subscribe(() => {
       const dropdown = nativeElement.querySelector('.dropdown');
       if (dropdown) {
          translateBackground(dropdown.getBoundingClientRect());
       }
    });
this.subscriptions.add(mouseEnterSubscription)
  • fromEvent(nativeElement, ‘mouseenter’) – observe mouseenter event on the <li> element
  • hoverLink(nativeElement) – a custom decorator that encapsulates the logic to add trigger-enter-active and trigger-enter classes to the <li> element
  • subscribe(() => { … }) – select the associated dropdown and invoke translateBackground function. translateBackground function translates the white background to the position of the dropdown and opens it
  • this.subscriptions.add(mouseEnterSubscription) – append mouseEnterSubscription subscription to this.subscriptions in order to release the memory in ngOnDestroy

Remove CSS classes and close dropdown

const mouseLeaveSubscription = fromEvent(nativeElement, 'mouseleave')
   .subscribe(() => {
       nativeElement.classList.remove('trigger-enter-active', 'trigger-enter');
       this.background.nativeElement.classList.remove('open');
   });

this.subscriptions.add(mouseLeaveSubscription)
  • fromEvent(nativeElement, ‘mouseleave’) – observe mouseleave event on the <li> element
  • subscribe(() => { … }) – subscribe the observable to remove trigger-enter-active and trigger-enter class from the <li> element. Then, remove ‘open’ class to close the dropdown
  • this.subscriptions.add(mouseLeaveSubscription) – append mouseLeaveSubscription subscription to this.subscriptions in order to release the memory in ngOnDestroy

This is it, we have completed the tutorial that opens the associated dropdown when mouse hovers a list item.

Final Thoughts

In this post, I show how to use RxJS and Angular to hover link and open dropdown. fromEvent observes mouseenter event to add the first CSS class and emits the event to concatMap to add the second CSS class after 150 milliseconds. The observable is subscribed to translate the white background and open the dropdown. Moreover, another fromEvent emits mouseleave to remove the previously added classes and close the dropdown.

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/day26-stripe-follow-along-nav
  2. Live demo: https://railsstudent.github.io/ng-rxjs-30/day26-stripe-follow-along-nav/
  3. Wes Bos’s JavaScript 30 Challenge: https://github.com/wesbos/JavaScript30

Click and slide HTML elements using RxJS and Angular

Reading Time: 5 minutes

 57 total views

Introduction

This is day 27 of Wes Bos’s JavaScript 30 challenge and I am going to use RxJS and Angular to click and slide a series of div elements. When mouse click occurs, I add a CSS class to the parent <div> element to perform scale transformation. When mouse is up or it exits the browser, I remove the class to undo the CSS transformation.

In this blog post, I describe how to use RxJS fromEvent to listen to mousedown event and emit the event to concatMap operator. In the callback of concatMap, it streams mousemove events, updates the scrollLeft property of the div element and flattens the inner Observables. Moreover, the Angular component resolves another Observable in the inline template in order to toggle CSS class in ngClass property.

let's go

Create a new Angular project

ng generate application day27-click-and-drag

Create Slider feature module

First, we create a Slider feature module and import it into AppModule. The feature module encapsulates SliderComponent with a list of <div> elements.

Import SliderModule in AppModule

// slider.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SliderComponent } from './slider/slider.component';

@NgModule({
  declarations: [
    SliderComponent
  ],
  imports: [
    CommonModule
  ],
  exports: [
    SliderComponent
  ]
})
export class SliderModule { }
// app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { SliderModule } from './slider';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    SliderModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Declare Slider component in feature module

In Slider feature module, we declare SliderComponent to build the application. SliderComponent depends on inline template and SCSS file because styling is long in this tutorial.

src/app
├── app.component.ts
├── app.module.ts
└── slider
    ├── index.ts
    ├── slider
    │   ├── slider.component.scss
    │   └── slider.component.ts
    └── slider.module.ts
// slider.component.ts

import { ChangeDetectionStrategy, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Observable, of } from 'rxjs';

@Component({
  selector: 'app-slider',
  template: `
    <div class="items" [ngClass]="{ active: active$ | async }" #items>
      <div *ngFor="let index of panels" class="item">{{index}}</div>
    </div>
  `,
  styleUrls: ['./slider.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SliderComponent implements OnInit, OnDestroy {

  @ViewChild('items', { static: true, read: ElementRef })
  slider!: ElementRef<HTMLDivElement>;

  active$!: Observable<boolean>;

  panels = [...Array(25).keys()].map(i => i < 9 ? `0${i + 1}` : `${i + 1}`);

  ngOnInit(): void {
    this.active$ = of(false);
  }

  ngOnDestroy(): void {
  }
}

SliderComponent generates 25 <div> elements numbered between 01 and 25. When I click any <div> element, I can slide from left to right and vice versa. Moreover, the parent <div> of the <div> elements includes an active CSS class when sliding occurs.

this.active$ is a boolean Observable, it resolves and toggles the active CSS class of the <div> element. When the class is found in the element , scale transformation occurs and background color changes.

Next, I delete boilerplate codes in AppComponent and render SliderComponent in inline template.

// app.component.ts

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

@Component({
  selector: 'app-root',
  template: '<app-slider></app-slider>',
  styles: [`
    :host {
      display: block;
    }
  `]
})
export class AppComponent {
  title = 'Day 27 Click and Drag';

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

Use RxJS and Angular to implement SliderComponent

I am going to rewrite active$ Observable and create a click-and-slide subscription in ngOnInit method.

Use ViewChild to obtain reference to div element

@ViewChild('items', { static: true, read: ElementRef })
slider!: ElementRef<HTMLDivElement>;

Declare subscription instance member and unsubscribe in ngDestroy()

subscription = new Subscription();

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

ngOnInit(): void {
    const sliderNative = this.slider.nativeElement;
    const mouseDown$ = fromEvent(sliderNative, 'mousedown');
    const mouseLeave$ = fromEvent(sliderNative, 'mouseleave');
    const mouseUp$ = fromEvent(sliderNative, 'mouseup');
    const stop$ = merge(mouseLeave$, mouseUp$);
    const mouseMove$ = fromEvent(sliderNative, 'mousemove');

    this.active$ = merge(mouseDown$.pipe(map(() => true)), stop$.pipe(map(() => false)))
      .pipe(startWith(false));

    this.subscription = mouseDown$.pipe(
        filter((moveDownEvent) => moveDownEvent instanceof MouseEvent),
        map((moveDownEvent) => moveDownEvent as MouseEvent),
        concatMap((moveDownEvent) => {
          const startX = moveDownEvent.pageX - sliderNative.offsetLeft;
          const scrollLeft = sliderNative.scrollLeft;          
          return mouseMove$.pipe(
            filter((moveEvent) => moveEvent instanceof MouseEvent),
            map((moveEvent) => moveEvent as MouseEvent),
            tap((moveEvent) => moveEvent.preventDefault()),
            map((e) => {
              const x = e.pageX - sliderNative.offsetLeft;
              const walk = (x - startX) * 3;
              sliderNative.scrollLeft = scrollLeft - walk;
            }),
            takeUntil(stop$)
          );
        }),
      ).subscribe();
}
  • const mouseDown$ = fromEvent(sliderNative, ‘mousedown’) – listens to mousedown event of the div elements
  • const mouseLeave$ = fromEvent(sliderNative, ‘mouseleave’) – listens to mouseleave event of the div elements
  • const mouseUp$ = fromEvent(sliderNative, ‘mouseup’) – listens to mouseup event of the div elements
  • const mouseMove$ = fromEvent(sliderNative, ‘mousemove’) – listens to mousemove event of the div elements
  • const stop$ = merge(mouseLeave$, mouseUp$); – stop click-and-slide when mouse exits the browser or mouse up

Toggle active class

this.active$ = merge(
     mouseDown$.pipe(map(() => true)), stop$.pipe(map(() => false))
).pipe(startWith(false))
  • mouseDown$.pipe(map(() => true) – when mouse is clicked, add active class
  • stop$.pipe(map(() => false)) – when click and slide stops, remove active class
  • merge(mouseDown$.pipe(map(() => true)), stop$.pipe(map(() => false))) – merge multiple streams to toggle active class
  • startWith(false)) – the initial value of this.active$ observable is false

Click and Slide HTML elements

mouseDown$.pipe(
   filter((moveDownEvent) => moveDownEvent instanceof MouseEvent),
   map((moveDownEvent) => moveDownEvent as MouseEvent),
   concatMap((moveDownEvent) => {
      const startX = moveDownEvent.pageX - sliderNative.offsetLeft;
      const scrollLeft = sliderNative.scrollLeft;          
      return mouseMove$.pipe(
          filter((moveEvent) => moveEvent instanceof MouseEvent),
          map((moveEvent) => moveEvent as MouseEvent),
          tap((moveEvent) => moveEvent.preventDefault()),
          map((e) => {
              const x = e.pageX - sliderNative.offsetLeft;
              const walk = (x - startX) * 3;
              sliderNative.scrollLeft = scrollLeft - walk;
          }),
          takeUntil(stop$)
      );
   })
)
  • mouseDown$.pipe(…) – observe mousedown event
  • filter((moveDownEvent) => moveDownEvent instanceof MouseEvent) – filter event is MouseEvent
  • map((moveDownEvent) => moveDownEvent as MouseEvent) – cast event as MouseEvent
  • concatMap(…) – create a new Observable that flattens mouseMove$ inner Observables
  • mouseMove$.pipe(…) – when sliding occurs, I update the scrollLeft property of div element
  • tap((moveEvent) => moveEvent.preventDefault()) – invoke preventDefault method of the mouse event
  • takeUntil(stop$) – sliding stops when mouse exits browser or mouse up

Final Thoughts

In this post, I show how to use RxJS and Angular to click and slide div elements until mouse up or mouse leave occurs. fromEvent observes mousedown event and emits the event to concatMap to stream mousemove events. For each mousemove event, scrollLeft property of the div element is calculated and concatMap flattens the Observable. Then, the div element can scroll left or right.

Moreover, when mouse down occurs, the Observable emits true to add the active class to div element. Otherwise, the Observable emits false to remove the class.

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/day27-click-and-drag
  2. Live demo: https://railsstudent.github.io/ng-rxjs-30/day27-click-and-drag/
  3. Wes Bos’s JavaScript 30 Challenge: https://github.com/wesbos/JavaScript30