complete-stopwatch

Demonstration of a state machine with data actions

by Tracy-Gregory Gilmore

HTML

<main>
  <div class="stateDisplay" id="divState"></div>
  <button id="btnStart">START</button>
  <button id="btnPause">PAUSE</button>
  <button id="btnStop">STOP</button>
  <button id="btnClear">CLEAR</button>
</main>

CSS

.stateDisplay {
  background-color: #DDD;
  text-align: center;
  text-transform: uppercase;

  width: 7em;
  font-size: 2rem;
  margin: 2px;

  padding: 0.25em;
  font-family: monospace;
}

JavaScript

function showState(counter) {
	const msec = `${counter % 100}`.padStart(2, '0');
	const secs = `${Math.floor((counter % 6000) / 100)}`.padStart(2, '0');
	const mins = `${Math.floor(counter / 6000)}`.padStart(2, '0');
	divState.textContent = `${mins}:${secs}.${msec}`;
}

function enableButtons(stateEngine) {
	btnStart.addEventListener('click', () => stateEngine.trigger('START'));
	btnStop.addEventListener('click', () => stateEngine.trigger('STOP'));
	btnPause.addEventListener('click', () => stateEngine.trigger('PAUSE'));
	btnClear.addEventListener('click', () => stateEngine.trigger('CLEAR'));
	return stateEngine;
}

function StateEngine(machine, initial, subject) {
	subject.state = initial;
	showState(subject.counter);

	return {
		trigger(event) {
			const currentState = machine[subject.state];

			if (event in currentState) {
				subject.state = currentState[event].state;
				if ('_action' in currentState[event]) {
					currentState[event]._action();
				}
				if ('_action' in machine[subject.state]) {
					machine[subject.state]._action();
				}
			}
			return subject.state;
		},
	};
}

const Subject = {
	state: '',
	counter: 0,
};

const stateEngine = enableButtons(
	StateEngine(
		{
			clear: {
				_action: () => {
					Subject.counter = 0;
					showState(Subject.counter);
				},
				START: {
					state: 'running',
					_action: () => Clock.start(),
				},
			},
			running: {
				STOP: {
					state: 'stopped',
					_action: () => Clock.stop(),
				},
				PAUSE: {
					state: 'paused',
					_action: () => Clock.pause(),
				},
				'CLOCK-TICK': {
					state: 'running',
					_action: () => {
						Subject.counter++;
						showState(Subject.counter);
					},
				},
			},
			paused: {
				PAUSE: {
					state: 'running',
					_action: () => Clock.resume(),
				},
			},
			stopped: {
				CLEAR: {
					state: 'clear',
				},
			},
		},
		'clear',
		Subject
	)
);

const Clock = (function (stateEngine) {
	let timer;

	return {
		start: startTimer,
		stop:...