wottpal
3/9/2019 - 12:10 PM

Simple Angular Counter/Countdown Component with dayjs & rxjs

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

}