Build a countdown timer using RxJS and Angular standalone components
Tags: angular, typescript, rxjs, tutorial
Published: 2023-03-07 14:55:59

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

###Create a new Angular project
```basg
ng generate application day29-countdown-timer-standalone
```

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

```typescript
// app.component.ts

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

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

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

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

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

```typescript
// main.ts

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

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

Next, I can delete AppModule that is now useless.

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

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

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

```typescript
// timer.component.ts

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

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

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

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

```typescript
// timer-controls.component.ts

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

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

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

  customMinutes = '';
  subscriptions!: Subscription;
  timerInputSubscription = timerInputSubscriptionFn();

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

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

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

```typescript
// timer-button.directive.ts

import { Directive, ElementRef } from '@angular/core';
import { Observable, fromEvent, map } from 'rxjs';

@Directive({
  selector: '[appTimerButton]',
  standalone: true
})
export class TimerButtonDirective {
  click$!: Observable<number>;

  constructor(el: ElementRef<HTMLButtonElement>) {
    const totalSeconds = +(el.nativeElement.dataset['seconds'] || '0');
    this.click$ = fromEvent(el.nativeElement, 'click').pipe(map(() => totalSeconds));
  }
}
```

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

```typescript
// timer-controls.component.ts 

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

```typescript
// timer-pane.components

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

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

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

###Add timer service to share RxJS subjects and observables

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

```typescript
// timer.service.ts

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

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

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

####Demystifies RxJS logic in TimerControlsComponent

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

```typescript
timerInputSubscription = timerInputSubscriptionFn();
```

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

```typescript
// timer-controls.helper.ts

import { inject } from '@angular/core';
import { Observable, merge } from 'rxjs';
import { TimerService } from '../services/timer.service';
  
export const timerInputSubscriptionFn = () => {
    const timerService = inject(TimerService);
    return (observables: Observable<number>[]) => merge(...observables).subscribe((seconds) => timerService.updateSeconds(seconds));
}
```

####Explain timerInputSubscriptionFn

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

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

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

```typescript
// timer-pane.helper.ts

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

const oneSecond = 1000;

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

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

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

`displayTimeLeftFn` simulates the count down effect reactively.

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

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

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

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

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

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

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

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

##Resources:

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