Sort data without articles using RxJS and Angular

Reading Time: 4 minutes

 36 total views

Introduction

This is day 17 of Wes Bos’s JavaScript 30 challenge and I am going to sort data without articles RxJS and Angular.

In this blog post, I describe how to use RxJS operators to covert array to Observable, use function to remove articles temporarily, compare texts to determine sort order and finally map the results to Observable variable.

let's go

Create a new Angular project in workspace

ng generate application day17-sorted-without-articles

Create Sorted List feature module

First, we create a List feature module and import it into AppModule. The feature module is consisted of SortedListComponent that encapsulates pattern matching and the comparison logic.

Then, Import ListModule in AppModule

// list.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SortedListComponent } from './sorted-list/sorted-list.component';

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

// app.module.ts

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

import { AppComponent } from './app.component';
import { ListModule } from './list';

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

Declare component in feature module

In List module, I declare SortedListComponent that displays the sorted data in a list.

The component will apply built-in RxJS operators and then use ngFor directive to render the sorted data in <li> elements.

src/app
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
└── list
    ├── index.ts
    ├── list.module.ts
    └── sorted-list
        ├── sorted-list.component.spec.ts
        └── sorted-list.component.ts

I define component selector, inline template and inline CSS styles in the file. RxJS codes will be implemented in the later sections of the blog post. For your information, <app-sorted-list> is the tag of SortListComponent.

@Component({
  selector: 'app-sorted-list',
  template: `
    <div>
      <h1>Sort bands without articles</h1>
      <ul id="bands">
        <ng-container *ngIf="sortedBands$ | async as sortedBands">
          <li *ngFor="let band of sortedBands">{{ band }}</li>
        </ng-container>
      </ul>
    </div>
  `,
  styles:[`
    :host {
      display: block;
    }

    div {
      display: flex;
      flex-wrap: wrap;
      justify-content: center;
    }

    h1 {
      text-align: center;
      margin-bottom: 2rem;
      flex-basis: 100%;
    }

    #bands {
      list-style: inside square;
      font-size: 20px;
      background: white;
      width: 500px;
      padding: 0;
      box-shadow: 0 0 0 20px rgba(0, 0, 0, 0.05);
    }
    
    #bands li {
      border-bottom: 1px solid #efefef;
      padding: 20px;
    }
    
    #bands li:last-child {
      border-bottom: 0;
    }

    a {
      color: #ffc600;
      text-decoration: none;
    }
  `],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class SortedListComponent {

  sortedBands$ = of(['']);

  private strip(bandName: string) {
    return bandName.replace(/^(a |the |an )/i, '').trim();
  }
}

sortedBands$ is an Observable<string[]> and it is resolved in inline template by async pipe to render the array elements. Sorting without articles occurs when I use strip to remove articles temporarily and then compare two texts to determine their sort order.

I use of operator instead of from operator because of streams the entire array to the next operator whereas from streams one item at a time and is not applicable to sorting purpose.

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

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

@Component({
  selector: 'app-root',
  template: '<app-sorted-list></app-sorted-list>',
  styles: [`
    :host {
      display: block;
    }
  `]
})
export class AppComponent {
  title = 'Day17 Sorted Without Articles';

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

Write RxJS code to return sorted data

In this section, I will incrementally modify showBands$ to sort data without articles.

// sorted-list.component.ts

import { map, of } from 'rxjs';

sortedBands$ = of(['The Plot in You', 'The Devil Wears Prada', 
    'Pierce the Veil', 
    'Norma Jean', 'The Bled', 
    'Say Anything', 
    'The Midway State', 
    'We Came as Romans', 
    'Counterparts', 
    'Oh, Sleeper', 
    'A Skylit Drive', 
    'Anywhere But Here', 
    'An Old Dog'
])
  .pipe(map(bands => ([...bands].sort((a, b) => this.strip(a) > this.strip(b) ? 1 : -1))));

Let’s explain each line of RxJS code

  • of([….]) converts the string array into Observable
  • map(bands => ([…bands].sort((a, b) => this.strip(a) > this.strip(b) ? 1 : -1))) strips articles, sorts the strings and maps the results

Finally, I have a simple page that can sort data without articles and render the sorted list subsequently.

Final Thoughts

In this post, I show how to use RxJS and Angular to sort data without articles. the takeaway is to use RxJS of operator to convert array to Observable. Then, the array is able to stream to the next operator for further data transformation.

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

Animate text shadow using RxJS and Angular

Reading Time: 4 minutes

 59 total views,  1 views today

Introduction

This is day 16 of Wes Bos’s JavaScript 30 challenge and I am going to add CSS animation to text using RxJS and Angular. Through RxJS, I can update text shadow style of text element during mouse moves to produce the effect of CSS animation.

In this blog post, I describe how to use RxJS operators to listen to mouse move event, calculate the X and Y distances of the text shadows, apply async pipe to resolve the observable and finally assign the values to text-shadow property. As a bonus, I refactor map operators into custom RxJS operator such that the observable codes in ngOnInit is kept as lean as possible.

let's go

Create a new Angular project in workspace

ng generate application day16-mouse-move

Create Mouse Move feature module

First, we create a MouseMove feature module and import it into AppModule. The feature module is consisted of MouseMoveComponent that encapsulates the logic of CSS animation.

Then, Import MouseMoveModule in AppModule

// mouse-move.module.ts

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

// app.module.ts

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

import { AppComponent } from './app.component';
import { MouseMoveModule } from './mouse-move';

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

Declare component in feature module

In MouseMove feature module, I declare MouseMoveComponent that derives text-shadow CSS property.

The component will apply built-in RxJS operators and the mapXYWalk operator in custom-operators directory.

src/app
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
└── mouse-move
    ├── custom-operators
    │   └── mapTextShadowStyle.operator.ts
    ├── index.ts
    ├── mouse-move
    │   ├── mouse-move.component.spec.ts
    │   └── mouse-move.component.ts
    └── mouse-move.module.ts

I define component selector, inline template and inline CSS styles in the file. RxJS codes will be implemented in the later sections of the blog post. For your information, <app-mouse-move> is the tag of MouseMoveComponent.

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

@Component({
  selector: 'app-mouse-move',
  template: `
    <div class="hero" #hero>
      <ng-container *ngIf="textShadow$ | async as textShadow">
        <h1 contenteditable [style.textShadow]="textShadow">🔥WOAH!</h1>
      </ng-container>
    </div>
  `,
  styles: [`
    :host { 
      display: block;
    }

    .hero {
      min-height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      color: black;
    }

    h1 {
      text-shadow: 10px 10px 0 rgba(0,0,0,1);
      font-size: 100px;
    }
  `],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class MouseMoveComponent implements OnInit {

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

  textShadow$!: Observable<string>;
  
  ngOnInit(): void {
    const nativeElement = this.hero.nativeElement;

    this.textShadow$ = fromEvent(nativeElement, 'mousemove')
      .pipe(
        startWith('')
      );
  }
}

I use VieChild decorator to obtain the <div> element that has the hero reference. It is needed because I am going to listen to the mousemove event on this.hero.nativeElement.

The initial value of text-shadow property is an empty string; therefore, there is no CSS animation until mouse cursor moves.

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

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

@Component({
  selector: 'app-root',
  template: '<app-mouse-move></app-mouse-move>',
  styles: [`
    :host {
      display: block;
    }
  `]
})
export class AppComponent {
  title = 'Day16 Mouse Move';

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

Write RxJS code to derive text-shadow property

In this section, I will incrementally modify textShadow$ to derive the text-shadow property of the text element.

// mouse-move.component.ts

import { filter, fromEvent, map, Observable, startWith } from 'rxjs';
import { mapTextShadowStyle } from '../custom-operators/mapTextShadowStyle.operator';

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

    this.textShadow$ = fromEvent(nativeElement, 'mousemove')
      .pipe(
        filter(e => e instanceof MouseEvent),
        map(e => e as MouseEvent),
        mapTextShadowStyle(nativeElement),
        startWith('')
      ); 
}

Let’s explain each line of RxJS code

  • fromEvent(nativeElement, ‘mousemove’) listens to the mousemove event of the <div> element
  • filter(e => e instanceof MouseEvent) filters the MouseEvent event
  • map(e => e as MouseEvent) cast the event to MouseEvent event
  • mapTextShadowStyle(nativeElement) is a RxJS custom operator that computes the values of text-shadow property
  • startWith(”) determines the initial value of the text-shadow style

Demystify mapTextShadowStyle custom operator

mapTextShadowStyle is a function that returns a function that returns Observable<string>. The inner function accepts a source Observable and emits mousemove event to map operators to return text-shadow property values.

// mapTextShadowStyle.operator.ts

import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export function mapTextShadowStyle<T extends HTMLDivElement>(nativeElement: T, walk = 500) {
    return function(source: Observable<MouseEvent>): Observable<string> {
        return source.pipe(
            map((e: MouseEvent) => {
                const { offsetX: x, offsetY: y } = e;
                const evtTarget = e.target as T;
                const newOffset = { x, y };
                if (evtTarget !== nativeElement) {
                    newOffset.x = newOffset.x + evtTarget.offsetLeft;
                    newOffset.y = newOffset.y + evtTarget.offsetTop;
                }
            
                const { offsetWidth: width, offsetHeight: height } = nativeElement;
                const xWalk = Math.round((x / width * walk) - (walk / 2));
                const yWalk = Math.round((y / height * walk) - (walk / 2));
                return { xWalk, yWalk };
            }),
            map(({ xWalk, yWalk }) => 
                (`
                    ${xWalk}px ${yWalk}px 0 rgba(255,0,255,0.7),
                    ${xWalk * -1}px ${yWalk}px 0 rgba(0,255,255,0.7),
                    ${yWalk}px ${xWalk * -1}px 0 rgba(0,255,0,0.7),
                    ${yWalk * -1}px ${xWalk}px 0 rgba(0,0,255,0.7)
                `))
            );
    }
}

The first map operator computes the x and y distance between text shadows and the text element. The second map operator uses the xWalk and yWalk parameters to compute text-shadow property values and outputs from textShadow$ observable.

Finally, I have a simple page that produces animated text shadows when mouse cursor moves on the <div> element.

Final Thoughts

In this post, I show how to use RxJS and Angular to demonstrate CSS animation. When observable.pipe() becomes longer, I can refactor RxJS operators into custom operator and reuse it in pipe method. Moreover, RxJS code is declarative that I can comprehend after coming back to the codebase after a couple of days.

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

Synchronize data with local storage using RxJS and Angular

Reading Time: 8 minutes

 58 total views

Introduction

This is day 15 of Wes Bos’s JavaScript 30 challenge and I am going to use RxJS and Angular to add, delete and check items in a list, and synchronize data with local storage.

In this blog post, I describe how to use Subject and RxJS operators to listen to form submit and JavaScript events, and synchronize data with the local storage. When events occur, RxJS operators are responsible for updating the user interface and persisting the data to the local storage. If data synchronization is successful, we will be able to restore the UI after closing and reopening browser window.

let's go

Create a new Angular project in workspace

ng generate application day15-local-storage

Create List feature module

First, we create a List feature module and import it into AppModule. The feature module ultimately encapsulates two components that are ListContainerComponent and DataListComponent. DataListComponent renders the item list in the local storage whereas ListContainerComponent is the parent of a template form and the DataListComponent.

Then, Import ListModule in AppModule

// list.module.ts

@NgModule({
  declarations: [
    ListContainerComponent,
    DataListComponent
  ],
  imports: [
    CommonModule,
    FormsModule
  ],
  exports: [
    ListContainerComponent
  ]
})
export class ListModule { }

// 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 { ListModule } from './list';

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

Declare components in feature module

In List feature module, we declare DataListComponent to render data in the local storage. When data row is checked, unchecked or deleted, event emitter emits the row item to ListContainerComponent to synchronize data with the local storage. Moreover, ListContainerComponent has a submit button that appends new item to the list and a button that either checks all items or un-checks all of them. Therefore, the application has multiple sources to synchronize data with the local storage.

The sources are:

  • submit form to add data
  • a button to check or uncheck all items
  • check/uncheck the checkbox of a single row
  • delete a single row

It sounds like a lot of work but the RxJS codes in the components are less than 40 lines respectively.

src/app
├── app.component.spec.ts
├── app.component.ts
├── app.module.ts
└── list
    ├── data-list
    │   ├── data-list.component.spec.ts
    │   └── data-list.component.ts
    ├── index.ts
    ├── interfaces
    │   ├── index.ts
    │   ├── new-item.interface.ts
    │   └── toggle-item.interface.ts
    ├── list-container
    │   ├── list-container.component.spec.ts
    │   ├── list-container.component.ts
    │   └── type-guard.ts
    └── list.module.ts

In DataListComponent, we define app selector, inline template and inline CSS styles. We will add the RxJS codes to implement the logic in the later sections. For your information, <app-data-list> is the tag of DataListComponent.

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

@Component({
  selector: 'app-data-list',
  template: `
    <ul class="plates" #plates>
      <li *ngFor="let plate of itemList; index as i">
        <input type="checkbox" [attr.data-index]="i" id="item{{i}}" [checked]="plate.done" />
        <label for="item{{i}}">{{plate.text}}</label>
        <button [attr.data-index]="i" id="btn{{i}}">X</button>
      </li>
    </ul>
  `,
  styles: [`
    ... omitted for brevity ...
  `],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class DataListComponent implements OnInit, OnDestroy {
  @ViewChild('plates', { static: true, read: ElementRef })
  plates!: ElementRef<HTMLUListElement>;

  @Input()
  itemList!: NewItem[];

  ngOnInit(): void {}

  ngOnDestroy(): void {}
}

Next, we create ListContainerComponent that encapsulates <app-data-list> and a HTML form. For your reference, the tag of ListContainerComponent is <app-list-container>.

import { Component, ChangeDetectionStrategy } from '@angular/core';
import { Subject } from 'rxjs';
import { NewItem, ToggleItems } from '../interfaces';

@Component({
  selector: 'app-list-container',
  template: `
  <div class="wrapper">
    <h2>LOCAL TAPAS</h2>
    <p></p>
    <ng-container *ngIf="itemList$ | async as itemList">
        <app-data-list [itemList]="itemList"></app-data-list>
    </ng-container>
    <form class="add-items" (ngSubmit)="submit$.next({ text: newItem, done: false })">
      <input type="text" name="item" placeholder="Item Name" [required]="true" name="newItem" [(ngModel)]="newItem">
      <input type="submit" value="+ Add Item">
      <input type="button" [value]="Check all" (click)="btnCheckAllClicked$.next({ action: 'toggleAll' })">
    </form>
  </div>
  `,
  styles: [`
    ...omitted by brevity...
  `],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListContainerComponent {

  newItem = '';
  submit$ = new Subject<NewItem>();
  toggleDone$ = new Subject<ToggleItem>();
  itemList$: Observable<NewItem[]> = of([]);
  btnCheckAllClicked$ = new Subject<ToggleItems>();
}

itemList$ is hardcoded of([]) but I will convert it to read from the local storage later.

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

import { APP_BASE_HREF } from '@angular/common';
import { Component, ElementRef, Inject } from '@angular/core';
import { Title } from '@angular/platform-browser';

@Component({
  selector: 'app-root',
  template: `
    <svg xmlns="http://www.w3.org/2000/svg">...svg path...</svg>
    <app-list-container></app-list-container>
  `,
  styles: [`
    :host {
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      text-align: center;
      background-repeat: no-repeat;
      background-position: center;
      background-size: cover;
    }
    ... omitted for brevity ...
  `]
})
export class AppComponent {
  title = 'Day15 LocalStorage';

  constructor(titleService: Title, private hostElement: ElementRef<HTMLElement>, @Inject(APP_BASE_HREF) private baseHref: string) {
    this.hostElement.nativeElement.style.backgroundImage = this.imageUrl;
    titleService.setTitle(this.title);
  }

  get imageUrl() {
    const isEndWithSlash = this.baseHref.endsWith('/');
    const image =  `${this.baseHref}${ isEndWithSlash ? '' : '/' }assets/images/oh-la-la.jpeg`; 
    return `url('${image}')`;
  }
}

Write RxJS code to synchronize data to the local storage – append item

In this section and the following sections, I will incrementally modify itemList$ to update item list in the local storage. First, I listen to the submit event to append the item to the local storage.

In ListContainerComponent, I declare storedItems to get the list items from the local storage keyed items.

storedItems = JSON.parse(localStorage.getItem('items') || JSON.stringify([])) as NewItem[];

Next, I create a NewItem interface that submit$ subject requires to stream data when submit event occurs

// new-item.interface.ts
export interface NewItem { 
    done: boolean; 
    text: string;
}

// interfaces/index.ts
export * from './new-item.interface';

In the inline template, ngSubmit emits submit$.next({ text: newItem done: false }) in the form element

<form class="add-items" (ngSubmit)="submit$.next({ text: newItem, done: false })">

After defining the interface and initializing storedItems, I can proceed to modify itemList$.

// list-container.component.ts

itemList$ = merge(this.submit$)
    .pipe(
      scan((acc, value) => {
        if (isNewItem(value)) {
          return acc.concat(value);
        }

        return acc;
      }, this.storedItems),
      tap((items) => {
        console.log('Update local storage');
        localStorage.setItem('items', JSON.stringify(items));
        this.newItem = '';
      }),
      shareReplay(1),
      startWith(this.storedItems),
 );

itemList$ will listen to other actions; therefore, I use merge RxJS operator to create a new observable. When value has the shape of NewItem interface, I use scan to append the item to array. Then, I use tap to update the local storage and clear the input field. startWith displays the initial local storage when application starts.

isNewItem is a type guard; it is a useful TypeScript feature when value is a union type and I need to know the actual type of it to perform the correct action.

export function isNewItem(data: any): data is NewItem {
    return 'text' in data;
}

When value has text property, it is a NewItem and I can append it to the list.

Write RxJS code to synchronize data to the local storage – check all and uncheck items

In this section, I click the “Check all” button in ListContainerComponent to check all items and changes the button text to “Uncheck all”. When I click the button again, all items are unchecked as the result and the text reverts to “Check all”.

Next, I define a ToggleItems interface that btnCheckAllClicked$ subject requires to stream data when click event occurs.

// toggle-item.interface.ts
export interface ToggleItems {
  action: 'toggleAll'
}

// interfaces/index.ts
export * from './toggle-item.interface';

In the inline template, checkAll button emits btnCheckAllClicked$.next({ action: ‘toggleAll’ }) when click event occurs. Moreover, btnToggleCheckText$ observable monitors the state of itemList$ to update button text accordingly.

<input type="button" [value]="btnToggleCheckText$ | async" (click)="btnCheckAllClicked$.next({ action: 'toggleAll' })">

Let’s modify itemList$ to stream btnCheckAllClicked$ subject.

// list-container.component.ts

itemList$ = merge(this.submit$, this.btnCheckAllClicked$)
    .pipe(
      scan((acc, value) => {
        if (isToggleItems(value)) {
          const done = !acc.every(item => item.done);
          return acc.map((item) => ({ ...item, done }));  
        } else if (isNewItem(value)) {
          return acc.concat(value);
        }

        return acc;
      }, this.storedItems),
      ... the rest stays the same ...,
 );

isToggleItem is also a type guard and it tests whether or not value satisfies the shape of ToggleItems interface.

export function isToggleItems(data: any): data is ToggleItems {
    return 'action' in data && data.action === 'toggleAll';
}

When value has action property and action is “toggleAll”, then it is a ToggleItems and I toggle the done property of the items.

We are not quite done yet, we still have to update the text of the button.

btnToggleCheckText$ =  this.itemList$
   .pipe(
      map(items => { 
        const isAllChecked = items.every(item => item.done);
        return isAllChecked ? 'Uncheck all' : 'Check all'; 
    }),
    startWith('Check all')
);

startWith initializes the text to ‘Check all’. When items are all done, I map the text to ‘Uncheck all’, otherwise, it defaults to ‘Check all’.

Emit data from data list to ListContainerComponent to synchronize data in RxJS

Next, I am going to apply RxJS to toggle and delete individual item in DataListComponent and emit the result to ListContainerComponent. I chose event emitter over service to avoid boilerplates. If parent and child components use a lot of subjects and observables for communication, I will centralize them in a shared service instead of scattering event emitters all over the places.

Declare an itemList input that accepts an array of NewItem. itemList receives the data from itemList$ observable in ListContainerComponent.

Declare toggleDone event emitter to emit the result of toggle item and delete item.

@Input()
itemList!: NewItem[];

@Output()
toggleDone = new EventEmitter<ToggleItem>();

destroy$ = new Subject<void>();

ToggleItem is the last interface that we need to record item index and state

// toggle-item.interface.ts

export interface ToggleItem {
    action: ItemAction;
    index: number;
    done: boolean;
}

Implement toggle and delete item in one go

ngOnInit(): void {
    fromEvent(this.plates.nativeElement, 'click')
      .pipe(
        filter(e => (e.target as any).matches('input') || (e.target as any).matches('button')),
        map((e: Event) => map((e: Event) => this.createToggleItem(e)),
        takeUntil(this.destroy$),
      )
      .subscribe((value) => {
        console.log(value);
        this.toggleDone.emit(value);
      });
 }

 private createToggleItem(e: Event): ToggleItem {    
    const target = e.target as any;
    const nodeName = `${target.nodeName}`;
    const index = +target.dataset.index;
    const done = !this.itemList[index].done;
    const action: ItemAction = nodeName === 'INPUT' ? 'toggle' : 'delete';
    return { action, index, done };
  }

 ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
 }
  • fromEvent(this.plates.nativeElement, ‘click’) listens to the click event of the unordered list
  • filter(e => (e.target as any).matches(‘input’) || (e.target as any).matches(‘button’)) filters the click event of checkbox and button
  • map((e: Event) => this.createToggleItem(e)) creates ToggleItem from the event
  • takeUntil(this.destroy$) stops the observable until this.destroy$ completes
  • In subscribe, I use toggleDone to emit the result of the observable

In createToggleItem, I use nodeName to derive the action. When node name is INPUT, I toggle the checkbox and action is ‘toggle’. When node name is BUTTON, I click the button to delete the item.

Handle toggle and delete individual item in ListContainerComponent

The inline template of ListContainerComponent receives the output of DataListComponent and processes it in the RxJS code.

// list-container.component.ts

<app-data-list [itemList]="itemList" (toggleDone)="toggleDone$.next($event)"></app-data-list>

toggleDone$ = new Subject<ToggleItem>();

itemList$ = merge(this.submit$, this.toggleDone$, this.btnCheckAllClicked$)
   .pipe(
      scan((acc, value) => {
        if (isToggleItems(value)) {
          const done = !acc.every(item => item.done);
          return acc.map((item) => ({ ...item, done }));         
        } else if (isNewItem(value)) {
          return acc.concat(value);
        } 
          
        const { action, done, index } = value
        if (action === 'toggle') {
           return acc.map((item, i) => i !== index ? item : { ...item, done });
        }
        return acc.filter((_, i) => i !== index);
      }, this.storedItems),
      ... the rest stays the same ...
  );

Finally, I have a simple page that synchronizes data with local storage when create, delete or update occurs.

Close the browser and reopen it and the item list is restored from the local storage.

Final Thoughts

In this post, I show how to use RxJS and Angular to demonstrate component composition and data synchronization with local storage. I am amazed that it did not not a lot of RxJS to handle multiple events. Moreover, RxJS code is declarative that I can comprehend after coming back to the codebase after a couple of days.

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

Slide in images on scroll using RxJS and Angular

Reading Time: 5 minutes

 64 total views

Introduction

This is day 13 of Wes Bos’s JavaScript 30 challenge and I am going to use RxJS and Angular to slide in 5 images when scrolling up and down the browser images.

In this blog post, I describe how to use RxJS operators (fromEvent, debounce time, map, startWith) to listen to scroll event and slide in images when they are half shown in the window. When image is in the viewport, the application dynamically adds CSS class slide in image. Otherwise, I remove the class to slide out the image.

let's go

Create a new Angular project in workspace

ng generate application day13-slide-in-on-scroll

Create Scroll feature module

First, we create a Scroll feature module and import it into AppModule. The feature ultimately encapsulates one component that is Scroll.

Then, Import ScrollModule in AppModule

// scroll.module.ts

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

// app.module.ts

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

Declare component in feature module

In Scroll feature module, we declare ScrollComponent to listen to window’s scroll event, uses scrollY and innerHeight to determine whether or not an image at least 50% visible in the browser window. When the condition is met, enables CSS class to trigger the slide in images effect. Otherwise, the CSS class is removed to cause the images to slide out.

src/app/scroll
├── index.ts
├── scroll
│   ├── scroll.component.html
│   ├── scroll.component.spec.ts
│   └── scroll.component.ts
└── scroll.module.ts

In ScrollComponent, we define app selector, and inline CSS styles. scroll.component.html is the template file of the HTML codes. We will add the RxJS codes to implement the functions in later sections. For your information, <app-scroll> is the tag of ScrollComponent.

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

@Component({
  selector: 'app-scroll',
  templateUrl: './scroll.component.html',
  styles: [`
    ... omit inline styles for brevity
  `],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ScrollComponent {}

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

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

@Component({
  selector: 'app-root',
  template: '<app-scroll></app-scroll>',
  styles: [`
    :host {
      display: block;
    }
  `]
})
export class AppComponent {
  title = 'Day 13 Slide in on Scroll';

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

Slide in images using RxJS and CSS transformation

Now, I am going to apply RxJS to implement scroll event in ScrollComponent and animate the effect of sliding images.

I take step-by-step approach to do the following:

  • Inject window provider to the component
  • Listen to window scroll event and derive an observable of Observable<boolean[]>
  • Use async pipe to resolve the observable. When the value is true, add active class to allow the images to slide in. When the value is false, remove active class to slide out the images.

Inject Window to ScrollComponent

First, I inject the provider, WINDOW, to the constructor of ScrollComponent

// scroll.component.ts 

import { WINDOW } from '../../core';

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

Implement RxJS logic to determine the state of CSS active class

Second, I modify HTML template to add #img reference to all <img> elements. Then, I can use ViewChildren to query all image elements in the code.

// scroll.component.html

<img src="https://unsplash.it/400/400" class="align-left slide-in" #img>
<img src="https://unsplash.it/400/401" class="align-right slide-in" #img>
<img src="https://unsplash.it/200/500" class="align-left slide-in" #img>
<img src="https://unsplash.it/200/200" class="align-right slide-in" #img>
<img src="https://unsplash.it/400/501" class="align-right slide-in" #img>   
// scroll.component.ts 

@ViewChildren('img')
sliderImages!: QueryList<ElementRef<HTMLImageElement>>;

Declare isSlideIn$ observable to determine the state of CSS class, active, for each image.

isSlideIn$ = fromEvent(this.window, 'scroll')
    .pipe(
      debounceTime(20),
      map(() => this.slideImages()),
      startWith([false, false, false, false, false])
    );

private slideImages() {
    const { scrollY, innerHeight } = this.window;
    return this.sliderImages.map(({ nativeElement: sliderImage }) => {
      // half way through the image
      const slideInAt = (scrollY + innerHeight) - sliderImage.height / 2;
      // bottom of the image
      const imageBottom = sliderImage.offsetTop + sliderImage.height;
      const isHalfShown = slideInAt > sliderImage.offsetTop;
      const isNotScrolledPast = scrollY < imageBottom;
      return isHalfShown && isNotScrolledPast;
    });
 }
  • fromEvent(this.window, ‘scroll’) listens to window scroll event
  • debounceTime(20) debounces 20 seconds so the events do not fire rapidly
  • map(() => this.slideImages()) determine to enable or disable css active class
  • startWith([false, false, false, false, false]) provides the initial values to disable css active class of all images

Use async pipe to resolve observable and bind the values to css classes

Lastly, I modify the HTML template to use async pipe to resolve isSlideIn$ observable. Finally, array elements are binded to [class.active] to perform CSS transformation in the HTML template.

// scroll.component.html

<div class="site-wrap" *ngIf="isSlideIn$ | async as isSlideIn">
   ... omit plain texts for brevity ...

<img src="https://unsplash.it/400/400" class="align-left slide-in" #img [class.active]="isSlideIn[0]">
<img src="https://unsplash.it/400/401" class="align-right slide-in" #img [class.active]="isSlideIn[1]">
<img src="https://unsplash.it/200/500" class="align-left slide-in" #img [class.active]="isSlideIn[2]">
<img src="https://unsplash.it/200/200" class="align-right slide-in" #img [class.active]="isSlideIn[3]">
<img src="https://unsplash.it/400/501" class="align-right slide-in" #img [class.active]="isSlideIn[4]">   
</div>

Finally, I have a simple page that demonstrates slide in/slide out images when window scrolls either up or down.

Final Thoughts

In this post, I show how to use RxJS and Angular to provide CSS animations in HTML template. The solution is applicable because the page has limited number of static images. I can use async pipe to resolve the observable and bind the observable value to css classes manually. At run time, the css classes are turned on and off to display the animations.

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/day13-slide-in-on-scroll
  2. Live demo: https://railsstudent.github.io/ng-rxjs-30/day13-slide-in-on-scroll/
  3. Wes Bos’s JavaScript 30 Challenge: https://github.com/wesbos/JavaScript30
  4. Angular Window Provider: https://brianflove.com/2018-01-11/angular-window-provider

Detect key sequence using RxJS and Angular

Reading Time: 5 minutes

 58 total views

Introduction

This is day 12 of Wes Bos’s JavaScript 30 challenge and I am going to use RxJS and Angular to detect key sequence. When user inputs the correct secret code, the application calls an external JavaScript library to render unicorns.

In this blog post, I describe how to use RxJS operators (scan, map, filter) to keep track of inputted keys. When the application detects that the key sequence matches the secret code, it calls cornify.js to render a unicorn at a random location.

let's go

Create a new Angular project in workspace

ng generate application day12-key-sequence-detection

Create Detect Key Sequence feature module

First, we create a KeySequenceDetection feature module and import it into AppModule. The feature ultimately encapsulates one component that is KeySequenceDetectionComponent.

Then, Import KeySequenceDetectionModule in AppModule

// key-sequence-detection.module.ts

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

// app.module.ts

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

Declare component in feature module

In KeySequenceDetection feature module, we declare KeySequenceDetectionComponent to listen to window’s keyup event, compare keys against the secret code (wesbos) and display unicorn when a match is found.

src/app/key-sequence-detection
├── index.ts
├── key-sequence-detection
│   ├── key-sequence-detection.component.spec.ts
│   └── key-sequence-detection.component.ts
└── key-sequence-detection.module.ts

In KeySequenceDetectionComponent, we define app selector, inline template and inline CSS styles. We will add the RxJS codes to implement the functions in later sections. For your information, <app-key-sequence-detection> is the tag of KeySequenceDetectionComponent.

import { ChangeDetectionStrategy, Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { filter, fromEvent, map, scan, Subject, takeUntil  } from 'rxjs';

@Component({
  selector: 'app-key-sequence-detection',
  template: `
    <div><p>Type the secret code to display unicorn(s)!</p></div>
  `,
  styles: [`
    :host {
      display: block;
    }
  `],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class KeySequenceDetectionComponent implements OnInit, OnDestroy {

  readonly secretCode = 'wesbos';
  constructor() { }

  ngOnInit(): void {}

  ngOnDestroy(): void {}
}

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

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

@Component({
  selector: 'app-root',
  template: '<app-key-sequence-detection></app-key-sequence-detection>',
  styles: [`
    :host {
      display: block;
    }
  `]
})
export class AppComponent {
  title = 'Day12 Key Sequence Detection';

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

Add window service to listen to key event

In order to detect key sequence on native Window, I write a window service to inject to KeySequenceDetectionComponent to listen to keyup 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 { }

Detect key sequence using RxJS

Now, I am going to apply RxJS to implement keyup event in KeySequenceDetectionComponent

I take step-by-step approach from declaring global JS library, token injection, creating subscription in ngOnInit and unsubscribing the subscription in ngOnDestroy.

Install and declare global JS library

First, I add script tag in the body of index.htm. I choose to add the JS script in the body because the application does not require it to execute immediately. The cornify.js library is needed when user types the correct secret code.

// index.html

<body>
  <app-root></app-root>
  <script type="text/javascript" src="https://www.cornify.com/js/cornify.js"></script>
</body>

Then, go back to KeySequenceDetectionComponent and declare a global variable of cornify.js at the top of the file.

The variable name is cornify_add to match the function name in the JS library.

// key-sequence-detection.component.ts

declare var cornify_add: any;

Inject Window to KeySequenceDetectionComponent

// key-sequence-detection.component.ts 

import { WINDOW } from '../../core';

destroy$ = new Subject<void>();

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

At the same time, I initialize a destroy$ subject to unsubscribe the subscription at the end of the post.

Implement RxJS logic to detect key sequence in ngOnInit

// key-sequence-detection.component.ts 

ngOnInit(): void {
    fromEvent(this.window, 'keyup')
      .pipe(
        filter(e => e instanceof KeyboardEvent),
        map(e => e as KeyboardEvent),
        scan((acc, e) => {
          acc.push(e.key);
          acc.splice(-this.secretCode.length - 1, acc.length - this.secretCode.length);
          return acc;
        }, [] as string[]),
        map(acc => acc.join('')),
        filter(inputtedCode => inputtedCode.includes(this.secretCode)),
        takeUntil(this.destroy$)
      )
      .subscribe(() => cornify_add());
}
  • fromEvent(this.window, ‘keyup’) listens to window keyup event
  • filter(e => e instanceof KeyboardEvent) discards event that is not KeyboardEvent
  • map(e => e as KeyboardEvent) casts event to KeyboardEvent because filter operator only passes down KeyboardEvent
  • scan(….) accumulates keys in an array and return return last six keys
  • map(acc => acc.join(”)) concatenates array elements into a string
  • filter(inputtedCode => inputtedCode.includes(this.secretCode)) compares the input matches the secret code
  • takeUntil(this.destroy$) indicates the event stream continues until this.destroy$ subject completes
  • .subscribe(() => cornify_add()) shows a beautiful unicorn

Unsubscribe the subscription

After subscribing to a subscription in ngOnInit, I have to unsubscribe it in ngOnDestroy.

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

Finally, I have a browser window covered by mystical unicorns.

Final Thoughts

In this post, I show how to use RxJS and Angular to detect key sequence. The first takeaway is to create a window provider in core module and inject window in the component. The second takeaway is to declare a global var variable for the external JS library. The final takeaway is when subscription is created in ngOnInit, developers need to unsubscribe it in ngDestroy.

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/day12-key-sequence-detection
  2. Live demo: https://railsstudent.github.io/ng-rxjs-30/day12-key-sequence-detection/
  3. Wes Bos’s JavaScript 30 Challenge: https://github.com/wesbos/JavaScript30
  4. Angular Window Provider: https://brianflove.com/2018-01-11/angular-window-provider

Use RxJS and Angular to build a custom video player

Reading Time: 8 minutes

 53 total views

Introduction

This is day 11 of Wes Bos’s JavaScript 30 challenge and I am going to use RxJS and Angular to build a custom video player. The player has the following functionalities:

  • pause and play video
  • fast forward the video by 25 seconds
  • backward the video by 10 seconds
  • increase/decrease the volume
  • increase/decrease playback rate
  • click progress bar

In this blog post, I describe how to create a video player component that encapsulates a native video element and child video controls components. Then, I use RxJS and Angular to implement each of the functionalities and use Subject/Observable to communicate between parent and child components.

let's go

Create a new Angular project

ng new application day11-custom-video-player

Create Video Player feature module

First, we create a VideoPlayer feature module and import it into AppModule. The feature ultimately encapsulates VideoPlayerComponent and VideoPlayerControlsComponent

Import VideoPlayerModule in AppModule

// video-player.module.ts

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

// app.module.ts

function getBaseHref(platformLocation: PlatformLocation): string {
  return platformLocation.getBaseHrefFromDOM();
}

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    VideoPlayerModule
  ],
  providers: [
    {
      provide: APP_BASE_HREF,
      useFactory: getBaseHref,
      deps: [PlatformLocation]
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

APP_BASE_HREF is an injector that injects the base href of the mp4 video. VideoPlayerComponent applies the injected value to derive the full url of the video and subsequently sets the source of the native <video> element.

Declare Video components in feature module

In VideoPlayer feature module, we declare two Angular components, VideoPlayerComponent and VideoPlayerControlsComponent to compose the video player. Then, the application uses the technique of RxJS and Angular to pass data between parent and child components

src/app/video-player
├── enums
│   ├── index.ts
│   └── video-actions.enum.ts
├── index.ts
├── interfaces
│   ├── index.ts
│   ├── video-action.interface.ts
│   └── video-player.interface.ts
├── services
│   ├── index.ts
│   ├── video-player.service.spec.ts
│   └── video-player.service.ts
├── video-player
│   ├── video-player.component.spec.ts
│   └── video-player.component.ts
├── video-player-controls
│   ├── video-player-controls.component.scss
│   ├── video-player-controls.component.spec.ts
│   └── video-player-controls.component.ts
└── video-player.module.ts

In VideoPlayerComponent, we define app selector, inline template and inline CSS styles. We will add the RxJS codes to implement the functions in later sections. For your information, <app-video-player-controls> is the tag of VideoPlayerControlsComponent.

import { APP_BASE_HREF } from '@angular/common';
import { Component, OnInit, ChangeDetectionStrategy, Inject, ViewChild, ElementRef, OnDestroy } from '@angular/core';
import { fromEvent, map, merge, Subscription, tap } from 'rxjs';
import { VideoActionEnum } from '../enums';
import { VideoAction, VideoPlayerRangeInput } from '../interfaces';
import { VideoPlayerService } from '../services';

@Component({
  selector: 'app-video-player',
  template: `
    <div class="player">
      <video class="player__video viewer" currentTime="10" #video>
        <source [src]="videoSrc" type="video/mp4">
      </video>
      <app-video-player-controls></app-video-player-controls>
    </div>
  `,
  styles: [`
    :host {
      display: flex;
      background: #7A419B;
      min-height: 100vh;
      background: linear-gradient(135deg, #7c1599 0%,#921099 48%,#7e4ae8 100%);
      background-size: cover;
      align-items: center;
      justify-content: center;
    }

    .player {
        max-width: 750px;
        border: 5px solid rgba(0,0,0,0.2);
        box-shadow: 0 0 20px rgba(0,0,0,0.2);
        position: relative;
        font-size: 0;
        overflow: hidden;
    }
      
    /* This css is only applied when fullscreen is active. */
    .player:fullscreen {
        max-width: none;
        width: 100%;
    }
      
    .player:-webkit-full-screen {
        max-width: none;
        width: 100%;
    }
      
    .player__video {
        width: 100%;
    }
  `],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class VideoPlayerComponent implements OnInit, OnDestroy {

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

  ngOnInit(): void {}

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

  ngOnDestroy(): void {}
}

Similar, I define app selector, inline template and CSS styles in VideoPlayerControlsComponent.

@Component({
  selector: 'app-video-player-controls',
  template: `
    <div class="player__controls">
      <div class="progress" #progress>
        <div class="progress__filled" [style.flexBasis]="videoProgressBar$ | async"></div>
      </div>
      <button class="player__button toggle" title="Toggle Play" [textContent]="videoButtonIcon$ | async" #toggle>►</button>
      <input type="range" name="volume" class="player__slider" min="0" max="1" step="0.05" value="1" #range>
      <input type="range" name="playbackRate" class="player__slider" min="0.5" max="2" step="0.1" value="1" #range>
      <button data-skip="-10" class="player__button" #skip>« 10s</button>
      <button data-skip="25" class="player__button" #skip>25s »</button>
    </div>
  `,
  styleUrls: ['./video-player-controls.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class VideoPlayerControlsComponent implements OnInit, OnDestroy, AfterViewInit {

  constructor() { }

  ngOnInit(): void {}

  ngAfterViewInit(): void {}

  ngOnDestroy(): void {}
}

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

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 = 'Day11 HTML Video Player';

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

Add video service to share RxJS subjects and observables

In order to communicate data between VideoPlayerComponent and VideoPlayerControlsComponent, I add a VideoPlayerService to store Subjects and Observables that the components subscribe to stream events.

@Injectable({
  providedIn: 'root'
})
export class VideoPlayerService {
  private readonly videoButtonIconSub = new Subject<string>();
  private readonly videoProgressBarSub = new Subject<string>();
  private readonly videoActionSub = new Subject<VideoAction>();

  readonly videoButtonIcon$ = this.videoButtonIconSub.asObservable();
  readonly videoProgressBar$ = this.videoProgressBarSub.asObservable();
  readonly videoAction$ = this.videoActionSub.asObservable();

  updateVideoButtonIcon(icon: string) {
    this.videoButtonIconSub.next(icon);
  }

  updateVideoProgressTime(flexBasis: string) {
    this.videoProgressBarSub.next(flexBasis);
  }

  updateVideoAction(action: VideoAction): void {
    this.videoActionSub.next(action);
  }
}

Use RxJS and Angular to implement Video Player functions

We are going to use RxJS to implement the following events:

  • Click <video> element to play or pause video
  • Update button icon when video is playing or pausing
  • Update progress bar when video is playing

Click <video> element to play or pause video

Use ViewChild to obtain the reference to video player

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

Declare subscription instance member and unsubscribe in ngDestroy()

subscription = new Subscription();

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

Create observable from click event, feed the value to videoActionSub subject and subscribe in ngOnInit().

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

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

    const togglePlay$ = fromEvent(videoNativeElement, 'click')
        .pipe(
          map(() => ({ action: VideoActionEnum.TOGGLE_PLAY, arg: undefined })),
          tap(nextAction => this.videoService.updateVideoAction(nextAction))
        )
        .subscribe();

    this.subscription.add(togglePlay$);
}

In ngOnInit(), subscribe to this.videoService.videoAction$ to obtain the value and process video action.

this.subscription.add(this.videoService.videoAction$.subscribe(nextAction => this.processAction(videoNativeElement, nextAction))
);
private processAction(videoNativeElement: HTMLVideoElement, nextAction: VideoAction): void {
    ...  other logic ...
    } else if (nextAction.action === VideoActionEnum.TOGGLE_PLAY) {
      const methodName = videoNativeElement.paused ? 'play' : 'pause';
      videoNativeElement[methodName]();
    }
    ... other logic ...
  }

Update button icon when video is playing or pausing

this.subscription.add(
    merge(
       fromEvent(videoNativeElement, 'pause').pipe(map(() => '►')), 
       fromEvent(videoNativeElement, 'play').pipe(map(() => '❚ ❚')),
    )
    .pipe(tap(icon => this.videoService.updateVideoButtonIcon(icon)))
    .subscribe()
);

In VideoPlayerControlsComponent, we subscribe this.videoPlayService.videoButtonIcon$ to render the textContent of the toggle button.

Update progress bar when video is playing

this.subscription.add(
      fromEvent(videoNativeElement, 'timeupdate')
        .pipe(
          map(() => { 
            const progressTime = (videoNativeElement.currentTime / videoNativeElement.duration) * 100;
            return `${progressTime}%`;
          }),
          tap(flexBasis => this.videoService.updateVideoProgressTime(flexBasis))
        )
        .subscribe()
);

In VideoPlayerControlsComponent, we subscribe this.videoPlayerService.videoProgressBar$ to adjust the flexBasic style of the progress bar.

Use RxJS and Angular to implement UI functions Video Player Controls

We are going to use RxJS to implement the UI functions of video player controls:

  • Click button to play or pause video. (Implemented in ngOnInit)
  • Click progress bar to play a specific frame of the video. (Implemented in ngOnInit)
  • Drag progress bar to play a specific frame of the video. (Implemented in ngOnInit)
  • Click forward and backward buttons to change the video time of the video. (Implemented in ngAfterViewInit)
  • Change the volume of the video. (Implemented in ngAfterViewInit)
  • Change the playback rate of the video. (Implemented in ngAfterViewInit)

Similarly, declare subscription instance member and unsubscribe in ngDestroy()

constructor(private videoPlayerService: VideoPlayerService) { }

subscription = new Subscription();

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

Click button to play or pause video

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

ngOnInit(): void {
    this.subscription.add(
      fromEvent(this.toggleButton.nativeElement, 'click')
        .pipe(
          map(() => ({ action: VideoActionEnum.TOGGLE_PLAY, arg: undefined })),
          tap(nextAction => this.videoPlayerService.updateVideoAction(nextAction))
        ).subscribe()
    );
}

Click progress bar to play a specific frame of the video.

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

private createProgressBarAction(action: VideoActionEnum, offsetX: number): VideoAction {
    return { action, arg: offsetX / this.progress.nativeElement.offsetWidth };
}

ngOnInit(): void {
    ...

    const progressNativeElement = this.progress.nativeElement;
    this.subscription.add(
      fromEvent(progressNativeElement, 'click')
        .pipe(
          filter(event => event instanceof PointerEvent),
          map(event => event as PointerEvent),
          map(({ offsetX }) => this.createProgressBarAction(VideoActionEnum.PROGESS_BAR_CLICKED, offsetX)),
          tap(nextAction => this.videoPlayerService.updateVideoAction(nextAction))
        ).subscribe()
    );
}

In VideoPlayerComponent, we subscribe this.videoPlayerService.videoAction$ to update the current time of the video.

Drag progress bar to play a specific frame of the video.

ngOnInit(): void {
    ...

    const mouseDown$ = fromEvent(progressNativeElement, 'mousedown');
    const drag$ = fromEvent(progressNativeElement, 'mousemove').pipe(
      takeUntil(fromEvent(progressNativeElement, 'mouseup'))
    );

    this.subscription.add(
      mouseDown$
        .pipe(
          concatMap(() => drag$.pipe(
            filter(event => event instanceof MouseEvent),
            map(event => event as MouseEvent),
            map(({ offsetX }) => this.createProgressBarAction(VideoActionEnum.PROGRESS_BAR_DRAGGED, offsetX))
          )),
          tap(nextAction => this.videoPlayerService.updateVideoAction(nextAction))        
        ).subscribe()
    );
}

In VideoPlayerComponent, we subscribe this.videoPlayerService.videoAction$ to update the current time of the video.

Click forward and backward buttons to change the video time of the video.

@ViewChildren('range', { read: ElementRef })
rangeInputs!: QueryList<ElementRef<HTMLInputElement>>

ngAfterViewInit(): void {
    const skipButtonEvents$ = this.skipButtons.reduce((acc, skipButton) => {
      const clickEvent$ = fromEvent(skipButton.nativeElement, 'click').pipe(
        map(({ target }) => {
          const strSeconds = (target as HTMLButtonElement).dataset['skip'];
          const seconds = strSeconds ? +strSeconds : 0;
          return {
            action: VideoActionEnum.SKIP_BUTTON_CLICKED,
            arg: seconds
          }
        }),
      )

      return acc.concat(clickEvent$);
    }, [] as Observable<VideoAction>[])

    this.subscription.add(merge(...skipButtonEvents$).pipe(
tap(nextAction => this.videoPlayerService.updateVideoAction(nextAction))
    ).subscribe());
}

Change the volume or playback rate of the video

@ViewChildren('range', { read: ElementRef })
rangeInputs!: QueryList<ElementRef<HTMLInputElement>>;

ngAfterViewInit(): void {
    ...

    const rangeInputEvents$ = this.rangeInputs.reduce((acc, rangeInput) => 
      acc.concat(this.addRangeUpdateEvent(rangeInput, 'change'), this.addRangeUpdateEvent(rangeInput, 'mousemove'))
    , [] as Observable<VideoAction>[]);

    this.subscription.add(merge(...skipButtonEvents$, ...rangeInputEvents$)
      .pipe(tap(nextAction => this.videoPlayerService.updateVideoAction(nextAction)))
      .subscribe());
}

The following is the full implementation of addRangeUpdateEvent

private addRangeUpdateEvent(rangeInput: ElementRef<HTMLInputElement>, eventName: string): Observable<VideoAction> {
    return fromEvent(rangeInput.nativeElement, eventName).pipe(
      map(({ target }) => {
        const { name, value } = target as HTMLInputElement;
        return {
          action: VideoActionEnum.RANGE_UPDATED,
          arg: {
            name: name as "volume" | "playbackRate",
            value: +value
          }
        }
      })
    );
  }

Subscribe RxJS observable and render Angular component

Use async pipe to subscribe this.videoPlayService.videoButtonIcon$ observable and update textContent of the button

videoButtonIcon$ = this.videoPlayerService.videoButtonIcon$.pipe(startWith('►'));

In inline template, update textContent attribute

<button class="player__button toggle" title="Toggle Play" [textContent]="videoButtonIcon$ | async" #toggle>►</button>

Similarly, the async pipe subscribes this.videoPlayerService.videoProgressBar$ to dynamically set the flex basis of the progress bar

videoProgressBar$ = this.videoPlayerService.videoProgressBar$

In inline template, bind the value to flexBasis style

<div class="progress" #progress>
   <div class="progress__filled" [style.flexBasis]="videoProgressBar$ | async">  
   </div>
</div>

Process the value of Subject in VideoPlayerComponent

In VideoPlayerControlsComponent, I call this.videoPlayerService.updateVideoAction(nextAction) a few times to push the value to subject.

In VideoPlayerComponent, I check the nextAction value in processAction and update the attributes of <video> element.

private processAction(videoNativeElement: HTMLVideoElement, nextAction: VideoAction): void {
    if (nextAction.action === VideoActionEnum.SKIP_BUTTON_CLICKED) {
      const seconds = nextAction.arg as number;
      videoNativeElement.currentTime = videoNativeElement.currentTime + seconds
    } else if (nextAction.action === VideoActionEnum.RANGE_UPDATED) {
      const rangeInput = nextAction.arg as VideoPlayerRangeInput;
      videoNativeElement[rangeInput.name] = rangeInput.value
    } else if (nextAction.action === VideoActionEnum.TOGGLE_PLAY) {
      const methodName = videoNativeElement.paused ? 'play' : 'pause';
      videoNativeElement[methodName]();
    } else if ([VideoActionEnum.PROGESS_BAR_CLICKED, VideoActionEnum.PROGRESS_BAR_DRAGGED].includes(nextAction.action)) {
      const proportion = nextAction.arg as number;
      videoNativeElement.currentTime = proportion * videoNativeElement.duration;
    }
 }

The video advances or rewind back to the new current time when the next action of the player is VideoActionEnum.SKIP_BUTTON_CLICKED, VideoActionEnum.PROGRESS_BAR_DRAGGED or VideoActionEnum.PROGESS_BAR_CLICKED.

When the next action is VideoActionEnum.SKIP_BUTTON_CLICKED, VideoActionEnum.PROGESS_BAR_CLICKED or VideoActionEnum.PROGRESS_BAR_DRAGGED, the code modifies the current time of the video.

The code modifies volume or playback rate attribute of the video when the next action is VideoActionEnum.RANGE_UPDATED.

The application is done and we have a custom vide player built with RxJS and Angular.

Final Thoughts

In this post, I show how to use RxJS and Angular to build a custom video player. The first takeaway is to encapsulate subject and observable in a shared service to exchange data between parent and child components. The second takeaway is to use async pipe to resolve observable such that developer does not have to clean up the subscription. The final take is when subscription is created in ngOnInit or ngAfterViewInput, developers need to unsubscribe in ngDestroy.

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

Create NestJS health check library

Reading Time: 6 minutes

 56 total views

Introduction

NestJS applications in my company implement health checking for k8s liveness probe. These applications check the following items are up and running: backend, database, redis, bull queue and external servers. Eventually, different backend teams encapsulate similar endpoint and services in a heath check module and import it to the application. This approach is not DRY and I decided to create a heath check library and reuse it in applications to replace the custom health check module.

In this blog post, I show the codes to create a nestjs health check library and use it in a sample application to check the health of backend, Postgres database, redis, bull queues and doc website of nestjs and angular.

let's go

Create health check library

First, create a new project called nestjs-health. Rename src/ to lib/ and update tsconfig.json and package.json respectively.

// Directory tree
lib
├── controllers
│   ├── health.controller.ts
│   └── index.ts
├── health-module-definition.ts
├── health.module.ts
├── index.ts
├── indicators
│   ├── bull-queue.health.ts
│   └── index.ts
├── interfaces
│   ├── health-module-options.interface.ts
│   └── index.ts
├── services
│   ├── health.service.ts
│   └── index.ts
└── types
    ├── index.ts
    └── redis-connection-options.type.ts
// tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "es2017",
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./lib",
    "incremental": true,
    "skipLibCheck": true,
    "strictNullChecks": false,
    "noImplicitAny": false,
    "strictBindCallApply": false,
    "forceConsistentCasingInFileNames": false,
    "noFallthroughCasesInSwitch": false,
    "esModuleInterop": true,
    "strict": true,
    "strictPropertyInitialization": false,
    "noImplicitThis": true,
    "noUnusedParameters": true,
    "noUnusedLocals": true,
    "noUncheckedIndexedAccess": true
  },
  "include": ["lib/**/*"],
  "exclude": ["dist", "test", "node_modules"]
}

Update outDir to ./dist and rootDir to ./lib. Include ['lib/**/*'] and excludes ["dist", "test", "node_modules"] from compilation

Add "main": "./dist/index.js" to package.json

Install dependencies

npm i --save-exact --save-dev @nestjs/terminus
npm i --save-exact @nestjs/microservices @nestjs/axios @nestjs/bull bull @nestjs/typeorm ts-node

Then, move some “dependencies” to “peerDependencies”

"peerDependencies": {
    "@nestjs/common": "^9.0.0",
    "@nestjs/core": "^9.0.0",
    "@nestjs/microservices": "^9.1.5",
    "@nestjs/axios": "^0.1.0",
    "@nestjs/bull": "^0.6.1",
    "bull": "^4.10.1",
    "@nestjs/typeorm": "^9.0.1",
    "ts-node": "^10.0.0"
}

Define HealthModuleOptions interface

First, we define HealthModuleOptions interface that enables us to pass configurations from application to the library

// redis-connection-options.type.ts

import { RedisOptions } from '@nestjs/microservices';

export type RedisConnectOptions = RedisOptions['options'];
// health-module-options.interface.ts

import { HealthIndicatorFunction } from '@nestjs/terminus';
import { RedisConnectOptions } from '../types';

export interface HealthModuleOptions {
  app: string;
  backendUrl: string;
  shouldCheckDatabase?: boolean;
  queueNames?: string[];
  redisOptions?: RedisConnectOptions;
  indicatorFunctions?: HealthIndicatorFunction[];
}
  • app – key of the backend application
  • backendUrl: URL of the backend application, for example, http://localhost:3000
  • shouldCheckDatabase: determine whether or not ping the database that uses TypeORM
  • queueNames: Name array of bull queues
  • indicatorFunctions: custom indicator functions designed to check components’ health

Define Health Module Definition

Define a ConfigurableModuleBuilder to export ConfigurationModuleClass and MODULE_OPTIONS_TOKEN.

Invoke setClassMethodName to override the static methods of HealthModule to forRoot and forRootAsync

import { ConfigurableModuleBuilder } from '@nestjs/common';
import { HealthModuleOptions } from './interfaces';

export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder<HealthModuleOptions>()
  .setClassMethodName('forRoot')
  .build();

Create module for the health check library

Create HealthModule that extends ConfigurableModuleClass

import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { ConfigurableModuleClass } from './health-module-definition';

@Module({
  imports: [TerminusModule],
  controllers: [],
  providers: [],
  exports: [],
})
export class HealthModule extends ConfigurableModuleClass {}

controllers, providers and exports are empty arrays but we will update them after generating services and controller.

Generate custom health indicator

The following code perform health check on arbitrary number of bull queues. A queue is up and can accept jobs when it connects to redis and redis status is ready.

// indicators/bull-queue.health.ts

import { getQueueToken } from '@nestjs/bull';
import { Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from '@nestjs/terminus';
import { Queue } from 'bull';

@Injectable()
export class BullQueueHealthIndicator extends HealthIndicator {
  constructor(private moduleRef: ModuleRef) {
    super();
  }

  async isHealthy(queues: string[]): Promise<HealthIndicatorResult> {
    const promiseResults = await this.checkQueuesReady(queues);
    const errorResults = this.filterErrors(promiseResults);

    if (errorResults.length) {
      throw new HealthCheckError('Bull queue failed', this.getStatus('bull', false, { errors: errorResults }));
    }

    return this.getStatus('bull', true);
  }

  private filterErrors(promiseResults: PromiseSettledResult<boolean>[]): string[] {
    const errorResults: string[] = [];
    for (const promiseResult of promiseResults) {
      if (promiseResult.status === 'rejected') {
        if (promiseResult.reason instanceof Error) {
          errorResults.push(promiseResult.reason.message);
        } else {
          errorResults.push(promiseResult.reason);
        }
      }
    }
    return errorResults;
  }

  private async checkQueuesReady(queues: string[]) {
    const promises = queues.map(async (name) => {
      const queueToken = this.moduleRef.get(getQueueToken(name), { strict: false });
      if ((queueToken as Queue).isReady) {
        const queue = await (queueToken as Queue).isReady();
        const isEveryClientReady = queue.clients.every((client) => client.status === 'ready');
        if (!isEveryClientReady) {
          throw new Error(`${name} - some redis clients are not ready`);
        }
        return true;
      }
      throw new Error(`${name} is not a bull queue`);
    });

    return Promise.allSettled(promises);
  }
}

Create health checking service

In the health service, I inject MODULE_OPTIONS_TOKEN injection token to obtain a reference to HealthModuleOptions. Then, I examine the options to determine the components to perform health check.

// services/health.service.ts

import { MODULE_OPTIONS_TOKEN } from '../health-module-definition';
import { HealthModuleOptions } from '../interfaces';
import { BullQueueHealthIndicator } from '../indicators';

@Injectable()
export class HealthService {
  constructor(
    @Inject(MODULE_OPTIONS_TOKEN) private options: HealthModuleOptions,
    private health: HealthCheckService,
    private http: HttpHealthIndicator,
    private db: TypeOrmHealthIndicator,
    private bull: BullQueueHealthIndicator,
    private microservice: MicroserviceHealthIndicator,
  ) {}

  check(): Promise<HealthCheckResult> {
    const indicatorFunctions: HealthIndicatorFunction[] = [];

    indicatorFunctions.push(() => this.http.pingCheck(this.options.app, this.options.backendUrl));

    if (this.options.shouldCheckDatabase) {
      indicatorFunctions.push(() => this.db.pingCheck('database'));
    }

    if (this.options.redisOptions) {
      indicatorFunctions.push(() =>
        this.microservice.pingCheck<RedisOptions>('redis', {
          transport: Transport.REDIS,
          options: this.options.redisOptions,
          timeout: 5000,
        }),
      );
    }

    if (this.options.queueNames?.length) {
      indicatorFunctions.push(() => this.bull.isHealthy(this.options.queueNames));
    }

    if (this.options.indicatorFunctions?.length) {
      indicatorFunctions.push(...this.options.indicatorFunctions);
    }

    return this.health.check(indicatorFunctions);
  }
}

Create health controller

The controller has one endpoint, /health, that injects HeathService to check the readiness of the components.

import { Controller, Get } from '@nestjs/common';
import { HealthCheck, HealthCheckResult } from '@nestjs/terminus';
import { HealthService } from '../services';

@Controller('health')
export class HealthController {
  constructor(private health: HealthService) {}

  @Get()
  @HealthCheck()
  check(): Promise<HealthCheckResult> {
    return this.health.check();
  }
}

Register controller and services

Now, we register new controller and services in HealthModule

// heath.module.ts

import { Module } from '@nestjs/common';
... other import statements ...
import { HealthService } from './services';
import { HealthController } from './controllers';
import { BullQueueHealthIndicator } from './indicators';

@Module({
  imports: [TerminusModule],
  controllers: [HealthController],
  providers: [HealthService, BullQueueHealthIndicator],
  exports: [HealthService],
})
export class HealthModule extends ConfigurableModuleClass {}

Finally, add index.ts to export controllers, interfaces, services, types and the health module

./index.ts

export * from './interfaces';
export * from './services';
export * from './health.module';
export * from './types';

I finished the implementation of the library and will publish it to npmjs.com. Npm publish is not part of the scope of this article.

Use case of health checking

I would continue to use nestjs-health-terminus project to demonstrate how to health check the following resources:

  • backend app
  • Postgres database that uses TypeORM
  • redis
  • bull queues
  • just for fun, ping docs.nestjs.com and angular.io/docs are alive

Clone or fork the Nestjs project

You can find the sample code of nestjs bull queue in this repo: https://github.com/railsstudent/nestjs-health-terminus

First, copy .env.example to .env to load environment variables of redis and Postgres database

REDIS_PORT=6379
REDIS_HOST=localhost
PORT=3000
NODE_DEV=development
BACKEND_DOMAIN=http://localhost:3000
DATABASE_CONNECT=postgres
DATABASE_HOST=localhost
DATABASE_USER=postgres
DATABASE_PASSWORD=postgres
DATABASE_NAME=testDB
DATABASE_PORT=5432

Second, run docker-compose to create and run redis and Postgres

docker-compose up -d

Install dependencies

Install nestjs-health library

npm install --save-exact nestjs-health

Import health module to AppModule

First, create health.config.ts in src/configs folder

import { ConfigService } from '@nestjs/config';
import { HttpHealthIndicator } from '@nestjs/terminus';
import { HealthModule } from 'nestjs-health';

export const healthConfig = HealthModule.forRootAsync({
  inject: [ConfigService, HttpHealthIndicator],
  useFactory: (configService: ConfigService, http: HttpHealthIndicator) => {
    return {
      app: 'nestjs-health-terminus',
      backendUrl: configService.get<string>('BACKEND_DOMAIN', ''),
      shouldCheckDatabase: true,
      queueNames: ['fibonacci', 'prime'],
      redisOptions: {
        host: configService.get<string>('REDIS_HOST', 'localhost'),
        port: configService.get<number>('REDIS_PORT', 0),
      },
      indicatorFunctions: [
        () => http.pingCheck('nestjs-docs', 'https://docs.nestjs.com'),
        () => http.pingCheck('angular-docs', 'https://angular.io/docs'),
      ],
    };
  },
});

heathConfig variable configures dependent resources that require health checking.

Next, import healthConfig in AppModule

@Module({
  imports: [
    ... other modules...
    healthConfig,
  ]
})
export class AppModule {}

Invoke health check endpoint

Run docker-compose to start redis. Since both fibonacci and prime queues can connect to redis, the health check status should be “up”

http://localhost:3000/health

Kill redis and call the same endpoint to display a different response.

docker stop <container id of redis>

redis and bull display health check error messages.

Kill Postgres and call the same endpoint to display a new response.

docker stop <container id of Postgres>

Final Thoughts

In this post, I show how to author a NestJS library to encapsulate health check services and controller in a dynamic module. Then, I publish the library to npmjs and install it into NestJS application to check the health of backend application, database, redis, bull queues and external servers. Finally, the library provides a defined /health endpoint that displays the status of components.

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in NestJS and other technologies.

Resources:

  1. NestJS Heath Library: https://github.com/railsstudent/nestjs-health
  2. Repo: https://github.com/railsstudent/nestjs-health-terminus
  3. npm package: https://www.npmjs.com/package/nestjs-health
  4. Terminus: https://docs.nestjs.com/recipes/terminus
  5. Dynamic Module: https://docs.nestjs.com/fundamentals/dynamic-modules#dynamic-modules

Check health of nestjs bull queue with terminus

Reading Time: 5 minutes

 48 total views

Introduction

It is common for enterprise applications to connect to external resources to process requests and get back responses. Therefore, application should know that the components it depends on are up and running all the times. Otherwise, the application behaves erroneously when serving client requests. To stay aware of the availability of connected resources, we can perform health check on them and wait for the results.

In this blog post, I describe the use case of health check bull queue in nestjs with terminus. First, it requires a custom health indicator to verify that the queue is connected to redis and redis status is ready. Next, I add a /health endpoint that calls the health indicator to return the status.

let's go

Clone or fork the Nestjs project

You can find the sample code of nestjs bull queue in this repo: https://github.com/railsstudent/nestjs-health-terminus

nest generate nestjs-health-terminus

Install dependencies

Install nestjs terminus library to the project

npm install --save @nestjs/terminus

Create health module for health check

nest g mo health

Import HealthModule in AppModule

@Module({
  imports: [
    ... other modules...
    HealthModule,
  ]
})
export class AppModule {}

Add a custom health indicator to health check bull queue

@nestjs/terminus does not provide built-in health indicator for bull queue; therefore, I create a custom one in the health module.

First, Use nestjs CLI to generate a health indicator

nest g s health/health/bullQueue --flat

My convention is to rename the filename suffix from .service.ts to .health.ts.

src/health
├── controllers
│   ├── health.controller.ts
│   └── index.ts
├── health
│   ├── bull-queue.health.ts
│   └── index.ts
├── health.module.ts
└── index.ts

Then, open bull-queue.health.ts and extends BullQueueHealthIndicator from HealthIndicator.

import { getQueueToken } from '@nestjs/bull';
import { Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from '@nestjs/terminus';
import { Queue } from 'bull';

@Injectable()
export class BullQueueHealthIndicator extends HealthIndicator {
  constructor(private moduleRef: ModuleRef) {
    super();
  }
}

Next, import TerminusModule into HealthModule and verify that the providers array has the entry of BullQueueHealthIndicator.

import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { HealthController } from './controllers';
import { BullQueueHealthIndicator } from './health';

@Module({
  imports: [TerminusModule],
  controllers: [HealthController],
  providers: [BullQueueHealthIndicator],
})
export class HealthModule {}

Add health check logic in custom health indicator

In BullQueueHealthIndicator, I implement isHealthy() method to check the health of fibonacci and prime queues. When the queue connects to redis and redis status is ready, it is considered up. Otherwise, the queue is down and the indicator returns error messages.

async isHealthy(queues: string[]): Promise<HealthIndicatorResult> {
    const promiseResults = await this.checkQueuesReady(queues);
    const errorResults = this.filterErrors(promiseResults);

    if (errorResults.length) {
      throw new HealthCheckError('Bull queue failed', this.getStatus('bull', 
         false, { errors: errorResults }));
    }

    return this.getStatus('bull', true);
 }

checkQueuesReady accepts a list of queue names and examines their readiness.

private async checkQueuesReady(queues: string[]) {
    const promises = queues.map(async (name) => {
      const queueToken = this.moduleRef.get(getQueueToken(name), 
          { strict: false });
      if ((queueToken as Queue).isReady) {
        const queue = await (queueToken as Queue).isReady();
        const isEveryClientReady = queue.clients.every((client) => client.status === 'ready');
        if (!isEveryClientReady) {
          throw new Error(`${name} - some redis clients are not ready`);
        }
        return true;
      }
      throw new Error(`${name} is not a bull queue`);
    });

    return Promise.allSettled(promises);
}

const queueToken = this.moduleRef.get(getQueueToken(name), { strict: false });

The code uses the queue token to obtain the queue instance. When the queue is ready, I get queue.clients that is an array of redis clients. When every redis client has ready status, the queue is ready to accept jobs. On the other hand, when redis store is not set up, the method throws an error message.

private filterErrors(promiseResults: PromiseSettledResult<boolean>[]): string[] {
    const errorResults: string[] = [];
    for (const promiseResult of promiseResults) {
      if (promiseResult.status === 'rejected') {
        if (promiseResult.reason instanceof Error) {
          errorResults.push(promiseResult.reason.message);
        } else {
          errorResults.push(promiseResult.reason);
        }
      }
    }
    return errorResults;
 }

The result of checkQueuesReady is a collection of resolved and rejected promises. filterErrors iterates the promise results, finds those that are rejected and stores the error messages.

When health check returns error messages, isHealthy throws HealthCheckError.

throw new HealthCheckError('Bull queue failed', this.getStatus('bull', false, 
    { errors: errorResults }));

When health check does not find error, isHealthy returns

this.getStatus('bull', true);

Define health controller

nest g co health/controllers/health --flat

import { Controller, Get } from '@nestjs/common';
import { HealthCheck, HealthCheckService } from '@nestjs/terminus';
import { BullQueueHealthIndicator } from '../health/bull-queue.health';

@Controller('health')
export class HealthController {
  constructor(private health: HealthCheckService, private bull: BullQueueHealthIndicator) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([() => this.bull.isHealthy(['fibonacci', 'prime'])]);
  }
}

First, the GET endpoint is annotated with a HealthCheck decorator. Second, the HealthCheckService expects some health indicators to execute. Therefore, I provide BullQueueHealthIndicator to the check method to process.

this.health.check([() => this.bull.isHealthy(['fibonacci', 'prime'])])

checks the health of fibonacci and prime queues and maps the health status to the object with key “bull”.

Invoke health check endpoint

Run docker-compose to start redis. Since both fibonacci and prime queues can connect to redis, the health check status should be “up”

http://localhost:3000/health

Kill docker-compose and call the same endpoint to see a different response.

Both queues are down because redis has not launched yet.

Suppose I also check ‘dummy’ that is not a bull queue, isHealthy outputs bad injection token error message.

return this.health.check([() => this.bull.isHealthy(['fibonacci', 'prime', 'dummy'])]);

Final Thoughts

In this post, I show how to use terminus package to write a custom health indicator to check the health of bull queue. When terminus package does not provide predefined indicators and there is no open source library, we have the ability to write our own indicators to perform the appropriate testing.

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in NestJS and other technologies.

Resources:

  1. Repo: https://github.com/railsstudent/nestjs-health-terminus
  2. Queue: https://docs.nestjs.com/techniques/queues
  3. Bull: https://github.com/OptimalBits/bull
  4. Terminus: https://docs.nestjs.com/recipes/terminus

Queue and job processing in nestjs

Reading Time: 5 minutes

 63 total views,  1 views today

Introduction

The responsibility of backend application is to handle client requests and return responses back to the client. However, backend performance can degrade when the request is resource intensive; UI thread waits endlessly and user interface is blocked for further user actions. To avoid the unresponsiveness, we can place jobs in queue and delegate the processor to manage job processing.

In this blog post, I describe how to register bull queues in a Nestjs project and add jobs to queue to run in the backend. When processor consumes the job from the queue and finishes job processing, the queue listener logs message to terminal.

let's go

Create a new Nestjs project

nest generate nestjs-health-terminus

Install dependencies

Install bull queue package for nestjs and other dependencies for configuration and validation.

npm i @nestjs/bull bull
npm i @nestjs/config class-validator class-transformer
npm i --save-dev @types/bull

Add Docker Compose to install redis

version: '3.1'

services:
  redis:
    container_name: redis
    image: redis:6-alpine
    restart: always
    ports:
      - '6379:6379'
    volumes: 
      - cache:/data
    networks:
      - terminus

volumes:
  cache:

networks:
  terminus:
    driver: bridge

When we launch docker compose, it will install and run Redis 6 on port number 6379.

Import Bull module used by queues

//.env
REDIS_PORT=6379
REDIS_HOST=localhost
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    BullModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        redis: {
          host: configService.get<string>('REDIS_HOST'),
          port: configService.get<number>('REDIS_PORT'),
        },
      }),
    }),
  ]
})
export class AppModule {}

Register bull queues to process jobs

To enable configService to find the values of REDIS_HOST and REDIS_PORT, the project needs to provide a .env with the environment variables.

// .env

REDIS_HOST=localhost
REDIS_PORT=6379

In tutorial, it is tempting to hardcode the values but professional developers should not do it in production application.

First, we create a queue module to demonstrate some examples of queue and process jobs.

import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common';
import { FibonacciService, PrimeService } from './services';
import { QueueController } from './controllers';
import { PrimeProcessor, FibonacciProcessor } from './processors';

@Module({
  imports: [
    BullModule.registerQueue({
      name: 'fibonacci',
    }),
    BullModule.registerQueue({
      name: 'prime',
    }),
  ],
  providers: [FibonacciService, FibonacciProcessor, PrimeProcessor, PrimeService],
  controllers: [QueueController],
})
export class QueueModule {}

The module is consisted of the following

  • Two bull queues, fibonacci and prime, respectively
  • Queue controller that adds jobs to fibonacci and prime queues
  • FibonacciService that calculates the first Nth fibonacci numbers
  • FibonacciProcessor that processes jobs in fibonacci queue
  • PrimeService that solves two problems of prime factors
  • PrimeProcessor that processes “prime-factors” and “distinct-prime-factors” jobs

Add jobs to queue and wait for job processing

I inject queues to add job in the controller in my example but queuing can also occur in services. If the background job depends on the previous results in the service, then we will inject the queue into the service and append jobs to it inside the method.

@Controller('queue')
export class QueueController {
  constructor(
    @InjectQueue('fibonacci') private fibonacciQueue: Queue<{ order: number }>,
  ) {}

  @Post('fib')
  async getFibonacci(@Query('order', ParseIntPipe) order: number): Promise<void>   {
    console.log(`${new Date()} - Job submitted to queue`, order);
    await this.fibonacciQueue.add({ order }, { delay: 1000 });
  }
}

Fibonacci queue has a single job that is to calculate the first Nth fibonacci numbers a

await this.fibonacciQueue.add({ order }, { delay: 1000 });

adds job with data { order } and delays by one second ({ delay: 1000 }).

Add named jobs to queue for job processing

@Controller('queue')
export class QueueController {
  constructor(@InjectQueue('prime') private primeQueue: Queue) {}


  @Post('prime-factors')
  async getPrimeFactors(@Query('input', ParseIntPipe) input: number): Promise<void> {
    console.log(`${new Date()} - Prime factors job submitted to prime queue`, input);
    await this.primeQueue.add('prime-factors', { input }, { delay: 1000 });
  }

  @Post('distinct-prime-factors')
  async getDistinctPrimeFactors(@Body() arrayDto: ArrayProductsDto): Promise<void> {
    console.log(`${new Date()} - Distinct prime factor job submitted to prime queue`, arrayDto.products);
    await this.primeQueue.add('distinct-prime-factors', {
       products: arrayDto.products,
    });
  }
}

On the other hand, prime queue manages two types of jobs. Therefore, we specify the job name when appending job to the queue.

await this.primeQueue.add('prime-factors', { input }, { delay: 1000 });

The code snippet appends ‘prime-factors’ job to queue to find all prime factors of an integer

await this.primeQueue.add('distinct-prime-factors', {
   products: arrayDto.products,
});

The other code snippet appends ‘distinct-prime-factors’ job to queue to find distinct prime factors of an integer.

Next, we define job processors to process the jobs such that they don’t stay in idle status in the queues.

Define job processors for job processing

It is really easy to create job processors in nestJs. Job processor is a class annotated by @Processor() decorator and queue name. Each method has a @process() decorator and an optional job name to consume queue job.

@Processor('fibonacci')
export class FibonacciProcessor {
  constructor(private fibonacciService: FibonacciService) {}

  @Process()
  calculateNFibonacciNumbers({ data }: Job<{ order: number }>): void {
    const fibSequences = this.fibonacciService.fibonacci(data.order);
    console.log(`${new Date()} Calculating ${data.order + 1} fibonacci numbers...`);
    for (let i = 0; i < data.order; i++) {
      console.log(`${new Date()} - Fib(${i}) = ${fibSequences[i]}`);
    }
  }
}

@Processor('fibonacci') listens to fibonacci queue registered in queue module. The method, calcualteNFibonacciNumbers, is responsible for all jobs of fibonacci queue because @Process() decorator does not specify a job name. In my opinion, the method will violate single responsibility principle if it processes all types of jobs of the queue.

Define specialized job method for job processing

If we want a process method to take care of a single type of job, @Process() decorator accepts name parameter that represents job name.

import { Process, Processor } from '@nestjs/bull';
import { Job } from 'bull';
import { PrimeService } from '../services';

@Processor('prime')
export class PrimeProcessor {
  constructor(private primeService: PrimeService) {}

  @Process('prime-factors')
  findPrimeFactors({ data }: Job<{ input: number }>): void {
    const primeFactors = this.primeService.getPrimeFactors(data.input);
    console.log(`${new Date()} - All prime factors`, primeFactors);
  }

  @Process('distinct-prime-factors')
  findDistinctPrimeFactors({ data }: Job<{ products: number[] }>): void {
    const { primeFactors, count } = this.primeService.getDistinctPrimeFactors(data.products);
    console.log(`${new Date()} - Distinct prime factors`, primeFactors, 'count:', count);
  }
}

findPrimeFactors receives 'prime-factors' job and determines all prime factors of an integer. Similarly, findDistinctPrimeFactors receives 'distinct-prime-factors' job and determines distinct prime factors and the number of them. It is a clean approach than if-then-else to execute different methods of primeService to return the expected results.

Listen to queue events during the lifecycle of job processing

Queue events of Bull offer pre-process and post-process hooks for developers to perform custom actions such as logging and caching. When the events are local, they must implement within the processor such as PrimeProcessor.

The code here implements OnQueueActive and OnQueueCompleted to log start and end time of the job.

@Processor('prime')
export class PrimeProcessor {
  constructor(private primeService: PrimeService) {}

 ...

 @OnQueueActive()
 onJobActive(job: Job) {
    console.log(
      `onJobActive - id: ${job.id}, name: ${job.name}, data: `,
      job.data,
      ` starts at ${new Date(job.timestamp)}`,
    );
 }

 @OnQueueCompleted()
 onJobSuccess(job: Job, result: any) {
    console.log(
      `onJobSuccess - id: ${job.id}, name: ${job.name}, data: `,
      job.data,
      ` completes at ${new Date(job.finishedOn)}`,
      'result',
      result,
    );
 }

Execute cURL request

curl --location --request POST 'http://localhost:3000/queue/prime-factors?input=88'

Console log output:

onJobActive - id: 1, name: prime-factors, data:  { input: 88 }  starts at Sat Sep 17 2022 11:20:54 GMT+0800 (Hong Kong Standard Time)
onJobSuccess - id: 1, name: prime-factors, data:  { input: 88 }  completes at Sat Sep 17 2022 11:20:55 GMT+0800 (Hong Kong Standard Time) result undefined

Final Thoughts

In this post, we show a comprehensive bull queue example in Nestjs. When an application has a task that is expected to run a long time, we have the option to place it in the queue to process somewhere. Then, we do not block the main event loop and ensure UI is responsive for further user actions.

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in NestJS and other technologies.

Resources:

  1. Queue: https://docs.nestjs.com/techniques/queues
  2. Bull: https://github.com/OptimalBits/bull
  3. Repo: https://github.com/railsstudent/nestjs-health-terminus

Lint files and dependencies in NestJs with unimported

Reading Time: 5 minutes

 81 total views

Introduction

When developers maintain a NestJS project, they add and delete codes to the project that some dependencies and files become redundant. When unused files and dependencies are left around, they become technical debts and reduce the code quality. Fortunately, we can use unimported to lint files and dependencies before git commit to avoid pushing unused files and dependencies to the code base.

In this blog post, I describe how to execute unimported binary to add .unimportedrc.json to a Nestjs project and create a pre-commit hook to lint files and dependencies of the projects during git commit.

let's go

Create a new Nestjs project

nest generate test-app

Add .unimportedrc.json to lint files and dependencies

First, we execute unimported binary to initialize .unimportedrc.json to the project

npx unimported --init

We will find a barebone .unimportedrc.json after the initialization.

{
  "ignorePatterns": [
    "**/node_modules/**",
    "**/*.tests.{js,jsx,ts,tsx}",
    "**/*.test.{js,jsx,ts,tsx}",
    "**/*.spec.{js,jsx,ts,tsx}",
    "**/tests/**",
    "**/__tests__/**",
    "**/*.d.ts"
  ],
  "ignoreUnimported": [],
  "ignoreUnused": [],
  "ignoreUnresolved": []
}

Most of the Nestjs projects that I work on use TypeOrm to generate migration scripts and interface with PostgreSQL; therefore, I want unimported to ignore **/migrations/** folder in ignorePatterns array.

{
  "ignorePatterns": [
    "**/node_modules/**",
    "**/*.tests.{js,jsx,ts,tsx}",
    "**/*.test.{js,jsx,ts,tsx}",
    "**/*.spec.{js,jsx,ts,tsx}",
    "**/tests/**",
    "**/__tests__/**",
    "**/*.d.ts",
    "**/migrations/**"
  ],
  "ignoreUnimported": [],
  "ignoreUnused": [],
  "ignoreUnresolved": []
}

NestJs is nodejs under the hood and unimported also supports a node preset.

npx unimported --show-preset        // show all available presets
npx unimported --show-preset node   // shows the json file of node preset

The following is the output of the configuration:

{
  preset: 'node',
  entry: [ 'src/main.ts' ],
  extensions: [ '.js', '.jsx', '.ts', '.tsx' ],
  flow: false,
  ignorePatterns: [
    '**/node_modules/**',
    '**/*.tests.{js,jsx,ts,tsx}',
    '**/*.test.{js,jsx,ts,tsx}',
    '**/*.spec.{js,jsx,ts,tsx}',
    '**/tests/**',
    '**/__tests__/**',
    '**/*.d.ts'
  ],
  ignoreUnimported: [],
  ignoreUnresolved: [],
  ignoreUnused: []
}

We copy preset, entry and extensions to the config file in the project and the configuration file finally completes.

{
  "preset": "node",
  "entry": [
    "src/main.ts"
  ],
  "extensions": [
    ".js",
    ".jsx",
    ".ts",
    ".tsx"
  ],
  "ignorePatterns": [
    "**/node_modules/**",
    "**/*.tests.{js,jsx,ts,tsx}",
    "**/*.test.{js,jsx,ts,tsx}",
    "**/*.spec.{js,jsx,ts,tsx}",
    "**/tests/**",
    "**/__tests__/**",
    "**/*.d.ts",
    "**/migrations/**"
  ],
  "ignoreUnimported": [],
  "ignoreUnused": [],
  "ignoreUnresolved": []
}

Create pre-commit hook to perform unimported check

First, we install husky devDependencies and add prepare script in package.json.

npm install husky --save-dev --save-exact
npm set-script prepare "husky install"
npm run prepare

Then, we add a pre-commit husky hook

npx husky add .husky/pre-commit "npx --no unimported"
git add .husky/pre-commit
git commit -m "chore: test unimported check"

If we see the follow output in the terminal, pre-commit hook has successfully performed unimported check.

✓ There don't seem to be any unimported files.
[master f33fe57] chore: test unimported check
 1 file changed, 2 insertions(+), 1 deletion(-)

Up to this point, we focus on setting up code quality tools for the project. Now, we are ready to demonstrate the power of unimported by adding unused file(s) and dependencies. In the demo, husky hook aborts whenever we commit the source codes and unimported prints the report card in the terminal. Then, we systematically modify the codes until we satisfy unimported check and commit the changes.

Demo unimported to lint files and dependencies

The demo is to add configuration and .env validation in Nestjs according to the official documentation https://docs.nestjs.com/techniques/configuration.

First, install dependencies

npm i --save-exact @nestjs/config dotenv joi
git add .
git commit -m "chore(deps): install dependencies"
       summary               unimported v1.21.0 (node)
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
       entry file          : src/main.ts

       unresolved imports  : 0
       unused dependencies : 3
       unimported files    : 0

─────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
      │ 3 unused dependencies
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1  │ @nestjs/config
   2  │ dotenv
   3  │ joi
─────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
       Inspect the results and run npx unimported -u to update ignore lists
husky - pre-commit hook exited with code 1 (error)

We have 3 unused dependencies but @nestjs/config will go away when we import ConfigModule in AppModule.

import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
git add .
git commit -m "chore(deps): install dependencies"

       summary               unimported v1.21.0 (node)
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
       entry file          : src/main.ts

       unresolved imports  : 0
       unused dependencies : 2
       unimported files    : 0

─────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
      │ 2 unused dependencies
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1  │ dotenv
   2  │ joi
─────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

We have resolved 1 unused dependency and 2 remain. Next, we use joi to validate the schema of .env.

Let’s create a new file to define the schema of .env file

touch src/validation-schema.ts

Then, we copy and paste the schema to the TypeScript file

// validation-schema.ts
import * as Joi from 'joi';

export const validationSchema = Joi.object({
  NODE_ENV: Joi.string()
    .valid('development', 'production', 'test', 'provision')
    .default('development'),
  PORT: Joi.number().default(3000),
});

If we commit the codes now, unimported will detect one unused file and 2 unused dependencies.

       summary               unimported v1.21.0 (node)
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
       entry file          : src/main.ts

       unresolved imports  : 0
       unused dependencies : 2
       unimported files    : 1

─────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
      │ 2 unused dependencies
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1  │ dotenv
   2  │ joi
─────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

─────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
      │ 1 unimported files
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1  │ src/validation-schema.ts
─────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

It seems umimported report card is worse than before but it is alright. We forgot to import validation-schema.ts in app.module.ts and pass the validationSchema object to ConfigModule.

Update app.module.ts and commit again

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { validationSchema } from './validation-schema';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

       summary               unimported v1.21.0 (node)
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
       entry file          : src/main.ts

       unresolved imports  : 0
       unused dependencies : 1
       unimported files    : 0

─────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
      │ 1 unused dependencies
─────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   1  │ dotenv
─────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Why is dotenv unused? I referred to package.json of @nestjs/config and found out that dotenv is a dependency of the library. I don’t require to install dotenv in the project and should remove it.

We can make it happen by running npm uninstall dotenv. Let’s commit the codes and not keep the suspense further.

git add .
connie@connie-ThinkPad-T450s ~/Documents/ws_react_graphql_nest/test-app $ git commit -m "chore(deps): install dependencies and add configuration"
✓ There don't seem to be any unimported files.
[redo-demo 44cbf8d] chore(deps): install dependencies and add configuration
 5 files changed, 6 insertions(+), 7 deletions(-)
 rename src/{validationSchema.ts => validation-schema.ts} (100%)

We have finally committed the changes and remove dotenv dependency that the project does not require.

Final Thoughts

In this post, we see how easy it is leave unused files and dependencies in the code base. Fortunately, we can apply unimported command in pre-commit hook to lint files and dependencies before code commit. When Nestjs project has unused files and dependences, code commit fails until we resolve unimported errors. This way, we can keep clean code and avoid introducing technical debts that the successors have to deal with.

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in NestJS and other technologies.

Resources:

  1. unimported: https://www.npmjs.com/package/unimported
  2. husky: https://www.npmjs.com/package/husky