Create an analog clock using RxJS and Angular standalone components
Connie Leung
Posted on February 18, 2023
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
Bootstrap AppComponent
First, I convert AppComponent
into standalone component such that I can bootstrap AppComponent
and inject providers in main.ts.
// app.component.ts
import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ClockComponent } from './clock';
@Component({
selector: 'app-root',
standalone: true,
imports: [
ClockComponent
],
template: '<app-clock></app-clock>',
styles: [`
:host {
display: block;
}
`],
})
export class AppComponent {
constructor(titleService: Title) {
titleService.setTitle('Day 2 NG and CSS Clock');
}
}
In Component decorator, I put standalone: true
to convert AppComponent
into a standalone component.
Instead of importing ClockComponent
in AppModule, I import ClockComponent
(that is also a standalone component) in the imports array because the inline template references it.
// main.ts
import { enableProdMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
bootstrapApplication(AppComponent).catch(err => console.error(err));
Second, I delete AppModule because it is not used anymore.
Declare Clock component
I declare standalone component, ClockComponent
, to create an analog clock. To verify the component is a standalone, standalone: true
is specified in the Component decorator.
src/app
├── app.component.ts
└── clock
├── clock.component.ts
├── clock.interface.ts
├── custom-operators
│ └── clock.operator.ts
└── index.ts
clock-operators.ts
encapsulates two custom RxJS operators that help draw the clock hands of the analog clock. currentTime
operator returns seconds, minutes and hours of the current time. rotateClockHands
receives the results of currentTime and calculate the rotation angle of the hands.
// clock.component.ts
import { AsyncPipe, NgIf } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { timer } from 'rxjs';
import { currentTime, rotateClockHands } from './custom-operators/clock.operator';
@Component({
selector: 'app-clock',
standalone: true,
imports: [
AsyncPipe,
NgIf,
],
template: `
<div class="clock" *ngIf="clockHandsTransform$ | async as clockHandsTransform">
<div class="clock-face">
<div class="hand hour-hand" [style.transform]="clockHandsTransform.hourHandTransform"></div>
<div class="hand min-hand" [style.transform]="clockHandsTransform.minuteHandTransform"></div>
<div class="hand second-hand" [style.transform]="clockHandsTransform.secondHandTransform"></div>
</div>
</div>
`,
styles: [...omitted due to brevity...],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ClockComponent {
readonly oneSecond = 1000;
clockHandsTransform$ = timer(0, this.oneSecond)
.pipe(
currentTime(),
rotateClockHands(),
);
}
ClockComponent
imports NgIf
and AsyncPipe
because the components uses ngIf
and async
keywords to resolve clockHandsTransform$
observable. clockHandsTransform$
is an observable that is consisted of the CSS styles to draw hour, minute and second hands. The observable is succinct because the currentTime
and rotateClockHands
custom operators encapsulate the logic.
Create RxJS custom operators
It is a matter of taste but I prefer to refactor RxJS operators into custom operators when observable has many lines of code. For clockHandsTransform$
, I refactor map into custom operators and reuse them in ClockComponent
.
// clock.operator.ts
export function currentTime() {
return map(() => {
const time = new Date();
return {
seconds: time.getSeconds(),
minutes: time.getMinutes(),
hours: time.getHours()
}
});
}
currentTime
operator gets the current time and calls the methods of the Date object to return the current second, minutes and hours.
// clock.operator.ts
function rotateAngle (seconds: number, minutes: number, hours: number): HandTransformations {
const secondsDegrees = ((seconds / 60) * 360) + 90;
const minsDegrees = ((minutes / 60) * 360) + ((seconds / 60) * 6) + 90;
const hourDegrees = ((hours / 12) * 360) + ((minutes / 60) * 30) + 90;
return {
secondHandTransform: `rotate(${secondsDegrees}deg)`,
minuteHandTransform: `rotate(${minsDegrees}deg)`,
hourHandTransform: `rotate(${hourDegrees}deg)`,
}
}
export function rotateClockHands() {
return function (source: Observable<{ seconds: number, minutes: number, hours: number }>) {
return source.pipe(map(({ seconds, minutes, hours }) => rotateAngle(seconds, minutes, hours)));
}
}
currentTime
emits the results to rotateClockHands
and the rotateClockHands
operator invokes a helper function, rotateAngle
, to derive the CSS styles of the hands.
Finally, I use both operators to compose clockHandsTransform$ observable.
Use RxJS and Angular to implement observable in clock component
// clock.component.ts
clockHandsTransform$ = timer(0, this.oneSecond)
.pipe(
currentTime(),
rotateClockHands(),
);
- timer(0, this.oneSecond) - emits an integer every second
- currentTime() - return the current second, minute and hour
- rotateClockHands() - calculate the rotation angle of second, minute and hour hands
This is it, we have created a functional analog clock that displays the current time.
Final Thoughts
In this post, I show how to use RxJS and Angular standalone components to create an analog clock. The application has the following characteristics after using Angular 15's new features:
- The application does not have NgModules and constructor boilerplate codes.
- In ClockComponent, I import NgIf and AsyncPipe rather than CommonModule, only the minimum parts that the component requires.
This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.
Resources:
- Github Repo: https://github.com/railsstudent/ng-rxjs-30/tree/main/projects/day2-ng-and-css-clock
- Live demo: https://railsstudent.github.io/ng-rxjs-30/day2-ng-and-css-clock/
- Wes Bos's JavaScript 30 Challenge: https://github.com/wesbos/JavaScript30
Posted on February 18, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.