Advent of JavaScript, Day 1
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:
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 input
s 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 cond
itional 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 TICK
ing 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 ofstop
.- After the
alert
is dismissed, the timer resets. - The timer pads number values with leading
0
s, but not when editing.
A few other things I learned along the way:
-
It was easier to use
XState
alone rather thanXStateVue
.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)
aservice
that’s transitioned to afinal
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">
andimport
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.