Eric Clemmons avatar
Eric Clemmons
Published on

Advent of JavaScript, Day 1

Authors
Table of Contents

Advent of JS Homagepage

For the first time ever, I'm going to attempt Advent of JS.

Note – this post is a stream-of-conciousness and not a "How-to" guide.


Challenge #1 is creating a Pomodoro timer:

Screenshot Pomodoro timer

What I appreciate is that it begins with the most important criteria: user stories.

Users should be able to:

  • Start the timer by clicking on the start link/button.
  • Once the user clicks start, the word start will change to stop. Then, the user can click on the stop button to make the timer stop.
  • Click on the gear icon to change the length (minutes and seconds) of the timer.
  • Once the timer finishes, the ring should change from green to red and an alert message is passed to the browser.

As a tip, when interviewing and doing a System Design challenge, User Requirements (also called General Requirements) are usually how you start. Then, you define Function (Technical) Requirements, Architecture, etc.

How to Begin?

Progressive Enhancement

After downloading the project files, the first thing I noticed is it's all static HTML/CSS.

This immediately reminded me of progressive enhancement, where server-rendered pages (often with PHP) would have JavaScript loaded with <script> tags that would sprinkle in interactivity using jQuery.

You can use as many (or as few) tools, libraries, and frameworks as you'd like. If you're trying to learn something new, this might be a great way to push yourself.

So, I'd like to find a way of solving for interactivity without rewriting HTML as React to toggle the disabled attribute:

<div class="time">
  <div class="minutes">
    <input type="text" value="15" disabled />
  </div>
  <div class="colon">:</div>
  <div class="seconds">
    <input type="text" value="00" disabled />
  </div>
</div>

I'm thinking of using Vue.js, which recommends not using vue-cli for beginners.

Instead, I'll install Vue using a CDN

<script src="https://unpkg.com/vue@next"></script>

State Management

I've really enjoyed using XState for the Amplify UI Authenticator.

I can manage much of this state by hand with Vue, but I'd really like to have a vizualization and assurance that I don't end up with impossible states.

Luckily, XState can be installed using a CDN, too:

<script src="https://unpkg.com/@xstate/vue/dist/xstate-vue.min.js"></script>

Environment

Ok, so I've picked a framework (Vue) and a state library (XState) to make this work.

But where am I going to work?

Advent of JS recommends using LiveReload, but I'd prefer something public & embeddable by default, like CodeSandbox or StackBlitz.

Since I haven't used StackBlitz, I'll give that a shot. They even support uploading from your computer!

Setting Up The Environment

I chose a Vanilla > Static (HTML/JS/CSS) Project, but quickly learned I'd have to pay $8 per month just to upload the files.

I shelled out the money but immediately cancelled the subscription because StackBlitz cannot serve static CSS!

Back to CodeSandbox and importing my project from the CLI:

npm install -g codesandbox
cd STARTER_FILES
codesandbox ./

2 minutes later, my CodeSandbox is live!

Wiring up Vue

Ok, I'm impressed! Vue only required a few lines of code to wire up my markup:

<!-- https://v3.vuejs.org/guide/installation.html#cdn -->
<script src="https://unpkg.com/vue@next"></script>

<!-- Custom app logic -->
<script src="app.js" type="module"></script>
const { Vue } = window

const Pomodoro = {
  data() {
    return {
      counter: 0,
    }
  },
}

Vue.createApp(Pomodoro).mount(document.querySelector('.wrapper'))

Wiring up XState

Now that I know I can make my static markup interactive with Vue, I'm going to model the User Requirements as a finite state machine using the XState Visualizer:

Users should be able to:

  • Start the timer by clicking on the start link/button.

This tells me we'll begin in an paused state and have a START transition to a running state.

  • Once the user clicks start, the word start will change to stop. Then, the user can click on the stop button to make the timer stop.

Vue will observe the machine and do something like state.matches('running') to determine whether or not to render the stop button. In Advent of JS' own solution, the button says pause, not stop.

When this button is clicked, it'll send a STOP event to the machine, which should transition back to paused.

  • Click on the gear icon to change the length (minutes and seconds) of the timer.

When the machine is paused, an EDIT event will transition the machine to editing.

Vue will check for state.matches('editing') and toggle the disabled attribute accordingly. These inputs will fire @change events, which will send CHANGE signals to the machine that will assign({ minutes, seconds }) to the machine's context.

  • Once the timer finishes, the ring should change from green to red and an alert message is passed to the browser.

I haven't ran a timer in XState before, but luckily there's an example: https://xstate.js.org/docs/tutorials/7guis/timer.html#coding

Normally, I would use setInterval when running and clearInterval when transitioning back to paused.

In the machine, I'll use a TICK event to assign({ minutes, seconds })

XState has a conditional guard that can be used to check if both minutes and seconds are 0. When this happens, we'll transition to a done state.

Vue will match this with state.matches('done') to toggle the ring color.

For TICKing down, rather than subtracting 1 each second, I'll compare endTime - startTime > 0. (I learned this trick when doing animations in MooTools because timers are unreliable, especially when there can be blocking threads.)

Now that I've written this out, it only took ~10 minutes to prototype the pomodoro machine:

Wiring up the UI

This is where we find out if initial assumptions actually work :D

One constraint I imposed on myself was not to expose XState's state or send directly to the view layer:

<input
  @input="send('CHANGE', { minutes: $event.target.value })"
  type="text"
  maxlength="2"
  :value="state.context.minutes"
  :disabled="!state.matches('paused')"
/>

Instead, there are computed properties and methods that are extension points:

<input
  @input="handleMinutes"
  type="text"
  maxlength="2"
  :value="minutes"
  :disabled="!isEditing"
/>

In this case, seconds dynamically changes from 05 to 5 when state.matchines('editing').

Finishing Up

From here, much of the refinement of the state machine was trial-and-error from using the UI. Subtle details not present in the User Requirements were visible in their demo video:

  • pause instead of stop.
  • After the alert is dismissed, the timer resets.
  • The timer pads number values with leading 0s, but not when editing.

A few other things I learned along the way:

  • It was easier to use XState alone rather than XStateVue.

    I would use @xstate/vue in a bundled environment, but via CDN it didn't seem to make things any easier.

  • You cannot .start(initialState) a service that's transitioned to a final state!

    I thought I could restart the service & reset it, but I couldn't. Instead, I had to recreate it all over again with a startMachine() utility.

  • This is my first time to actually use <script type="module"> and import in the browser without a bundler!

All in all, I probably spent a couple of hours reading Vue & XState docs and working out the kinks. The first hour covered ~80% of the functionality, but the last 20% was used to account for '' values & resetting the machine.