Create an analog clock using RxJS and Angular standalone components

Connie Leung - Feb 18 '23 - - Dev Community

Introduction

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

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

Create a new Angular project

ng generate application day2-ng-and-css-clock
Enter fullscreen mode Exit fullscreen mode

Bootstrap AppComponent

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

// app.component.ts

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

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

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

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

// main.ts

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

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

bootstrapApplication(AppComponent).catch(err => console.error(err));
Enter fullscreen mode Exit fullscreen mode

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

Declare Clock component

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

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

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

// clock.component.ts

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

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

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

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

Create RxJS custom operators

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

// clock.operator.ts

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

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

// clock.operator.ts

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

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

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

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

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

Use RxJS and Angular to implement observable in clock component

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

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

Final Thoughts

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

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

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

Resources:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .