Click and slide HTML elements using RxJS and Angular

Reading Time: 5 minutes

 30 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

Sticky navigation bar after scroll using RxJS and Angular

Reading Time: 6 minutes

 22 total views

Introduction

This is day 23 of Wes Bos’s JavaScript 30 challenge and I am going to use RxJS and Angular to create a sticky navigation bar after window scrolls past the header. When the header is not in the viewport, I use RxJS behaviorSubject to update boolean flag to add CSS class to HTML element of child components. The CSS class creates sticky navigation bar in one child component and transforms the <div> container of another one.

In this blog post, I describe how to use RxJS fromEvent to listen to window scroll event, update inline style of body element and the behaviorSubject in StickyNav service. Angular components then observe the behaviorSubject, resolve the Observable in the inline template and toggle CSS class in ngClass property.

let's go

Create a new Angular project

ng generate application day24-sticky-nav

Add window service to listen to scroll event

In order to detect scrolling on native Window, I write a window service to inject to ScrollComponent to listen to scroll 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. */
export 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. */
const browserWindowProvider: ClassProvider = {
  provide: WindowRef,
  useClass: BrowserWindowRef
};

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

/* Create an array of providers. */
export const WINDOW_PROVIDERS = [
  browserWindowProvider,
  windowProvider
];

Then, we provide WINDOW injection token in CoreModule and import CoreModule to AppModule.

// core.module.ts

import { NgModule } from '@angular/core';

import { CommonModule } from '@angular/common';
import { WINDOW_PROVIDERS } from './services/window.service';

@NgModule({
  declarations: [],
  imports: [
    CommonModule
  ],
  providers: [WINDOW_PROVIDERS]
})
export class CoreModule { }
// app.module.ts

... other import statements ...
import { CoreModule } from './core';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    ... other imports ...
    CoreModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Create StickyNav feature module

First, we create a StickyNav feature module and import it into AppModule. The feature module encapsulates StickyNavPageComponent, StickyNavHeaderComponent and StickyNavContentComponent.

Import StickyNavModule in AppModule

// sticky-nav.module.ts

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { StickyNavContentComponent } from './sticky-nav-content/sticky-nav-content.component';
import { StickyNavHeaderComponent } from './sticky-nav-header/sticky-nav-header.component';
import { StickyNavPageComponent } from './sticky-nav-page/sticky-nav-page.component';

@NgModule({
  declarations: [
    StickyNavPageComponent,
    StickyNavHeaderComponent,
    StickyNavContentComponent
  ],
  imports: [
    CommonModule
  ],
  exports: [
    StickyNavPageComponent
  ]
})
export class StickyNavModule { }
// app.module.ts

import { AppComponent } from './app.component';
import { CoreModule } from './core';
import { StickyNavModule } from './sticky-nav';

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

Declare Sticky navigation bar components in feature module

In StickyNav feature module, we declare three Angular components, StickyNavPageComponent, StickyNavHeaderComponent and StickyNavContentComponent to build the application.

src/app
├── app.component.ts
├── app.module.ts
├── core
│   ├── core.module.ts
│   ├── index.ts
│   └── services
│       └── window.service.ts
└── sticky-nav
    ├── index.ts
    ├── services
    │   └── sticky-nav.service.ts
    ├── sticky-nav-content
    │   └── sticky-nav-content.component.ts
    ├── sticky-nav-header
    │   └── sticky-nav-header.component.ts
    ├── sticky-nav-page
    │   └── sticky-nav-page.component.ts
    └── sticky-nav.module.ts

StickyNavPageComponent acts like a shell that encloses StickyNavHeaderComponent and StickyNavContentComponent. For your information, <app-stick-nav-page> is the tag of StickyNavPageComponent.

// sticky-nav-page.component.ts

import { ChangeDetectionStrategy, Component } from '@angular/core';

@Component({
  selector: 'app-sticky-nav-page',
  template: `
    <ng-container>
      <app-sticky-nav-header></app-sticky-nav-header>
      <app-sticky-nav-content></app-sticky-nav-content>
    </ng-container>
  `,
  styles: [`
    :host {
      display: block;
    }
  `],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class StickyNavPageComponent {}

StickyNavHeaderComponent encapsulates a header image and a menu that turns into a sticky navigation bar. StickyNavContentComponent is consisted of several paragraphs and random images.

// sticky-nav-header.component.ts

import { ChangeDetectionStrategy, Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core';
import { Observable, of } from 'rxjs';
import { WINDOW } from '../../core';
import { StickyNavService } from '../services/sticky-nav.service';

@Component({
  selector: 'app-sticky-nav-header',
  template: `
    <header>
      <h1>A story about getting lost.</h1>
    </header>
    <nav id="main" #menu [ngClass]="{ 'fixed-nav': shouldFixNav$ | async }">
      <ul>
        <li class="logo"><a href="#">LOST.</a></li>
        <li><a href="#">Home</a></li>
        <li><a href="#">About</a></li>
        <li><a href="#">Images</a></li>
        <li><a href="#">Locations</a></li>
        <li><a href="#">Maps</a></li>
      </ul>
    </nav>
  `,
  styles: [`
    :host {
      display: block;
    }

    ... omitted due to brevity ...

    nav.fixed-nav {
      position: fixed;
      box-shadow: 0 5px 0 rgba(0,0,0,0.1);
    }
      
    .fixed-nav li.logo {
      max-width: 500px;
    }
  `],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class StickNavHeaderComponent implements OnInit {
  @ViewChild('menu', { static: true, read: ElementRef })
  nav!: ElementRef<HTMLElement>;

  shouldFixNav$!: Observable<boolean>;

  constructor(@Inject(WINDOW) private window: Window, private service: StickyNavService) { }

  ngOnInit(): void {
    this.shouldFixNav$ = of(true);
  }
}

shouldFixNav$ is a boolean Observable, it resolves and toggles the fixed-nav CSS class of the <nav> element. When the class is found in the element , header image disappears and the navigation bar is positioned at the top of the page.

// sticky-nav-content.component.ts

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { StickyNavService } from '../services/sticky-nav.service';

@Component({
  selector: 'app-sticky-nav-content',
  template: `
    <div class="site-wrap" [ngClass]="{ 'fixed-nav': shouldFixNav$ | async }">
      ... paragraphs and images are omitted due to brevity...
    </div>
  `,
  styles: [`
    :host {
      display: block;
    }

    .fixed-nav.site-wrap {
      transform: scale(1);
      border: 2px solid black;
    }

    ... omitted due to brevity ...
  `],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class StickNavContentComponent {
  shouldFixNav$ = this.service.shouldFixNav$;

  constructor(private service: StickyNavService) { }
}

Similar to StickyNavHeaderComponent, StickNavContentComponent performs scale transformation and adds border around the <div> container when the container includes fixed-nav CSS class.

// sticky-nav.service.ts

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

@Injectable({
  providedIn: 'root'
})
export class StickyNavService {
  private readonly shouldFixNavSub = new BehaviorSubject<boolean>(false);
  readonly shouldFixNav$ = this.shouldFixNavSub.asObservable();

  addClass(value: boolean) {
    this.shouldFixNavSub.next(value);
  }
}

StickNavContentComponent injects StickyNavService service to the constructor to share the boolean Observable. StickyNavService creates a new Observable using shouldFixNavSub as the source. The inline template resolves the Observable in order to toggle the CSS class.

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

// app.component.ts

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

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

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

Use RxJS and Angular to implement StickyNavHeaderComponent

I am going to rewrite shouldFixNav$ and update the BehaviorSubject in tap.

Use ViewChild to obtain reference to nav element

@ViewChild('menu', { static: true, read: ElementRef })
nav!: ElementRef<HTMLElement>;
// stick-nav-header.component.ts

ngOnInit(): void {
    const navNative = this.nav.nativeElement;
    const body = navNative.closest('body');

    this.shouldFixNav$ = fromEvent(this.window, 'scroll')
      .pipe(
        map(() => this.window.scrollY > navNative.offsetTop),
        tap((result) => {
          if (body) {
            body.style.paddingTop = result ? `${navNative.offsetHeight}px` : '0';
          }            
          this.service.addClass(result);
        }),
        startWith(false)
      );
}
  • fromEvent(this.window, ‘scroll’) – listens to scroll event of the window
  • map(() => this.window.scrollY > navNative.offsetTop) – determines whether or not the navigation bar is out of the viewport
  • tap() – update paddingTop property of the body element and the BehaviorSubject in StickyNavService
  • startWith(false) – emits the initial value of the Observable

Final Thoughts

In this post, I show how to use RxJS and Angular to create a sticky navigation bar. fromEvent observes window scrolling to detect when the navigation bar is not in the viewport. When it occurs, the Observable emits true to add the fixed-nav class to HTML elements. Otherwise, the Observable emits false to remove the class and revert the sticky effect.

The header component communicates with the shared service to toggle the value of the BehaviorSubject. Sibling content component injects the service to obtain the Observable to toggle the class of the div container.

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

Follow along link highlighter using RxJS and Angular

Reading Time: 7 minutes

 13 total views

Introduction

This is day 22 of Wes Bos’s JavaScript 30 challenge and I am going to use RxJS and Angular to create a highlighter that follows a link when cursor hovers it. The follow along link highlighter updates CSS width, height and transform when mouseenter event occurs on the links of Angular components.

In this blog post, I describe how to use RxJS fromEvent to listen to mouseenter event of anchor elements and update the behaviorSubject in Highlighter service. Angular components observe the behaviorSubject and emit CSS width, height and transform to an Observable stream. The stream resolves in the inline template by async pipe and the follow along link highlighter effect occurs.

let's go

Create a new Angular project

ng generate application day22-follow-along-link-highlighter

Create Highlighter feature module

First, we create a Highlighter feature module and import it into AppModule. The feature module encapsulates HighlighterPageComponent, HighlighterMenuComponent, HighlighterContentComponent and HighlightAnchorDirective.

Import HighlighterhModule in AppModule

// highlighter.module.ts

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { HighlightAnchorDirective } from './directives/highlight-anchor.directive';
import { HighlighterContentComponent } from './highlighter-content/highlighter-content.component';
import { HighlighterMenuComponent } from './highlighter-menu/highlighter-menu.component';
import { HighlighterPageComponent } from './highlighter-page/highlighter-page.component';

@NgModule({
  declarations: [
    HighlighterPageComponent,
    HighlightAnchorDirective,
    HighlighterMenuComponent,
    HighlighterContentComponent,
  ],
  imports: [
    CommonModule
  ],
  exports: [
    HighlighterPageComponent
  ]
})
export class HighlighterModule { }

// app.module.ts

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

import { AppComponent } from './app.component';
import { CoreModule } from './core';
import { HighlighterModule } from './highlighter';

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

Declare Highlighter components in feature module

In Highlighter feature module, we declare three Angular components, HighlighterPageComponent, HighlighterMenuComponent and HighlighterContentComponent to build the application.

src/app
├── app.component.ts
├── app.module.ts
├── core
│   ├── core.module.ts
│   ├── index.ts
│   └── services
│       └── window.service.ts
└── highlighter
    ├── directives
    │   └── highlight-anchor.directive.ts
    ├── helpers
    │   └── mouseenter-stream.helper.ts
    ├── highlighter-content
    │   └── highlighter-content.component.ts
    ├── highlighter-menu
    │   └── highlighter-menu.component.ts
    ├── highlighter-page
    │   └── highlighter-page.component.ts
    ├── highlighter.interface.ts
    ├── highlighter.module.ts
    ├── index.ts
    └── services
        └── highlighter.service.ts

HighlighterPageComponent acts like a shell that encloses HighlighterMenuComponent and HighlighterContentComponent. For your information, <app-highlighter-page> is the tag of HighlighterPageComponent.

// highlighter-page.component.ts

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { HighlighterService } from '../services/highlighter.service';

@Component({
  selector: 'app-highlighter-page',
  template: `
    <ng-container>    
      <app-highlighter-menu></app-highlighter-menu>
      <app-highlighter-content></app-highlighter-content>
      <ng-container *ngIf="highlightStyle$ | async as hls">
        <span class="highlight" [ngStyle]="hls"></span>
      </ng-container>
    </ng-container>
  `,
  styles: [`...omitted due to brevity ...`],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class HighlighterPageComponent {
  highlightStyle$ = this.highlighterService.highlighterStyle$

  constructor(private highlighterService: HighlighterService) {}
}

HighlighterService is a simple service that stores CSS width, height and transform of follow along link highlighter in a BehaviorSubject.

// highlighter.interface.ts
export interface HighlighterStyle {
    width: string,
    height: string,
    transform: string,
}

// highlighter.service.ts

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { HighlighterStyle } from '../highlighter.interface';

@Injectable({
  providedIn: 'root'
})
export class HighlighterService {
  private readonly highlighterStyleSub = new BehaviorSubject<HighlighterStyle>({
      width: '0px',
      height: '0px',
      transform: ''
  });
  readonly highlighterStyle$ = this.highlighterStyleSub.asObservable();

  updateStyle(style: HighlighterStyle) {
    this.highlighterStyleSub.next(style);
  }
}

HighlighterMenuComponent encapsulates a menu and each menu item encloses an anchor element whereas HighlighterContentComponent is consisted of several paragraphs with 19 embedded anchor elements.

// highlighter-menu.component.ts

import { ChangeDetectionStrategy, Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Subscription } from 'rxjs';
import { WINDOW } from '../../core';
import { createMouseEnterStream } from '../helpers/mouseenter-stream.helper';
import { HighlighterService } from '../services/highlighter.service';

@Component({
  selector: 'app-highlighter-menu',
  template: `
    <nav>
      <ul class="menu">
        <li><a href="" #home>Home</a></li>
        <li><a href="" #order>Order Status</a></li>
        <li><a href="" #tweet>Tweets</a></li>
        <li><a href="" #history>Read Our History</a></li>
        <li><a href="" #contact>Contact Us</a></li>
      </ul>
    </nav>
  `,
  styles: [`...omitted due to brevity...`],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class HighlighterMenuComponent implements OnInit, OnDestroy {
  @ViewChild('home', { static: true, read: ElementRef })
  home!: ElementRef<HTMLAnchorElement>;

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

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

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

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

  subscription!: Subscription;

  constructor(private highlighterService: HighlighterService, @Inject(WINDOW) private window: Window) {}

  ngOnInit(): void {}

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
}
// highlighter-content.component.ts

import { AfterViewInit, ChangeDetectionStrategy, Component, Inject, OnDestroy, QueryList, ViewChildren } from '@angular/core';
import { Subscription } from 'rxjs';
import { WINDOW } from '../../core';
import { HighlightAnchorDirective } from '../directives/highlight-anchor.directive';
import { createMouseEnterStream } from '../helpers/mouseenter-stream.helper';
import { HighlighterService } from '../services/highlighter.service';

@Component({
  selector: 'app-highlighter-content',
  template: `
    <div class="wrapper">
      <p>Lorem ipsum dolor sit amet, <a href="">consectetur</a> adipisicing elit. Est <a href="">explicabo</a> unde natus necessitatibus esse obcaecati distinctio, aut itaque, qui vitae!</p>
      <p>Aspernatur sapiente quae sint <a href="">soluta</a> modi, atque praesentium laborum pariatur earum <a href="">quaerat</a> cupiditate consequuntur facilis ullam dignissimos, aperiam quam veniam.</p>
      <p>Cum ipsam quod, incidunt sit ex <a href="">tempore</a> placeat maxime <a href="">corrupti</a> possimus <a href="">veritatis</a> ipsum fugit recusandae est doloremque? Hic, <a href="">quibusdam</a>, nulla.</p>
      <p>Esse quibusdam, ad, ducimus cupiditate <a href="">nulla</a>, quae magni odit <a href="">totam</a> ut consequatur eveniet sunt quam provident sapiente dicta neque quod.</p>
      <p>Aliquam <a href="">dicta</a> sequi culpa fugiat <a href="">consequuntur</a> pariatur optio ad minima, maxime <a href="">odio</a>, distinctio magni impedit tempore enim repellendus <a href="">repudiandae</a> quas!</p>
    </div>
  `,
  styles: [`...omitted due to brevty...`],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class HighlighterContentComponent implements AfterViewInit, OnDestroy {

  @ViewChildren(HighlightAnchorDirective)
  anchors!: QueryList<HighlightAnchorDirective>;

  subscription!: Subscription;

  constructor(private highlighterService: HighlighterService, @Inject(WINDOW) private window: Window) { }

  ngAfterViewInit(): void {}

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

There are 19 anchor elements in this component; it is tedious to include template reference variables and reference them by 19 @ViewChild decorators. Therefore, I declare a HighlightAnchorDirective and pass the directive to @ViewChildren decorator to obtain all references to anchor elements.

// highlight-anchor.directive.ts

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

@Directive({
  selector: 'a'
})
export class HighlightAnchorDirective {
  nativeElement!: HTMLAnchorElement;
  constructor(el: ElementRef<HTMLAnchorElement>) { 
    this.nativeElement = el.nativeElement;
  }
}

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

// app.component.ts

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

@Component({
  selector: 'app-root',
  template: `<app-highlighter-page></app-highlighter-page>`,
  styles: [`
    :host {
      display: block;
    }
  `],
})
export class AppComponent {
  title = '👀👀👀 Day 22 Follow along link highlighter';

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

Add window service to listen to scroll event

In order to detect scrolling on native Window, I write a window service to inject to ScrollComponent to listen to scroll 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. */
export 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. */
const browserWindowProvider: ClassProvider = {
  provide: WindowRef,
  useClass: BrowserWindowRef
};

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

/* Create an array of providers. */
export const WINDOW_PROVIDERS = [
  browserWindowProvider,
  windowProvider
];

Then, we provide WINDOW injection token in CoreModule and import CoreModule to AppModule.

// core.module.ts

import { NgModule } from '@angular/core';

import { CommonModule } from '@angular/common';
import { WINDOW_PROVIDERS } from './services/window.service';

@NgModule({
  declarations: [],
  imports: [
    CommonModule
  ],
  providers: [WINDOW_PROVIDERS]
})
export class CoreModule { }
// app.module.ts

... other import statements ...
import { CoreModule } from './core';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    ... other imports ...
    CoreModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Create helper to make mouseenter stream

Both HighlighterMenuComponent and HighlighterContentComponent listen to mouseenter event to order to emit CSS properties the follow along link highlighter. Therefore, I create a function that accepts HTMLAnchorElements and returns a merge stream of mouseenter events.

import { ElementRef, QueryList } from '@angular/core';
import { fromEvent, map, merge } from 'rxjs';
import { HighlightAnchorDirective } from '../directives/highlight-anchor.directive';

export function createMouseEnterStream(
  elementRefs: ElementRef<HTMLAnchorElement>[] | QueryList<HighlightAnchorDirective>, 
  window: Window
) {
    const mouseEnter$ = elementRefs.map(({ nativeElement }) => 
      fromEvent(nativeElement, 'mouseenter')
        .pipe(
          map(() => {
            const linkCoords = nativeElement.getBoundingClientRect();
            return {
              width: linkCoords.width,
              height: linkCoords.height,
              top: linkCoords.top + window.scrollY,
              left: linkCoords.left + window.scrollX
            };
          })
        ));

    return merge(...mouseEnter$)
      .pipe(
        map((coords) => ({
          width: `${coords.width}px`,
          height: `${coords.height}px`,
          transform: `translate(${coords.left}px, ${coords.top}px)`
        })),
      );    
 }
  • fromEvent(nativeElement, ‘mouseenter’) – listens to mouseenter event of anchor element
  • map – finds the dimensions and top-left point of the anchor element

elementRefs maps to mouseEnter$ that is an array of Observable

  • merge(…mouseEnter$) – merges mouseenter Observables
  • map – returns CSS width, height and transform of the anchor element

Use RxJS and Angular to implement HighlighterMenuComponent

I am going to define an Observable, subscribe it and update the BehaviorSubject in the service.

Use ViewChild to obtain references to anchor elements

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

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

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

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

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

Declare subscription instance member and unsubscribe in ngDestroy()

subscription = new Subscription();

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

Subscribe the observable and update BehaviorSubject in HighlighterService.

// highlighter-menu.component.ts

ngOnInit(): void {
    this.subscription = createMouseEnterStream(
        [this.home, this.order, this.tweet, this.history, this.contact], 
        this.window
    ).subscribe((style) => this.highlighterService.updateStyle(style));
}

Use RxJS and Angular to implement HighlighterContentComponent

Use ViewChildren to obtain references to anchor elements

@ViewChildren(HighlightAnchorDirective)
anchors!: QueryList<HighlightAnchorDirective>;

Declare subscription instance member and unsubscribe in ngDestroy()

subscription = new Subscription();

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

Create mouseenter stream, subscribe it and update the CSS styles to the BehaviorSubject in the service.

// highlighter-content.component.ts

ngAfterViewInit(): void {
    this.subscription = createMouseEnterStream(this.anchors, this.window)
      .subscribe((style) => this.highlighterService.updateStyle(style));
}

The subject invokes HighlighterService to update the BehaviorSubject.

Move the highlighter in HighlighterPageComponent

In HighlighterPageComponent, the constructor injects HighlighterService and I assign this.highlighterService.highlighterStyle$ to highlightStyle$ instance member.

highlightStyle$ = this.highlighterService.highlighterStyle$

In inline template, async pipe resolves highlightStyle$ and updates CSS styles of <span> element. Then, the span element highlights the text of the hovered anchor element.

// highlighter-page.component.ts

<ng-container *ngIf="highlightStyle$ | async as hls">
    <span class="highlight" [ngStyle]="hls"></span>
</ng-container>

Final Thoughts

In this post, I show how to use RxJS and Angular to build a highlighter that moves to the hovered anchor element. Child components create Observables to pass CSS properties to shared HighlighterService. Parent component observes the observable and updates the CSS styles of the span element to produce the effect.

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: Source code
  2. Live demo: Demo in Github Page
  3. Wes Bos’s JavaScript 30 Challenge: https://github.com/wesbos/JavaScript30

Text to speech tutorial using RxJS and Angular

Reading Time: 6 minutes

 49 total views

Introduction

This is day 23 of Wes Bos’s JavaScript 30 challenge and I am going to use RxJS and Angular to create an English text to speech tutorial. The Web Speech API provides interfaces to make speech request and turn text into speech according to the selected voice.

In this blog post, I describe how to use RxJS fromEvent to listen to change event of input controls and update the properties of SpeechSynthesisUtterance object. SpeechSynthesisUtterance interface creates a speech request and calls SpeechSynthesis interface to speak the text and turn the English text to speech.

let's go

Create a new Angular project

ng generate application day23-speech-synthesis

Create Speech feature module

First, we create a Speech feature module and import it into AppModule. The feature module encapsulates SpeechSynthesisComponent, SpeechTextComponent and SpeechVoiceComponent.

Import SpeechModule in AppModule

// speech.module.ts

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { SpeechSynthesisComponent } from './speech-synthesis/speech-synthesis.component';
import { SpeechTextComponent } from './speech-text/speech-text.component';
import { SpeechVoiceComponent } from './speech-voice/speech-voice.component';

@NgModule({
  declarations: [
    SpeechSynthesisComponent,
    SpeechVoiceComponent,
    SpeechTextComponent
  ],
  imports: [
    CommonModule,
    FormsModule,
  ],
  exports: [
    SpeechSynthesisComponent
  ]
})
export class SpeechModule { }

// app.module.ts

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

import { AppComponent } from './app.component';
import { SpeechModule } from './speech';

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

Declare Speech components in feature module

In Speech feature module, we declare three Angular components, SpeechSynthesisComponent, SpeechTextComponent and SpeechVoiceComponent to build an English text to speech application.

src/app
├── app.component.ts
├── app.module.ts
└── speech
    ├── index.ts
    ├── interfaces
    │   └── speech.interface.ts
    ├── services
    │   └── speech.service.ts
    ├── speech-synthesis
    │   └── speech-synthesis.component.ts
    ├── speech-text
    │   └── speech-text.component.ts
    ├── speech-voice
    │   └── speech-voice.component.ts
    └── speech.module.ts

SpeechComponent acts like a shell that encloses SpeechTextComponent and SpeechVoiceComponent. For your information, <app-speech-synthesis> is the tag of SpeechComponent.

// speech-synthesis.component.ts

import { ChangeDetectionStrategy, Component } from '@angular/core';

@Component({
  selector: 'app-speech-synthesis',
  template: `
    <div class="voiceinator">
      <h1>The Voiceinator 5000</h1>
      <app-speech-voice></app-speech-voice>
      <app-speech-text></app-speech-text>
    </div>`,
  styles: [` ...omitted due to brevity... `],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SpeechSynthesisComponent {}

SpeehVoiceComponent encapsulates input controls to change the rate, pitch and voice of the speech whereas SpeechTextComponent is composed of text area, speak and stop buttons to decide what and when to speak.

// speech-voice.component.ts

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

@Component({
  selector: 'app-speech-voice',
  template: `
    <ng-container>
      <select name="voice" id="voices" #voices>
        <option *ngFor="let voice of voices$ | async" [value]="voice.name">{{voice.name}} ({{voice.lang}})</option>
      </select>
      <label for="rate">Rate:</label>
      <input name="rate" type="range" min="0" max="3" value="1" step="0.1" #rate>
      <label for="pitch">Pitch:</label>
      <input name="pitch" type="range" min="0" max="2" step="0.1" #pitch value="1">
    </ng-container>
  `,
  styles: [...omitted due to brevity...],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SpeechVoiceComponent implements OnInit, OnDestroy {
  @ViewChild('rate', { static: true, read: ElementRef })
  rate!: ElementRef<HTMLInputElement>;

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

  @ViewChild('voices', { static: true, read: ElementRef })
  voiceDropdown!: ElementRef<HTMLSelectElement>;
  
  voices$!: Observable<SpeechSynthesisVoice[]>;

  constructor(private speechService: SpeechService) { }

  ngOnInit(): void {
    this.voices$ = of([]);
  }

  ngOnDestroy(): void {}
}
// speech-text.component.ts

import { ChangeDetectionStrategy, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Subject, Subscription, fromEvent, map, merge, tap } from 'rxjs';
import { SpeechService } from '../services/speech.service';

@Component({
  selector: 'app-speech-text',
  template: `
    <ng-container>
      <textarea name="text" [(ngModel)]="msg" (change)="textChanged$.next()"></textarea>
      <button id="stop" #stop>Stop!</button>
      <button id="speak" #speak>Speak</button>
    </ng-container>
  `,
  styles: [...omitted due to brevity...],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SpeechTextComponent implements OnInit, OnDestroy {
  @ViewChild('stop', { static: true, read: ElementRef })
  btnStop!: ElementRef<HTMLButtonElement>;

  @ViewChild('speak', { static: true, read: ElementRef })
  btnSpeak!: ElementRef<HTMLButtonElement>;

  textChange$ = new Subject<void>();
  msg = 'Hello! I love JavaScript 👍';

  constructor(private speechService: SpeechService) { }

  ngOnInit(): void {
    this.speechService.updateSpeech({ name: 'text', value: this.msg });
  }

  ngOnDestroy(): void {}
}

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

// app.component.ts

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

@Component({
  selector: 'app-root',
  template: '<app-speech-synthesis></app-speech-synthesis>',
  styles: [`
    :host {
      display: block;
    }
  `]
})
export class AppComponent {
  title = 'Day 23 Speech Synthesis';

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

Add speech service to implement text to speech

I create a shared service to add a layer on top of Web Speech API to make speech request and speak the texts according to the selected voice, rate and pitch.

// speech.service.ts

import { Injectable } from '@angular/core';
import { SpeechProperties } from '../interfaces/speech.interface';

@Injectable({
  providedIn: 'root'
})
export class SpeechService {
  private voices: SpeechSynthesisVoice[] = [];

  updateSpeech(property: SpeechProperties): void {
    const { name, value } = property;
    if ((name === 'text')) {
      localStorage.setItem(name, value);
    } else if (['rate', 'pitch'].includes(name)) {
      localStorage.setItem(name, `${value}`);
    }
    this.toggle();
  }

  setVoices(voices: SpeechSynthesisVoice[]): void {
    this.voices = voices;
  }

  updateVoice(voiceName: string): void {
    localStorage.setItem('voice', voiceName);
    this.toggle();
  }

  private findVoice(voiceName: string): SpeechSynthesisVoice | null {
    const voice = this.voices.find(v => v.name === voiceName);
    return voice ? voice : null;
  }

  toggle(startOver = true): void {
    const speech = this.makeRequest();
    speechSynthesis.cancel();
    if (startOver) {
      speechSynthesis.speak(speech);
    }
  }

  private makeRequest() {
    const speech = new SpeechSynthesisUtterance();
    speech.text = localStorage.getItem('text') || '';
    speech.rate = +(localStorage.getItem('rate') || '1');
    speech.pitch = +(localStorage.getItem('pitch') || '1');
    const voice = this.findVoice(localStorage.getItem('voice') || '');
    if (voice) {
      speech.voice = voice;
    }
    return speech;
  }
}
  • updateSpeech updates pitch, rate or text in local storage
  • setVoices stores English voices in internal member of SpeechService
  • findVoice find voice by voice name
  • updateVoice updates voice name in local storage
  • makeRequest loads the property values from local storage and creates a SpeechSynthesisUtternce request
  • toggle ends and speaks the text again

Use RxJS and Angular to implement SpeechVoiceComponent

I am going to define an Observable to retrieve English voices and populate voices dropdown.

Use ViewChild to obtain references to input ranges and voice dropdown

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

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

@ViewChild('voices', { static: true, read: ElementRef })
voiceDropdown!: ElementRef<HTMLSelectElement>;

Declare subscription instance member and unsubscribe in ngDestroy()

subscription = new Subscription();

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

Declare voices$ Observable and populate options in voices dropdown in ngOnInit().

ngOnInit(): void {
    this.voices$ = fromEvent(speechSynthesis, 'voiceschanged')
       .pipe(
          map(() => speechSynthesis.getVoices().filter(voice => voice.lang.includes('en'))),
          tap((voices) => this.speechService.setVoices(voices)),
       );
}

In inline template, use async pipe to resolve this.voices$ and populate options in voices dropdown

<select name="voice" id="voices" #voices>
     <option *ngFor="let voice of voices$ | async" [value]="voice.name">{{voice.name}} ({{voice.lang}})</option>
</select>

Use fromEvent to listen to change event of the dropdown, update voice name in local storage and speak the texts.

const voiceDropdownNative = this.voiceDropdown.nativeElement;
this.subscription.add(
   fromEvent(this.voiceDropdown.nativeElement, 'change')
     .pipe(
        tap(() => this.speechService.updateVoice(voiceDropdownNative.value))
     ).subscribe()
);

Similarly, use fromEvent to listen to change event of input ranges, update rate and pitch in local storage and speak the texts

const rateNative = this.rate.nativeElement;
const pitchNative = this.pitch.nativeElement;
this.subscription.add(
      merge(fromEvent(rateNative, 'change'), fromEvent(pitchNative, 'change'))
        .pipe(
          map((e) => e.target as HTMLInputElement),
          map((e) => ({ name: e.name as 'rate' | 'pitch', value: e.value })),
          tap((property) => this.speechService.updateSpeech(property))
      ).subscribe()
);

Use RxJS and Angular to implement SpeechTextComponent

Use ViewChild to obtain references to text area and buttons

@ViewChild('stop', { static: true, read: ElementRef })
btnStop!: ElementRef<HTMLButtonElement>;

@ViewChild('speak', { static: true, read: ElementRef })
btnSpeak!: ElementRef<HTMLButtonElement>;

Declare subscription instance member and unsubscribe in ngDestroy()

subscription = new Subscription();

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

Declare textChanged$ subject that emits value when text changes in text area and tab out to lose focus.

// speech-text.component.ts

textChanged$ = new Subject<void>();
  
ngOnInit() {
    this.subscription.add(
       this.textChanged$
         .pipe(tap(() => this.speechService.updateSpeech({ name: 'text', value: this.msg })))
         .subscribe()
    );
}

The subject invokes SpeechService to update the message in local storage and say the texts.

Similarly, use fromEvent to listen to click event of buttons. When speak button is clicked, the stream stops and starts saying the text in the text area. When cancel button is clicked, the stream stops the speech immediately.

ngOnInit() {
    const btnStop$ = fromEvent(this.btnStop.nativeElement, 'click').pipe(map(() => false));
    const btnSpeak$ = fromEvent(this.btnSpeak.nativeElement, 'click').pipe(map(() => true));
    this.subscription.add(
      merge(btnStop$, btnSpeak$)
        .pipe(tap(() => this.speechService.updateSpeech({ name: 'text', value: this.msg })))
        .subscribe((startOver) => this.speechService.toggle(startOver))
    );
}
  • fromEvent(this.btnStop.nativeElement, 'click').pipe(map(() => false)) – stop button maps to false to end the speech immediately
  • fromEvent(this.btnSpeak.nativeElement, 'click').pipe(map(() => true)) – speak button maps true to stop and restart the speech

The example is done and we have a page that comprehends English words and able to say them.

Final Thoughts

In this post, I show how to use RxJS and Angular to build a a text to speech application that reads and speak English texts. Web Speech API supports various spoken languages and voices to say texts with different parameters (rate, pitch, text and volume). Child components create Observables to pass values to shared SpeechService to update local storage and make new speech request to speak.

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. Repo: https://github.com/railsstudent/ng-rxjs-30/tree/main/projects/day23-speech-synthesis
  2. Live demo: https://railsstudent.github.io/ng-rxjs-30/day23-speech-synthesis/
  3. Wes Bos’s JavaScript 30 Challenge: https://github.com/wesbos/JavaScript30

Build video speed controller using RxJS and Angular

Reading Time: 4 minutes

 27 total views

Introduction

This is day 28 of Wes Bos’s JavaScript 30 challenge and I am going to use RxJS and Angular to build a video speed controller.

In this blog post, I describe how to compose a RxJS stream to listen to mousemove event of video speed bar and emit value to two other streams to update 1) CSS height of the bar and 2) display the formatted playback rate.

let's go

Create a new Angular project

ng generate application day28-video-speed-controller

Create Video feature module

First, we create a video feature module and import it into AppModule. The feature module encapsulates VideoPlayerComponent that is the host of a video player and a speed bar.

Import VideoModule in AppModule

// video.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { VideoPlayerComponent } from './video-player/video-player.component';

@NgModule({
  declarations: [
    VideoPlayerComponent
  ],
  imports: [
    CommonModule
  ],
  exports: [
    VideoPlayerComponent
  ]
})
export class VideoModule { }
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { VideoPlayerComponent } from './video-player/video-player.component';

@NgModule({
  declarations: [
    VideoPlayerComponent
  ],
  imports: [
    CommonModule
  ],
  exports: [
    VideoPlayerComponent
  ]
})
export class VideoModule { }

// app.module.ts 

import { APP_BASE_HREF, PlatformLocation } from '@angular/common';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { VideoModule } from './video'

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    VideoModule
  ],
  providers: [
    {
      provide: APP_BASE_HREF,
      useFactory: (platformLocation: PlatformLocation) => platformLocation.getBaseHrefFromDOM(),
      deps: [PlatformLocation]
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Declare Video component in feature module

In Video feature module, we declare VideoPlayerComponent and implement the logic to control the playback rate of the video player.

src/app/
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
└── video
    ├── index.ts
    ├── video.module.ts
    └── video-player
        ├── video-player.component.spec.ts
        └── video-player.component.tspa
// video-player.component.ts

import { APP_BASE_HREF } from '@angular/common';
import { ChangeDetectionStrategy, Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core';
import { filter, fromEvent, map, Observable, shareReplay, startWith, tap } from 'rxjs';

@Component({
  selector: 'app-video-player',
  template: `
    <div class="wrapper">
      <video class="flex" width="765" height="430" [src]="videoSrc" loop controls #video></video>
      <div class="speed" #speed>
        <div class="speed-bar" [style.height]="height$ | async">{{ playbackRate$ | async }}</div>
      </div>
    </div>
  `,
  styles: [`...omitted due to brevity....`],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class VideoPlayerComponent implements OnInit {

  height$!: Observable<string>;
  playbackRate$!: Observable<string>;

  constructor(@Inject(APP_BASE_HREF) private baseHref: string) {}

  get videoSrc(): string {
    const isEndWithSlash = this.baseHref.endsWith('/');
    return `${this.baseHref}${isEndWithSlash ? '' : '/'}assets/video.mp4`;
  }

  ngOnInit(): void {
    this.height$ = of('');    
    this.playbackRate$ = of('1x');
  }
}

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

// app.component.ts

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

@Component({
  selector: 'app-root',
  template: '<app-video-player></app-video-player>',
  styles: [`
  :host {
    display: block;
  }
  `]
})
export class AppComponent {
  title = 'Day28 Video Speed Controller';

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

Implement RxJS stream to listen to mousemove event

Use ViewChild to obtain references to the video player and the speed bar

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

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

In ngOnit, I declare an observable to listen to mousemove event.

ngOnInit(): void {
    const nativeElement = this.speed.nativeElement;

    const mouseMove$ = fromEvent(nativeElement, 'mousemove')
      .pipe(
        filter((e) => e instanceof MouseEvent),
        map((e) => e as MouseEvent),
        map((e) => {
          const y = e.pageY - nativeElement.offsetTop;
          const percent = y / nativeElement.offsetHeight;
          const min = 0.4;
          const max = 4;
          return {
            height: `${Math.round(percent * 100)}%`,
            playbackRate: percent * (max - min) + min,
          };
        }),
        tap(({ playbackRate }) => this.video.nativeElement.playbackRate = playbackRate),
        shareReplay(1),
      );
 }

After deriving height and playback rate, the tap operator updates the playback rate of the video player.

Use shareReplay to cache height and playbackRate because moveMove$ source Observable will emit result to this.height$ and this.playbackRate$ later.

Complete height$ and playbackRate$ observables

Currently, height$ and playbackRate$ are Observables with hardcoded strings. It is going to change when mouseMove$ becomes their source Observable.

this.height$ applies map operator to extract height property from the object.

this.height$ = mouseMove$.pipe(map(({ height }) => height));

Use async pipe to resolve this.height$ and bind the value to CSS height.

<div class="speed-bar" [style.height]="height$ | async"></div>

this.playbackRate$ applies map to format the playback rate and startWith('1x') provides the initial value when mousemove event has not fired yet.

this.playbackRate$ = mouseMove$.pipe(
    map(({ playbackRate }) => `${playbackRate.toFixed(2)}x`),
    startWith('1x')
);

Use async pipe to resolve this.playbackRate$ and display the string inside the div element of the inline template

<div class="speed-bar" [style.height]="height$ | async">{{ playbackRate$ | async }}</div>

The example is done and we build a video speed controller that plays the video at different speed.

Final Thoughts

In this post, I show how to use RxJS and Angular to build video speed controller. One nice thing about this example is all Observables are reactive and do not require manual subscribe. Therefore, this example does not have ngOnDestroy method to unsubcribe any subscription. Async pipe is responsible to subscribe the observables and clean up the subscriptions automatically.

The second cool thing is mousemove$ observable leverages tap operator to update video player’s playback rate. Otherwise, I have to perform the update in subscribe and create a subscription that requires clean up. When mouseMove$ is not an observable, it cannot build height$ and playbackRate$ Observables and subscribe in the inline template.

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. Repo: https://github.com/railsstudent/ng-rxjs-30/tree/main/projects/day28-video-speed-controller
  2. Live demo: https://railsstudent.github.io/ng-rxjs-30/day28-video-speed-controller/
  3. Wes Bos’s JavaScript 30 Challenge: https://github.com/wesbos/JavaScript30

Create whack a mole game using RxJS and Angular

Reading Time: 8 minutes

 20 total views

Introduction

This is day 30 of Wes Bos’s JavaScript 30 challenge and I am going to use RxJS and Angular to create whack a mole game. The whack a mole game defines multiple streams to perform many UI tasks at the same time:

  • wait 3 seconds before the game starts and the first mole appears
  • build a game loop to show a mole at random hole and random time
  • increment score when mole is clicked
  • finish the game after 10 seconds

In this blog post, I describe how to create RXJS streams to handle intensive UI tasks described above in a step-by-step manner. Ultimately, we create whack a mole game that does not require to write a lot of codes and RxJS custom operators.

let's go

Create a new Angular project

ng generate application day30-whack-a-mole

Create Game feature module

First, we create a game feature module and import it into AppModule. The feature module encapsulates MoleComponent, WhackAMoleMessagePipe and RemainingTimePipe.

Import GameModule in AppModule

// game.module.ts

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MoleComponent } from './mole/mole.component';
import { RemainingTimePipe, WhackAMoleMessagePipe } from './pipes';

@NgModule({
  declarations: [
    MoleComponent,
    WhackAMoleMessagePipe,
    RemainingTimePipe
  ],
  imports: [
    CommonModule
  ],
  exports: [
    MoleComponent
  ]
})
export class GameModule { }

import { APP_BASE_HREF, PlatformLocation } from '@angular/common';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { GameModule } from './game';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    GameModule
  ],
  providers: [
    {
      provide: APP_BASE_HREF,
      useFactory: (platformLocation: PlatformLocation) => platformLocation.getBaseHrefFromDOM(),
      deps: [PlatformLocation]
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Declare Game components in feature module

In Game feature module, we declare component, pipes and RxJS custom operators to create whack a mole game. All game logic happens in MoleComponent and the component calls RemainingTimePipe and WhackAMoleMessagePipe to render messages. To make RxJS streams shorter and more declarative to comprehend, RxJS logic is refactored into custom operators, peep, trackGameTime and whackAMole respectively.

src/app
├── app.component.ts
├── app.module.ts
└── game
    ├── custom-operators
    │   ├── index.ts
    │   ├── peep.operator.ts
    │   ├── time-tracker.operator.ts
    │   └── whack-a-mole.operator.ts
    ├── game.module.ts
    ├── index.ts
    ├── mole
    │   ├── mole.component.scss
    │   ├── mole.component.ts
    │   └── mole.enum.ts
    └── pipes
        ├── index.ts
        ├── remaining-time.pipe.ts
        └── whack-a-mole-message.pipe.ts

MoleComponent is the centerpiece of this game and the component tag is <app-mole>.

// mole.component.ts

import { APP_BASE_HREF } from '@angular/common';
import { ChangeDetectionStrategy, Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { BehaviorSubject, Observable, Subscription, concatMap, delay, fromEvent, map, merge, scan, shareReplay, startWith, take, takeUntil, timer } from 'rxjs';
import { peep, trackGameTime, whackAMole } from '../custom-operators';
import { SCORE_ACTION } from './mole.enum';

@Component({
  selector: 'app-mole',
  template: `
    <h1>Whack-a-mole! <span class="score">{{ score$ | async }}</span></h1>
    <button #start class="start">Start!</button>
    <ng-container *ngIf="{ timeLeft: timeLeft$ | async } as data">
      <span class="duration">{{ data.timeLeft | remainingTime }}</span>
    </ng-container>
    <ng-container *ngIf="{ delayGameMsg: delayGameMsg$ | async } as data">
      <span class="message">{{ data.delayGameMsg | whackAMoleMessage }}</span>
    </ng-container>
    <div class="game">
      <div class="hole hole1" [style]="'--hole-image:' + holeSrc" #hole1>
        <div class="mole" [style]="'--mole-image:' + moleSrc" #mole1></div>
      </div>
      <div class="hole hole2" [style]="'--hole-image:' + holeSrc" #hole2>
        <div class="mole" [style]="'--mole-image:' + moleSrc" #mole2></div>
      </div>
      <div class="hole hole3" [style]="'--hole-image:' + holeSrc" #hole3>
        <div class="mole" [style]="'--mole-image:' + moleSrc" #mole3></div>
      </div>
      <div class="hole hole4" [style]="'--hole-image:' + holeSrc" #hole4>
        <div class="mole" [style]="'--mole-image:' + moleSrc" #mole4></div>
      </div>
      <div class="hole hole5" [style]="'--hole-image:' + holeSrc" #hole5>
        <div class="mole" [style]="'--mole-image:' + moleSrc" #mole5></div>
      </div>
      <div class="hole hole6" [style]="'--hole-image:' + holeSrc" #hole6>
        <div class="mole" [style]="'--mole-image:' + moleSrc" #mole6></div>
      </div>
    </div>`,
  styleUrls: ['mole.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class MoleComponent implements OnInit, OnDestroy {

  @ViewChild('start', { static: true, read: ElementRef })
  startButton!: ElementRef<HTMLButtonElement>;

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

  ... repeat the same step for hole2, hole3, hole4, hole5 and hole6 ...  

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

  ... repeat the same step for mole2, mole3, mole4, mole5 and mole6 ...  
  
  score$!: Observable<number>;
  timeLeft$!: Observable<number>;
  delayGameMsg$!: Observable<number>
  subscription = new Subscription();
  lastHoleUpdated = new BehaviorSubject<number>(-1);

  constructor(@Inject(APP_BASE_HREF) private baseHref: string) { }

  ngOnInit(): void {
    this.score$ = of(0);  
    this.delayGameMsg = of(3);   
    this.timeLeft$ = of(10);
  }

  get moleSrc(): string {
    return this.buildImage('mole.svg');
  }

  get holeSrc(): string {
    return this.buildImage('dirt.svg');
  }

  private buildImage(image: string) {
    const isEndWithSlash = this.baseHref.endsWith('/');
    const imagePath = `${this.baseHref}${isEndWithSlash ? '' : '/'}assets/images/${image}`;
    return `url('${imagePath}')`
  }

  ngOnDestroy(): void {}
}

whackAMoleMessagePipe displays the count down to allow the player to prepare before the game actually starts.

// whack-a-mole-message.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'whackAMoleMessage'
})
export class WhackAMoleMessagePipe implements PipeTransform {

  transform(seconds: number | null): string {
    if (seconds == null) {
      return '';
    }

    const units = seconds > 1 ? 'seconds' : 'second'; 
    return seconds > 0 ? `Whack a mole will begin in ${seconds} ${units}` : '';
  }
}

RemainingTimePipe displays the time remained in the game.

// remaining-time.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'remainingTime'
})
export class RemainingTimePipe implements PipeTransform {

  transform(seconds: number | null): string {
    if (seconds == null) {
      return '';
    }
    
    const units = seconds > 1 ? 'seconds' : 'second';
    return `Time remained: ${seconds} ${ units }`;
  }
}

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

// app.component.ts

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

@Component({
  selector: 'app-root',
  template: '<app-mole></app-mole>',
  styles: [`
    :host {
      display: block;
    }
  `]
})
export class AppComponent {
  title = 'Day30 Wrack a mole';

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

Define RxJS custom decorators to simplify game logic

The game has 3 RxJS custom operators, trackGameTime, peep and whackAMole. I refactor the logic out of MoleComponent to maintain a short ngOnInit method.

trackGameTime is a custom operator to start the count down from gameDurationInSeconds and return the remaining seconds

// time-tracker.operator.ts

import { Observable, concatMap, scan, startWith, take, timer } from 'rxjs';

export function trackGameTime<T>(gameDurationInSeconds = 10) {
    return function(source: Observable<T>) {
        return source.pipe(
            concatMap(() => timer(0, 1000).pipe(
                take(gameDurationInSeconds),
                scan((acc) => acc - 1, gameDurationInSeconds),
              )),
              startWith(gameDurationInSeconds),
        )
    }
}

peep selects a random mole to display in a random hole for a random time. The operator adds and removes CSS class in order to bring out the mole and hide it after some amount of time.

// peep.operator.ts

import { ElementRef } from '@angular/core';
import { BehaviorSubject, Observable, concatMap, map, tap, timer } from 'rxjs';

function randomTime(min: number, max: number): number {
    return Math.round(Math.random() * (max - min) + min);
}

function randomHole(holes: ElementRef<HTMLDivElement>[], lastHole: number): number {
    const idx = Math.floor(Math.random() * holes.length);
    console.log('In randomHole', 'lastHole', lastHole, 'next hole', idx);

    if (idx === lastHole) {
      console.log('Ah nah thats the same one bud');
      return randomHole(holes, lastHole);
    }

    return idx;
}

export function peep<T extends number>(holes: ElementRef<HTMLDivElement>[], minUpTime: number, maxUpTime: number) {
    return function(source: Observable<T>) {
        return source.pipe(
            map((lastHole) => ({
                upTime: randomTime(minUpTime, maxUpTime),
                holeIdx: randomHole(holes, lastHole),
            })),
            concatMap(({ upTime, holeIdx }) => {
                if (source instanceof BehaviorSubject) {
                    source.next(holeIdx);
                }
                const hole = holes[holeIdx].nativeElement;
                hole.classList.add('up');
                return timer(upTime).pipe(tap(() => hole.classList.remove('up')))
            }),
        );
    }
}
  • map selects a random mole and random hole
  • concatMap updates the last hole in the behaviorSubject to ensure the same hole is not picked the next time
  • timer delays by upTime and fires one time to remove the CSS class to remove the mole

whackAMole listens to the click event on mole, removes the mole and increments the score by 1

// whack-a-mole.operator.ts

import { Observable, filter, map, tap } from 'rxjs';
import { SCORE_ACTION } from '../mole/mole.enum';

export function whackAMole<T extends HTMLElement>(nativeElement: T) {
    return function(source: Observable<Event>) {
        return source.pipe(
            filter(event => event.isTrusted),
            tap(() => {
                if (nativeElement.parentElement) {
                    nativeElement.parentElement.classList.remove('up');
                }
            }),
            map(() => SCORE_ACTION.ADD)
        );
    }
}

Create whack a mole game using RxJS streams

Compose the RxJS streams from the easiest to the most difficult

  • calculate score
  • delay the game by 3 seconds after button click
  • display count down from 3, 2, 1 and 0
  • display remaining time in the guard
  • create game loop that runs 10 seconds and finishes the game

Initialize HTML elements

Use ViewChild to obtain references to button, moles and holes

  @ViewChild('start', { static: true, read: ElementRef })
  startButton!: ElementRef<HTMLButtonElement>;

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

  @ViewChild('hole3', { static: true, read: ElementRef })
  hole3!: ElementRef<HTMLDivElement>;
  
  @ViewChild('hole4', { static: true, read: ElementRef })
  hole4!: ElementRef<HTMLDivElement>;
  
  @ViewChild('hole5', { static: true, read: ElementRef })
  hole5!: ElementRef<HTMLDivElement>;
  
  @ViewChild('hole6', { static: true, read: ElementRef })
  hole6!: ElementRef<HTMLDivElement>;

  @ViewChild('mole1', { static: true, read: ElementRef })
  mole1!: ElementRef<HTMLDivElement>;
  
  @ViewChild('mole2', { static: true, read: ElementRef })
  mole2!: ElementRef<HTMLDivElement>;  
  
  @ViewChild('mole3', { static: true, read: ElementRef })
  mole3!: ElementRef<HTMLDivElement>;  
  
  @ViewChild('mole4', { static: true, read: ElementRef })
  mole4!: ElementRef<HTMLDivElement>;  
  
  @ViewChild('mole5', { static: true, read: ElementRef })
  mole5!: ElementRef<HTMLDivElement>;  
  
  @ViewChild('mole6', { static: true, read: ElementRef })
  mole6!: ElementRef<HTMLDivElement>;

Declare subscription instance member and unsubscribe in ngDestroy()

subscription = new Subscription();

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

Calculate game score

Update this.score$ observable to calculate the game score in ngOnInit().

private createMoleClickedObservables(...moles: ElementRef<HTMLDivElement>[]): Observable<SCORE_ACTION>[] {
    return moles.map(({ nativeElement }) => fromEvent(nativeElement, 'click').pipe(whackAMole(nativeElement)));
}

ngOnInit(): void {
   const molesClickedArray$ = this.createMoleClickedObservables(this.mole1, this.mole2, this.mole3, this.mole4, this.mole5, this.mole6);
    const startButtonClicked$ = fromEvent(this.startButton.nativeElement, 'click')
      .pipe(
        map(() => SCORE_ACTION.RESET),
        shareReplay(1)
      );

   this.score$ = merge(...molesClickedArray$, startButtonClicked$)
     .pipe(
       scan((score, action) => action === SCORE_ACTION.RESET ? 0 : score + 1, 0),
       startWith(0),
     );
}

Score will update when player clicks start button to reset game or whacks a mole to increment the score.

Delay start time of the game

I want the game to start 3 seconds after the button is clicked to allow the player to settle down. Therefore, I define delayGameMsg$ and delayGameStart$ observables.

const delayTime = 3;
this.delayGameMsg$ = startButtonClicked$.pipe(
   concatMap(() => timer(0, 1000)
      .pipe(
         take(delayTime + 1),
         map((value) => delayTime - value),
      ))
);

const delayGameStart$ = startButtonClicked$.pipe(
    delay(delayTime * 1000),
    shareReplay(1)
);

this.delayGameMsg$ is a timer that emits 3, 2, 1 and 0, and each value is fed to whackAMoleMessage pipe to display “Whack a mole will begin in X seconds”.

delayGameStart$ delays 3 seconds after the button click before the first mole appears.

Display the remaining time in the game

After the delay, I launch another timer Observable to show the game clock. The game clock sets to 10 seconds initially and goes down to 0.

const gameDuration = 10;
const resetTime$ = startButtonClicked$.pipe(map(() => gameDuration));
this.timeLeft$ = merge(resetTime$, delayGameStart$.pipe(trackGameTime(gameDuration)));
  • resetTime$ resets the clock to 10 seconds whenever the start button is clicked
  • delayGameStart$.pipe(trackGameTime(gameDuration) changes the game clock after the 3 seconds delay
  • merge merges the Observables to display the remaining time in the game

Create game loop

lastHoleUpdated = new BehaviorSubject<number>(-1);

const holes = [this.hole1, this.hole2, this.hole3, this.hole4, this.hole5, this.hole6];

const createGame = delayGameStart$.pipe(concatMap(() => this.lastHoleUpdated
   .pipe(
       peep(holes, 350, 1000),
       takeUntil(timer(gameDuration * 1000))
    )
))
.subscribe();

this.subscription.add(createGame);

After 3 seconds elapse, the game loop starts and continues for 10 seconds.

this.lastHoleUpdated is a behavior subject that keeps track of the last chosen hole. When this.lastHoleUpdated receives a new value, it calls peep operator to display another mole between 350 milliseconds and 1 second. takeUntil(timer(gameDuration * 1000)) ends the game loop after 10 seconds.

Since the inner observable returns a new observable, I use concatMap to return the result of this.lastHoleUpdated.pipe(....).

Add createGame to this.subscription and unsubscribe it in ngOnDestroy.

The example is done and we have built a whack a mole game successfully.

Final Thoughts

In this post, I show how to use RxJS and Angular to create whack a mole game. The first takeaway is to compose multiple RxJS streams together to implement game loop. The second takeaway is to encapsulate RxJS operators to custom operators to define streams that are lean and easy to understand. The final takeaway is to use async pipe to resolve observables such that developers do not have to clean up subscriptions.

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. Repo: https://github.com/railsstudent/ng-rxjs-30/tree/main/projects/day30-whack-a-mole
  2. Live demo: https://railsstudent.github.io/ng-rxjs-30/day30-whack-a-mole/
  3. Wes Bos’s JavaScript 30 Challenge: https://github.com/wesbos/JavaScript30

Build a reactive countdown timer using RxJS and Angular

Reading Time: 6 minutes

 31 total views

Introduction

This is day 29 of Wes Bos’s JavaScript 30 challenge and I am going to use RxJS and Angular to build a reactive countdown timer. The reactive countdown timer has the following functionalities:

  • a button toolbar to start a timer that has 20 seconds, 5 minutes, 15 minutes, 30 minutes or 60 minutes interval
  • A input field to enter arbitrary minutes
  • Display time left
  • Display the time when the timer stops

In this blog post, I describe how to merge button click and form submit streams to create a new stream to derive timer seconds. The new stream then emits the value to other streams to initiate count down and display timer stop time respectively. Ultimately, we build a reactive countdown timer that does not need a lot of codes to write.

let's go

Create a new Angular project

ng generate application day29-countdown-timer

Create Timer feature module

First, we create a Timer feature module and import it into AppModule. The feature module encapsulates TimerComponent, TimerControlsComponent and TimerPaneComponent.

Import TimerModule in AppModule

// timer.module.ts

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { TimerControlsComponent } from './timer-controls/timer-controls.component';
import { TimerPaneComponent } from './timer-pane/timer-pane.component';
import { TimerComponent } from './timer/timer.component';

@NgModule({
  declarations: [
    TimerComponent,
    TimerControlsComponent,
    TimerPaneComponent,
  ],
  imports: [
    CommonModule,
    FormsModule,
  ],
  exports: [
    TimerComponent
  ]
})
export class TimerModule { }

// app.module.ts

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

Declare Timer components in feature module

In Timer feature module, we declare three Angular components, TimerComponent, TimerControlsComponent and TimerPaneComponent to build a reactive countdown timer.

src/app
├── app.component.ts
├── app.module.ts
└── timer
    ├── index.ts
    ├── services
    │   └── timer.service.ts
    ├── timer
    │   └── timer.component.ts
    ├── timer-controls
    │   └── timer-controls.component.ts
    ├── timer-pane
    │   └── timer-pane.component.ts
    └── timer.module.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 { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-timer',
  template: `
  <div class="timer">
    <app-timer-controls></app-timer-controls>
    <app-timer-pane></app-timer-pane>
  </div>
  `,
  styles: [` ...omitted due to brevity... `],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimerComponent {}

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 { ChangeDetectionStrategy, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { TimerService } from '../services/timer.service';

@Component({
  selector: 'app-timer-controls',
  template: `
  <div class="timer__controls">
    <button class="timer__button" #timer1>20 Secs</button>
    <button class="timer__button" #timer2>Work 5</button>
    <button class="timer__button" #timer3>Quick 15</button>
    <button class="timer__button" #timer4>Snack 20</button>
    <button class="timer__button" #timer5>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 OnInit, OnDestroy {

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

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

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

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

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

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

  customMinutes = '';

  constructor(private timerService: TimerService) {}

  ngOnInit(): void {}

  ngOnDestroy(): void {}
}
// timer-pane.component.ts

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { of } from 'rxjs';
import { TimerService } from '../services/timer.service';

@Component({
  selector: 'app-timer-pane',
  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 {

  oneSecond = 1000;
  
  displayEndTime$ = of('');

  displayTimeLeft$ = of('');

  constructor(private titleService: Title, private timerService: TimerService) {}
}

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

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

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

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

Add timer service to share RxJS subjects and observables

In order to communicate data between TimerControlsComponent and TimerPaneComponent, I implement a TimerService to store Subjects and Observables that the components subscribe to stream events.

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

Use RxJS and Angular to implement timer control components

I am going to define Observables for button click and form submit events. Then, merge these observables to create a new observable to emit the selected seconds

Use ViewChild to obtain references to buttons and template-driven form

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

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

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

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

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

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

Declare subscription instance member and unsubscribe in ngDestroy()

subscription = new Subscription();

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

Create observables and emit value to secondsSub subject and subscribe in ngOnInit().

ngOnInit(): void {
    const videoNativeElement = this.video.nativeElement;
    const timer1$ = this.createButtonObservable(this.timer1.nativeElement, 20);
    const timer2$ = this.createButtonObservable(this.timer2.nativeElement, 300);
    const timer3$ = this.createButtonObservable(this.timer3.nativeElement, 900);
    const timer4$ = this.createButtonObservable(this.timer4.nativeElement, 1200);
    const timer5$ = this.createButtonObservable(this.timer5.nativeElement, 3600);

    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.add(
      merge(timer1$, timer2$, timer3$, timer4$, timer5$, myForm$)
        .subscribe((seconds) => this.timerService.updateSeconds(seconds))
    );
}

createButtonObservable(nativeElement: HTMLButtonElement, seconds: number) {
   return fromEvent(nativeElement, 'click').pipe(map(() => seconds))
}

myForm$ involves several steps in order to emit inputted seconds

  • filter(() => !!this.customMinutes) does nothing until input field has value
  • map(() => parseFloat(this.customMinutes)) converts value from string to number
  • map((customMinutes) => Math.floor(customMinutes * 60)) converts minutes to seconds
  • tap(() => this.myForm.nativeElement.reset()) resets template-driven form

Implement count down in TimerPaneComponent reactively

// timer-pane.component.ts

constructor(private titleService: Title, private timerService: TimerService) { }
  
oneSecond = 1000;
nowTo$ = this.timerService.seconds$.pipe(shareReplay(1));

countDown$ = this.nowTo$.pipe(
    switchMap((seconds) => timer(0, this.oneSecond).pipe(take(seconds + 1)))
);
displayTimeLeft$ = this.countDown$
   .pipe(
       withLatestFrom(this.nowTo$),
       map(([countdown, secondsLeft]) => secondsLeft - countdown),
       map((secondsLeft) => this.displayTimeLeft(secondsLeft)),
       tap((strTimeLeft) => this.titleService.setTitle(strTimeLeft))
    );

private displayTimeLeft(seconds: number) {
    const minutes = Math.floor(seconds / 60);
    const remainderSeconds = seconds % 60;
    return `${minutes}:${remainderSeconds < 10 ? '0' : '' }${remainderSeconds}`;
}

nowTo$ is an observable that emits the selected seconds. When I provide the selected 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, one second has elapsed and time left also decrements by 1 second

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

Therefore, displayTimeLeft$ is responsible to emit the remaining time in mm:ss format.

Display timer end time reactively

// timer-pane.component.ts

displayEndTime$ = this.nowTo$.pipe(map((seconds) => this.displayEndTime(Date.now(), seconds)));

private displayEndTime(now: number, seconds: number): string {
    const timestamp = now + seconds * this.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}`;
 }

displayEndTime$ is a trivial observable. It adds seconds to the current time to obtain the end time of the timer. Then, the Date object is formatted to hh:mm:ss AM/PM. Next, the observable is resolved in the inline template by async pipe.

The example is done and we have built a reactive countdown timer successfully.

Final Thoughts

In this post, I show how to use RxJS and Angular to build a reactive countdown timer. The first takeaway is to use switchMap and timer to create an observable to emit an integer per second to mimic count down. I achieve the effect declaratively without implementing any complex logic. The second takeaway is to encapsulate subject and observable in a shared service to exchange data between sibling components. The final takeaway is to use async pipe to resolve observable such that developers do not have to clean up subscriptions.

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. Repo: https://github.com/railsstudent/ng-rxjs-30/tree/main/projects/day29-countdown-timer
  2. Live demo: https://railsstudent.github.io/ng-rxjs-30/day29-countdown-timer/
  3. Wes Bos’s JavaScript 30 Challenge: https://github.com/wesbos/JavaScript30

See RxJS counterparts of array methods in action

Reading Time: 4 minutes

 42 total views

Introduction

This is day 7 of Wes Bos’s JavaScript 30 challenge where I am going to apply some RxJS counterparts of array methods to streams to obtain results.

In this blog post, I create observables from arrays and use some, every, filter, find and findIndex operators on RxJS streams to get some answers to some questions.

let's go

Create a new Angular project in workspace

ng generate application day7-array-cardio-part2

Create RxJS custom operator to do Array.some

RxJS counterparts of array methods include every, filter, find and findIndex, and some is not one of them. Therefore, I create a custom-operators directory to define a custom operator for Array.some.

The implementation of the custom operator can be found in some.operator.ts

// some.operator.ts

import { Observable, find, map, defaultIfEmpty } from 'rxjs';

export function some<T>(predicate: (item: T) => boolean) {
    return function (source: Observable<T>) {
        return source.pipe(
            find(item => predicate(item)),
            map(c => !!c),
            defaultIfEmpty(false),
        )
    }
}
  • the operator accepts a predicate that returns a boolean result
  • the source stream uses RxJS find to look for the first item that satisfies the predicate
  • emits the first item and use map to coerce object to boolean value
  • when source stream does not emit any item, defaultIfEmpty defaults the result to false

Define RxJS streams and emit them to arrays

src/app
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
├── custom-operators
│   └── some.operator.ts
└── interfaces
    ├── comment.interface.ts
    └── person.interface.ts

Let’s construct some streams and then use RxJS counterparts of array methods. Persons is an array of object and I convert it into an observable using from operator.

// app.component.ts

persons = [
    { name: 'Wes', year: 1988 },
    { name: 'Mary', year: 2006 },
    { name: 'George', year: 2009 },
    { name: 'Kait', year: 1986 },
    { name: 'Irv', year: 1970 },
    { name: 'Lux', year: 2015 }
];

people$ = from(this.persons).pipe(
    map((person) => this.calculateAge(person)),
    shareReplay(this.persons.length),
);

private calculateAge(person: PersonNoAge): Person {
   return {
      ...person,
      age: new Date().getFullYear() - person.year
   };
}

shareReplay(this.persons.length) caches all the persons before other streams reuse people$ to test conditions and do filtering. CalculateAge calculates the age of each person to help me verify the results.

Define peopleArray$ to get a persons array when people$ completes

peopleArray$ = this.people$.pipe(toArray());

Next, I construct a similar comments stream that is going to illustrate find and findIndex examples.

// app.component.ts

comments = [
    { text: 'Love this!', id: 523423 },
    { text: 'Super good', id: 823423 },
    { text: 'You are the best', id: 2039842 },
    { text: 'Ramen is my fav food ever', id: 123523 },
    { text: 'Nice Nice Nice!', id: 542328 }
] 

comments$ = from(this.comments).pipe(shareReplay(this.comments.length));
commentsArray$ = this.comments$.pipe(toArray());

Render peopleArray$ and commentsArray$ to visualize the data sets

@Component({
  selector: 'app-root',
  template: `
    <div class="container">
      <section class="people">
        <h1>People</h1>
        <ul *ngIf="peopleArray$ | async as peopleArray">
          <li *ngFor="let p of peopleArray">Name: {{ p.name }}<br/> Year: {{ p.year }}<br/> Age: {{ p.age }}</li>
        </ul>
      </section>
      <section class="comments">
        <h1>Comments</h1>
        <ul *ngIf="commentsArray$ | async as commentsArray">
          <li *ngFor="let p of commentsArray">Id: {{ p.id }}<br/> Text: {{ p.text }}</li>
        </ul>
      </section>
    </div>
  `,
  styles: [`...omitted for brevity...`]
})
export class AppComponent { ...RxJS codes... }

Example 1: Is at least one person an adult (19 or older)?

The question can answer by custom operator, some.

isAdult$ = this.people$.pipe(some(person => this.isAnAdult(person)));

private isAnAdult(person: Person, age = 19): boolean {
   return person.age >= age;
}

The stream finds the first person that is 19 or older and returns a boolean value.

Use async pipe to resolve isAdult$ and display the value

<p>Is Adult (at least one person is 19 or older)? {{ isAdult$ | async }}</p>

Example 2: Is everyone an adult (19 or older)?

allAdults$ = this.people$.pipe(every(person => this.isAnAdult(person)));

The stream validates every person is 19 or older.

Use async pipe to resolve allAdults$ and display the value

<p>All Adults (everyone is 19 or older)? {{ allAdults$ | async }}</p>

Example 3: Who are 19 years old or older?

adults$ = this.people$.pipe(
    filter(person => this.isAnAdult(person)),
    toArray()
); 

<section class="people">
   <h1>Adults</h1>
   <ul *ngIf="adults$ | async as adults">
       <li *ngFor="let p of adults">Name: {{ p.name }}<br/> Year: {{ p.year }}<br/> Age: {{ p.age }}</li>
   </ul>
 </section>

adults$ stream filters persons who are 19 and older. When stream completes, I use toArray to emit a person array in order to render the array elements in inline template.

Example 4: Find comment where id = 823423

comment$ = this.comments$.pipe(find(c => c.id === 823423));

The type of comment$ is Observable<Comment | undefined> because the stream may or may have the id.

<ng-container *ngIf="comment$ | async as comment; else noComment">
     <p>Find comment 823423?</p>
     <p>Id: {{ comment.id }}, text: {{ comment.text }}</p>
</ng-container>

<ng-template #noComment>
   <p>Comment does not exist</p>
</ng-template>

When comment exists, the template displays comment id and comment id. Otherwise, the template displays “Comment does not exist”.

Example 5: Find index of comment where id = 823423

Instead of getting back the comment, I am interested in its index. It is feasible by replacing find with findIndex.

commentIndex$ = this.comments$.pipe(findIndex(c => c.id === 823423));

<p>FindIndex of comment 823423? {{ commentIndex$ | async }}</p>

Final Thoughts

In this post, I show that some array methods have RxJS counterparts and they are used in a similar fashion except the source is an observable. When RxJS counterparts are absent, for example some and sort, I build my own custom operators that work for observable. Ultimately, the results are the same as if the input is an array.

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. Repo: https://github.com/railsstudent/ng-rxjs-30/tree/main/projects/day7-array-cardio-part2
  2. Live demo: https://railsstudent.github.io/ng-rxjs-30/day7-array-cardio-part2/
  3. Wes Bos’s JavaScript 30 Challenge: https://github.com/wesbos/JavaScript30

Create custom operators to sum and sort RxJS streams

Reading Time: 5 minutes

 59 total views

Introduction

This is day 4 of Wes Bos’s JavaScript 30 challenge where I am going to create RxJS custom decorators to sum and sort streams.

In this blog post, I describe the steps to create RxJS custom decorators, sum and sort to manipulate streams. First, I apply sort() to sort RxJS streams by a comparison function and render the sorted results on browser. Then, I apply sum() to accumulate stream by a property and output the total on browser. When RxJS does not provide operator for a task, we have the ability to build our own and use them to process RxJS streams.

let's go

Create a new Angular project in workspace

ng generate application day4-array-cardio-part1

Create RxJS custom operator to sum stream

I create custom-operators directory and add a new file, sum.operator.ts

// sum.operator.ts

import { reduce } from 'rxjs';

export function sum<T, A extends number>(sumFn: (acc: A, t: T) => A, initial: A) {
    return reduce(sumFn, initial);
}
  • the operator accepts an initial value (initial parameter) and accumulator (sumFn)
  • call RxJS’s reduce to calculate the sum of a stream
  • emit total when stream completes.

Create RxJS custom operator to sort stream

Similarly, I add sort.operator.ts to custom-operators directory to sort stream when it completes

import { Observable, map, toArray } from 'rxjs';

export function sort<T>(sortFn: (x: T, y: T) => number) {
    return function(source: Observable<T>) {
        return source.pipe(
            toArray(),
            map(items => items.sort(sortFn))
        )
    }
}
  • the operator accepts a comparison function (sortFn) that compares two objects and return an integer to define order
  • when source stream completes, I call toArray() to emit an T[]
  • execute map() to pass sortFn to Array.sort() and emit the sorted array

Get RxJS stream in an array

src/app
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
└── custom-operators
    ├── sort.operator.ts
    └── sum.operator.ts

Inventors is an array of object I convert the array into a stream using from operator

inventors = [
    { first: 'Albert', last: 'Einstein', year: 1879, passed: 1955 },
    { first: 'Isaac', last: 'Newton', year: 1643, passed: 1727 },
    { first: 'Galileo', last: 'Galilei', year: 1564, passed: 1642 },
    { first: 'Marie', last: 'Curie', year: 1867, passed: 1934 },
    { first: 'Johannes', last: 'Kepler', year: 1571, passed: 1630 },
    { first: 'Nicolaus', last: 'Copernicus', year: 1473, passed: 1543 },
    { first: 'Max', last: 'Planck', year: 1858, passed: 1947 },
    { first: 'Katherine', last: 'Blodgett', year: 1898, passed: 1979 },
    { first: 'Ada', last: 'Lovelace', year: 1815, passed: 1852 },
    { first: 'Sarah E.', last: 'Goode', year: 1855, passed: 1905 },
    { first: 'Lise', last: 'Meitner', year: 1878, passed: 1968 },
    { first: 'Hanna', last: 'Hammarström', year: 1829, passed: 1909 }
  ];

inventors$ = from(this.inventors).pipe(shareReplay(this.inventors.length));

shareReplay(this.inventors.length) caches all the inventors before other streams reuse inventors$ to sum or sort RxJS stream.

Define inventoryArray$ to get an inventory when inventors$ completes

inventorArray$ = this.inventors$.pipe(toArray());

Define a ng template to output inventory array as an unordered list. Subsequently, I reuse this template to output other array examples.

// app.component.ts

@Component({
  selector: 'app-root',
  template: `
    <div class="container">
      <ng-container 
        *ngTemplateOutlet="inventors; context: { $implicit: 'Inventors', list: inventorArray$ | async }">
      </ng-container>
    </div>

    <ng-template #inventors let-title let-list="list">
      <section class="inventors">
        <h2>{{ title }}</h2>
        <ul>
          <li *ngFor="let inventory of list; trackby: inventoryTrackBy">
            Name: {{ inventory.first }} {{ inventory.last }}<br />
            {{ inventory.year }} - {{ inventory.passed }}, Age: {{ inventory.passed - inventory.year }}
          </li>
        </ul>
      </section>
    </ng-template>
  `,
  styles: [`
    :host {
      display: block;
    }
    ... omitted for brevity ...
  `]
})
export class AppComponent { ...RxJS codes ... }

Sort inventor stream by year

ordered$ = this.inventors$.pipe(sort((a, b) => a.year > b.year ? 1 : -1));

I simply pass a comparison function to the custom sort operator to compare the year property of two inventory element. It is possible because toArray returns an JavaScript array that has a sort function to perform sorting.

Similarly, the template is available to render ordered$ after it is resolved

<ng-container 
     *ngTemplateOutlet="inventors; context: { $implicit: 'Ordered Inventors', list: ordered$ | async }">
</ng-container>

Sort inventor stream by age from oldest to youngest

oldest$ = this.inventors$.pipe(sort((a, b) => { 
    const lastInventor = a.passed - a.year;
    const nextInventor = b.passed - b.year;
    return lastInventor > nextInventor ? -1 : 1;
  }));

I pass a different comparison function to the operator to compare the age of the inventors. When the next inventor has a greater age (passed – year) than the previous inventor, the comparison function swaps the position to obtain the correct ordering.

<ng-container 
    *ngTemplateOutlet="inventors; context: { $implicit: 'Oldest Inventors', list: oldest$ | async }">
</ng-container>

The template displays the oldest$ stream to list the inventors by their age

Demonstrate sort operator can work on any data type

The custom sort operator can work on any data type because of a powerful TypeScript concept called generic. It can sort any element of type T as long as the comparison function returns an integer to define order.

The following is an example to sort a people stream by last name.

people = ['Bernhard, Sandra', 'Bethea, Erin', 'Becker, Carl', 'Bentsen, Lloyd', 'Beckett, Samuel', 'Blake, William', 'Berger, Ric', 'Beddoes, Mick', 'Beethoven, Ludwig','Belloc, Hilaire', 'Begin, Menachem', 'Bellow, Saul', 'Benchley, Robert', 'Blair, Robert', 'Benenson, Peter', 'Benjamin, Walter', 'Berlin, Irving','Benn, Tony', 'Benson, Leana', 'Bent, Silas', 'Berle, Milton', 'Berry, Halle', 'Biko, Steve', 'Beck, Glenn', 'Bergman, Ingmar', 'Black, Elk', 'Berio, Luciano','Berne, Eric', 'Berra, Yogi', 'Berry, Wendell', 'Bevan, Aneurin', 'Ben-Gurion, David', 'Bevel, Ken', 'Biden, Joseph', 'Bennington, Chester', 'Bierce, Ambrose', 'Billings, Josh', 'Birrell, Augustine', 'Blair, Tony', 'Beecher, Henry', 'Biondo, Frank'];

people$ = from(this.people).pipe(shareReplay(this.people.length));
alpha$ = this.people$.pipe(sort((lastOne, nextOne) => {
   const [aLast] = lastOne.split(', ');
   const [bLast] = nextOne.split(', ');
   return aLast > bLast ? 1 : -1;
}));

The data type of people in string whereas the inventors in the inventors stream are objects. Yet, sort sorts the streams in the correct order in all the examples.

Demonstrate sum operator to sum RxJS stream

The example is to add the year of all the inventors and display the total year. The sum operator calls reduce under the hood; therefore, it expects a reducer function and an initial value.

inventors$ = from(this.inventors).pipe(shareReplay(this.inventors.length));
totalYears$ = this.inventors$.pipe(sum((acc: number, y) => acc + (y.passed - y.year), 0));

<section class="inventors">
    <h2>Total Years</h2>
    <p>{{ totalYears$ | async }}</p>
</section>

The custom operator is very simple and it can take other functions to do things such as count the number of characters in first or last.

totalFirstLength$ = this.inventors$.pipe(sum((acc: number, y) => acc + y.first.length, 0));

Another one-liner that states the purpose of the stream clearly.

Final Thoughts

In this post, I show how to create custom RxJS operators and use them to transform streams in an Angular application. To emphasize DRY principle, I create a ng template to render the streams in a list to reduce duplicated codes in the inline template.

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. Repo: https://github.com/railsstudent/ng-rxjs-30/tree/main/projects/day4-array-cardio-part1
  2. Live demo: https://railsstudent.github.io/ng-rxjs-30/day4-array-cardio-part1/
  3. Wes Bos’s JavaScript 30 Challenge: https://github.com/wesbos/JavaScript30

Take photos with web camera using RxJS and Angular

Reading Time: 7 minutes

 52 total views

Introduction

This is day 19 of Wes Bos’s JavaScript 30 challenge where I am going to use RxJS operators and Angular to take photos, add them to photo section for me to download to my local hard drive.

In this blog post, I inject native Navigator to component such that I can load web camera to video element. Every 16 seconds, a callback function draws the video image to canvas with special effects. Whenever I click “Take photo” button, the canvas converts data to base64 string and add it to photo section from most recent to earliest.

let's go

Create a new Angular project in workspace

ng generate application day19-webcam-fun      

Define Native Navigator

First, we create core module and define native navigaor to inject into the web camera component.

// core.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NAVIGATOR_PROVIDERS } from './navigator.service';

@NgModule({
  declarations: [],
  imports: [
    CommonModule
  ],
  providers: [NAVIGATOR_PROVIDERS]
})
export class CoreModule { }

Next, I create NAVIGATOR injection token and NAVIGATOR_PROVIDERS in navigator service

// navigator.service.ts
import { isPlatformBrowser } from '@angular/common';
import { ClassProvider, FactoryProvider, InjectionToken, PLATFORM_ID } from '@angular/core';

/* Create a new injection token for injecting the navigator into a component. */
export const NAVIGATOR = new InjectionToken('NavigatorToken');

export abstract class NavigatorRef {
  get nativeNavigator(): Navigator | Object {
    throw new Error('Not implemented.');
  }
}

/* Define class that implements the abstract class and returns the native navigator object. */
export class BrowserNavigatorRef extends NavigatorRef {

  constructor() {
    super();
  }

  override get nativeNavigator(): Object | Navigator {
    return navigator;    
  }
}

/* Create a injectable provider for the NavigatorRef token that uses the BrowserNavigatorRef class. */
const browserNavigatorProvider: ClassProvider = {
  provide: NavigatorRef,
  useClass: BrowserNavigatorRef
};

/* Create an injectable provider that uses the navigatorFactory function for returning the native navigator object. */
const navigatorProvider: FactoryProvider = {
  provide: NAVIGATOR,
  useFactory: (browserWindowRef: BrowserNavigatorRef, platformId: Object) => 
    isPlatformBrowser(platformId) ? browserWindowRef.nativeNavigator : new Object(),
  deps: [ NavigatorRef, PLATFORM_ID ]
};

/* Create an array of providers. */
export const NAVIGATOR_PROVIDERS = [
  browserNavigatorProvider,
  navigatorProvider
];

After defining the providers, I provide NAVIGATOR_PROVIDERS to the core module

// core.module.ts 

@NgModule({
  declarations: [],
  imports: [
    CommonModule
  ],
  providers: [NAVIGATOR_PROVIDERS]
})
export class CoreModule { }

The definition of the core module is now complete and I import CoreModule to AppModule.

app.module.ts

import { APP_BASE_HREF, PlatformLocation } from '@angular/common';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { CoreModule } from './core';

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

Create Web Camera feature module

The next feature module to create is Web Camera feature module and it also imports into AppModule. I declare two components in the feature module: WebCameraComponent and PhotoStripeComponent. WebCameraComponent takes photos with a web camera and lists them in PhotoStripeComponent from most recent to earliest.

Then, Import WebCamModule in AppModule

// webcam.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { WebCameraComponent } from './web-camera/web-camera.component';
import { PhotoStripeComponent } from './photo-stripe/photo-stripe.component';

@NgModule({
  declarations: [
    WebCameraComponent,
    PhotoStripeComponent
  ],
  imports: [
    CommonModule
  ],
  exports: [
    WebCameraComponent
  ]
})
export class WebCamModule { }

// app.module.ts

import { APP_BASE_HREF, PlatformLocation } from '@angular/common';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { CoreModule } from './core';
import { WebCamModule } from './webcam';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    WebCamModule,
    CoreModule,
  ],
  providers: [
    {
      provide: APP_BASE_HREF,
      useFactory: (platformLocation: PlatformLocation) => platformLocation.getBaseHrefFromDOM(),
      deps: [PlatformLocation]
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Declare components in web camera feature module

In web camera module, I declare WebCameraComponent that loads the web camera of my laptop to video element in order to take photos. PhotoStripeComponent is a presentation component that iterates an array of base64 string and display them from latest to earliest.

src/assets
└── audio
    └── snap.mp3

src/app
├── app.component.ts
├── app.module.ts
├── core
│   ├── core.module.ts
│   ├── index.ts
│   └── navigator.service.ts
└── webcam
    ├── index.ts
    ├── interfaces
    │   └── webcam.interface.ts
    ├── photo-stripe
    │   └── photo-stripe.component.ts
    ├── web-camera
    │   └── web-camera.component.ts
    └── webcam.module.ts

I define component selector, inline template and inline CSS styles in WebCameraComponent. Later sections of the blog post will implement RxJS codes to add the functionality. For your information, <app-web-camera> is the tag of the component.

// webcam.interface.ts

export interface Photo {
    data: string;
    description: string;
    download: string;
}
// web-camera.component.ts

import { APP_BASE_HREF } from '@angular/common';
import { ChangeDetectionStrategy, Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Observable, of, Subject } from 'rxjs';
import { NAVIGATOR } from '../../core/navigator.service';
import { Photo } from '../interfaces/webcam.interface';

@Component({
  selector: 'app-web-camera',
  template: `
  <ng-container>
    <div class="photobooth">
      <div class="controls">
        <button #btnPhoto>Take Photo</button>
      </div>
      <canvas class="photo" #photo></canvas>
      <video class="player" #video></video>
      <ng-container *ngIf="photoStripe$ | async as photoStripe">
        <app-photo-stripe [photoStripe]="photoStripe"></app-photo-stripe>
      </ng-container>
    </div>
    <audio class="snap" [src]="soundUrl" hidden #snap></audio>
  </ng-container>
  `,
  styles: [`
    :host {
      display: block;
    }
    ...omitted for brevity...
  `],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class WebCameraComponent implements OnInit, OnDestroy {

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

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

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

  @ViewChild('photo', { static: true, read: ElementRef })
  canvas!: ElementRef<HTMLCanvasElement>;

  destroy$ = new Subject<void>();

  photoStripe$!: Observable<Photo[]>;

  constructor(@Inject(APP_BASE_HREF) private baseHref: string, @Inject(NAVIGATOR) private navigator: Navigator) { }

  ngOnInit(): void {
    const videoNative = this.video.nativeElement;
    const canvasNative = this.canvas.nativeElement;
    const ctx = canvasNative.getContext('2d', { willReadFrequently: true });

    this.getVideo();

    this.photoStripe$ = of([]);
  }

  get soundUrl() {
    const isEndWithSlash = this.baseHref.endsWith('/');
    return `${this.baseHref}${ isEndWithSlash ? '' : '/' }assets/audio/snap.mp3`; 
  }

  private getVideo() {
    console.log('navigator', this.navigator);

    this.navigator.mediaDevices.getUserMedia({ video: true, audio: false })
      .then(localMediaStream => {
        console.log(localMediaStream);

        const nativeElement = this.video.nativeElement;         
        nativeElement.srcObject = localMediaStream;
        nativeElement.play();
      })
      .catch(err => {
        console.error(`OH NO!!!`, err);
      });
  }

  private rgbSplit(pixels: ImageData) {
    for (let i = 0; i < pixels.data.length; i += 4) {
      pixels.data[i - 150] = pixels.data[i + 0]; // RED
      pixels.data[i + 500] = pixels.data[i + 1]; // GREEN
      pixels.data[i - 550] = pixels.data[i + 2]; // Blue
    }
    return pixels;
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

photoStripe$ is an Observable<Photo[]> and async pipe resolves the observable in inline template to render the array elements.

<ng-container *ngIf="photoStripe$ | async as photoStripe">
    <app-photo-stripe [photoStripe]="photoStripe"></app-photo-stripe>
 </ng-container>

async resolves photoStripe$ to photoStripe variable and photoStripe is the input parameter of PhotoStripeComponent.

getVideo is a method that uses the native Navigator to load web camera and assign it to video element.

// photo-stripe.component.ts

import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { Photo } from '../interfaces/webcam.interface';

@Component({
  selector: 'app-photo-stripe',
  template: `<div class="strip">
    <a *ngFor="let photo of photoStripe; index as i;" [href]="photo.data" download="{{photo.download}}{{i + 1}}">
      <img [src]="photo.data" [alt]="photo.description" />
    </a>
  </div>`,
  styles: [`
    :host {
      display: block;
    }
    ... omitted for brevity ...
  `],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class PhotoStripeComponent {

  @Input()
  photoStripe!: Photo[];
}

PhotoStripeComponent is a simple presentation component that renders base64 strings to <a> and <img> elements, and the hyperlinks are downloaded when clicked.

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

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

@Component({
  selector: 'app-root',
  template: '<app-web-camera></app-web-camera>', 
  styles: [`
    :host {
      display: block;
    }
  `],
})
export class AppComponent {
  title = 'Day 19 Web Cam Fun';

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

Apply RxJS operators to render video capture in canvas

In ngOnInit, I use RxJS to render video capture in 2D canvas. The event streaming starts when video is ready to play.

// web-camera.component.ts

import { concatMap, filter, fromEvent, map, Observable, scan, startWith, Subject, takeUntil, tap, timer } from 'rxjs';

const videoNative = this.video.nativeElement;
const canvasNative = this.canvas.nativeElement;
const ctx = canvasNative.getContext('2d', { willReadFrequently: true });

fromEvent(videoNative, 'canplay')
  .pipe(
      filter(() => !!ctx),
      map(() => ctx as CanvasRenderingContext2D),
      concatMap((canvasContext) => {
        const width = videoNative.videoWidth;
        const height = videoNative.videoHeight;    
        canvasNative.width = width;
        canvasNative.height = height;
        const interval = 16;   

        return timer(0, interval).pipe(
           tap(() => {
              canvasContext.drawImage(this.video.nativeElement, 0, 0, width, height);
              // take the pixels out
              const pixels = canvasContext.getImageData(0, 0, width, height);
          
              this.rgbSplit(pixels);
              canvasContext.globalAlpha = 0.8;  
              canvasContext.putImageData(pixels, 0, 0);
           })
         )
       }),
       takeUntil(this.destroy$)
  )
  .subscribe();

Explanations:

  • fromEvent(videoNative, ‘canplay’) listens to canplay event of the video
  • filter(() => !!ctx) validates 2D canvas is defined
  • map(() => ctx as CanvasRenderingContext2D) casts 2D canvas as CanvasRenderingContext2D
  • concatMap((canvasContext) => {….}) creates a timer observable to draw the canvas every 16 seconds
  • takeUntil(this.destroy$) unsubscribes the observable

    timer returns an Observable; therefore, I use concatMap instead of map to write the pixels to the canvas

    concatMap((canvasContext) => {
        const width = videoNative.videoWidth;
        const height = videoNative.videoHeight;    
        canvasNative.width = width;
        canvasNative.height = height;    
    
        return timer(0, interval).pipe(
           tap(() => {
              canvasContext.drawImage(this.video.nativeElement, 0, 0, width, height);
              // take the pixels out
              const pixels = canvasContext.getImageData(0, 0, width, height);
              
              this.rgbSplit(pixels);
              canvasContext.globalAlpha = 0.8;  
              canvasContext.putImageData(pixels, 0, 0);
            })
         )
    })

    Build photo list with RxJS operators

    // web-camera.component.ts
    
    this.photoStripe$ = fromEvent(this.btnPhoto.nativeElement, 'click')
      .pipe(
          tap(() => {
             const snapElement = this.snap.nativeElement;
             snapElement.currentTime = 0;
             snapElement.play();
          }),
          map(() => ({ 
             data: this.canvas.nativeElement.toDataURL('image/jpeg'),
             description: 'My photo',
             download: 'photo',
          })),
          scan((photos, photo) => [photo, ...photos], [] as Photo[]),
          startWith([] as Photo[]),
      );

    Explanations:

    • tap(() => { …play sound… }) plays an audio file when I click “Take photo” button
    • map(() => { …create base64 string, description and file download name … }) constructs base64 string, description and file name
    • scan((photos, photo) => [photo, …photos], [] as Photo[]) accumulates photos from most recent to earliest
    • startWith([] as Photo[]) initializes an empty photo list

    This is the end of the example. I built an Angular and RxJS example to take photos and prepend new photo in photo stripe component for download.

    Final Thoughts

    In this post, I show how to use RxJS and Angular to take fun photos with web camera and make them available for download.

    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. Repo: https://github.com/railsstudent/ng-rxjs-30/tree/main/projects/day19-webcam-fun
    2. Live demo: https://railsstudent.github.io/ng-rxjs-30/day19-webcam-fun/
    3. Wes Bos’s JavaScript 30 Challenge: https://github.com/wesbos/JavaScript30