yassinedoghri/react-timer-machine

View on GitHub
src/index.js

Summary

Maintainability
A
45 mins
Test Coverage
B
82%
/**
 * @class TimerMachine
 */

import PropTypes from "prop-types";
import React, { Component } from "react";

class TimerMachine extends Component {
  static msToTime(ms) {
    const milliseconds = ms % 1000;
    const ms1 = (ms - milliseconds) / 1000;
    const seconds = ms1 % 60;
    const ms2 = (ms1 - seconds) / 60;
    const minutes = ms2 % 60;
    const hours = (ms2 - minutes) / 60;

    return {
      h: hours,
      m: minutes,
      s: seconds,
      ms: milliseconds
    };
  }

  static formatTime(time) {
    const pad = (n, z = 2) => `00${n}`.slice(-z);
    return `${pad(time.h)}:${pad(time.m)}:${pad(time.s)}.${pad(time.ms, 3)}`;
  }

  constructor(props) {
    super(props);

    this.state = {
      time: TimerMachine.msToTime(props.timeStart),
      milliseconds: props.timeStart
    };
    this.timer = 0;
    this.every = props.interval;
    this.internalState = 0; //  0 = idle, 1 = running, 2 = paused, 3 = resumed
    this.remaining = 0;
    this.startTime = 0;

    this.startTimer = this.startTimer.bind(this);
    this.pauseTimer = this.pauseTimer.bind(this);
    this.resumeTimer = this.resumeTimer.bind(this);
    this.stopTimer = this.stopTimer.bind(this);
    this.resetTimer = this.resetTimer.bind(this);
    this.timeoutCallback = this.timeoutCallback.bind(this);
    this.tick = this.tick.bind(this);
  }

  componentDidMount() {
    this.forceUpdate();
  }

  componentDidUpdate() {
    const { countdown, started, paused, interval } = this.props;

    this.every = countdown ? -interval : interval;

    if (started) {
      // start timer if not started already
      if (this.internalState === 0) {
        this.resetTimer();
        this.startTimer();
      }
      if (paused) {
        this.pauseTimer();
      } else {
        this.resumeTimer();
      }
    } else {
      this.stopTimer();
    }
  }

  componentWillUnmount() {
    this.stopTimer();
  }

  startTimer() {
    const { onStart } = this.props;

    onStart(this.state.time);
    this.startTime = new Date();
    this.timer = setInterval(this.tick, this.props.interval);
    this.internalState = 1;
  }

  stopTimer() {
    const { onStop } = this.props;

    onStop(this.state.time);
    if (this.timer) {
      clearInterval(this.timer);
    }
    this.timer = 0;
    this.internalState = 0;
  }

  pauseTimer() {
    if (this.internalState !== 1) return;

    const { interval, onPause } = this.props;

    onPause(this.state.time);
    this.remaining = interval - (new Date() - this.startTime);
    clearInterval(this.timer);
    this.internalState = 2;
  }

  resumeTimer() {
    if (this.internalState !== 2) return;
    const { onResume } = this.props;

    onResume(this.state.time);
    window.setTimeout(this.timeoutCallback, this.remaining);
    this.internalState = 3;
  }

  resetTimer() {
    const { timeStart } = this.props;
    this.setState({
      time: TimerMachine.msToTime(timeStart),
      milliseconds: timeStart
    });
  }

  timeoutCallback() {
    if (this.internalState !== 3) return;

    this.tick();

    this.startTime = new Date();
    this.timer = setInterval(this.tick, this.props.interval);
    this.internalState = 1;
  }

  tick() {
    // Remove interval, set state so a re-render happens.
    const { onComplete, onTick, timeEnd, countdown } = this.props;
    const { milliseconds } = this.state;

    const msRemaining = milliseconds + this.every;
    const timeRemaining = TimerMachine.msToTime(msRemaining);
    this.setState({
      time: timeRemaining,
      milliseconds: msRemaining
    });
    onTick(timeRemaining);

    // Check if timer completed.
    if (
      (countdown && msRemaining <= timeEnd) ||
      (!countdown && (timeEnd && msRemaining >= timeEnd))
    ) {
      this.stopTimer();
      onComplete(timeRemaining);
    }
  }

  render() {
    const { time, milliseconds } = this.state;
    const timer = this.props.formatTimer(time, milliseconds);

    return <React.Fragment>{timer}</React.Fragment>;
  }
}

TimerMachine.propTypes = {
  timeStart: PropTypes.number.isRequired,
  timeEnd: PropTypes.number,
  countdown: PropTypes.bool,
  interval: PropTypes.number,
  started: PropTypes.bool,
  paused: PropTypes.bool,
  formatTimer: PropTypes.func,
  onTick: PropTypes.func,
  onStart: PropTypes.func,
  onPause: PropTypes.func,
  onResume: PropTypes.func,
  onStop: PropTypes.func,
  onComplete: PropTypes.func
};

TimerMachine.defaultProps = {
  timeEnd: 0,
  countdown: false,
  interval: 1000,
  started: false,
  paused: false,
  formatTimer: time => TimerMachine.formatTime(time),
  onTick: () => {},
  onStart: () => {},
  onPause: () => {},
  onResume: () => {},
  onStop: () => {},
  onComplete: () => {}
};

export default TimerMachine;