Simple Angular Counter/Countdown Component with dayjs & rxjs
/**
*
* Example Usage:
*
* # Counter
* const now = dayjs()
* <app-counter [autostart]="true" [mode]="forwards" [startTime]="now"></app-counter>
*
* # Countdown
* const later = dayjs().add(1, 'minute')
* <app-counter [autostart]="true" [mode]="backwards" [stopTime]="later"></app-counter>
*
*/
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import * as dayjs from 'dayjs';
import { interval, Subject, Subscription } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'app-counter',
template: `{{ timeFormatted }}`,
})
export class CounterComponent implements OnInit, OnDestroy {
// Forwards-Mode (Counter)
@Input() forwards: boolean
@Input() startTime: Date
public elapsedSeconds: number
public counterHasStarted: boolean
// Backwards-Mode (Countdown)
@Input() backwards: boolean
@Input() stopTime: Date
public countdownHasFinished: boolean
public remainingSeconds: number
public timeFormatted: string
@Input() autostart: boolean
@Input() updateInterval: number = 1000 / 2
public timeInterval$: Subscription
private unsubscribe$ = new Subject()
constructor() { }
ngOnInit() {
if (this.autostart) {
this.start()
}
}
ngOnDestroy() {
this.stop()
}
/**
* Initializes the Counter / Countdown
*/
public start() {
if (this.forwards == this.backwards) {
console.error("Couldn't start counter as no mode or both modes are set.")
return
}
if (this.forwards && (!this.startTime || !dayjs(this.startTime).isValid())) {
console.error("Couldn't start counter as mode is 'forwards' but no start-time is provided.")
return
}
if (this.backwards && (!this.stopTime || !dayjs(this.stopTime).isValid())) {
console.error("Couldn't start counter as mode is 'forwards' but no start-time is provided.")
return
}
// Start Interval
this.timeInterval$ = interval(this.updateInterval).startWith(0).pipe(
takeUntil(this.unsubscribe$)
).subscribe(_ => {
this.updateTime()
})
}
/**
* Stops the Counter / Countdown
*/
public stop() {
this.unsubscribe$.next(true)
this.unsubscribe$.complete()
}
/**
* Updates `timeFormatted` of the Counter / Countdown
*/
private updateTime() {
const now = dayjs()
if (this.forwards) {
// Start-Time from which the counter gets increased
const startTime = dayjs(this.startTime)
this.counterHasStarted = now.isAfter(startTime)
if (!this.counterHasStarted) {
this.timeFormatted = '0:00'
this.elapsedSeconds = 0
return
}
let elapsedTime = dayjs(now.valueOf() - startTime.valueOf())
elapsedTime = elapsedTime.subtract(dayjs().utcOffset(), 'minute')
this.elapsedSeconds = now.diff(startTime, 'second')
const format = elapsedTime.hour() ? 'H:mm:ss' : 'm:ss'
this.timeFormatted = elapsedTime.format(format)
} else if (this.backwards) {
// Stop-Time until which the countdown gets decreased
const stopTime = dayjs(this.stopTime)
this.countdownHasFinished = now.isAfter(stopTime)
if (this.countdownHasFinished) {
this.timeFormatted = '0:00'
this.remainingSeconds = 0
return
}
let remainingTime = dayjs(stopTime.valueOf() - now.valueOf())
remainingTime = remainingTime.subtract(dayjs().utcOffset(), 'minute')
this.remainingSeconds = stopTime.diff(now, 'second')
const format = remainingTime.hour() ? 'H:mm:ss' : 'm:ss'
this.timeFormatted = remainingTime.format(format)
}
}
}