7 Guis in ClojureScript: Timer, CRUD, Circle Drawer
This post is part 3 of a series:
- 7 GUIs in ClojureScript: Setup and Counter.
- 7 GUIs in ClojureScript: Temperature Converter, Flight Booker
- 7 GUIs in ClojureScript: Timer, CRUD, Circle Drawer
Timer
Reading the description of the Timer task, I started worrying about the concurrency aspect. Will I need to pull some dependencies to deal with it, or will it be a small mess of timeout IDs floating around in the model? The progress bar and range slider had me worried too.
Timeouts or interval
The first thing I wanted to solve for this task was a timer that I could start and stop.
I had two options: a tick
function that runs the desired action and optionally
schedules itself to run in the future, or an interval that runs the desired
action. The interval would be created and destroyed to start and stop the timer.
I wanted to keep things simple, so I tried the tick
function approach first. I
had a straightforward data model with the elapsed time and the timer duration:
(def state (r/atom {:elapsed 0 :duration 17}))
To start the timer, we call tick
. Every time tick
runs, it increments
:elapsed
. The ticking function has some simple logic that checks if :elapsed
is less than :duration
and schedules itself to run a second later. As soon as
the elapsed time is equal to or greater than the duration, the function will not
schedule another run and the timer stops.
To debug the timer, I created a simple component to display the value of
:elapsed
and added it to the app.
I tried the tick
functionality in the REPL a few times, and changed the value
of :duration
while the timer was running, and everything was working.
What about restarting the timer?
The next thing I wanted to have was a “reset” event that I could call when the
code was reloaded in the browser. This functionality required a bit more than
just setting :elapsed
to 0. I couldn’t just call tick
on reload because
another call might be already scheduled. My first idea was to add a :running?
flag to the model, and toggle that in a start
function. It would be toggled
again in tick
when the elapsed time was equal to or greater than the desired
duration. It turned out to be an easy and simple solution: to reset the timer, I
just needed to set :elapsed
to 0 and call start
.
I guessed that I had about half of the functionality ready at this point, and I hadn’t run into any problems. I was feeling good!
Competing signals
When I started, I was worried about coordinating the ticking timer and the user
changing the timer’s duration. The way I had implemented the functionality
made this trivial: change the value of :duration
in the model and call
start
.
At this point, I was so glad that everything was going so well that I spent a bit of time changing the tick interval from 1 second to 100 milliseconds.
Fancier user controls and a pleasant surprise
I knew about the new(ish) built-in range slider and progress bar controls, but I had never used them. In my previous work experience, I always had an SDK with custom controls, following specific guidelines and visual style particular to the app I was building.
I didn’t want to bring any external dependencies yet, so I was hoping that the built-in controls would be enough, and they were. I implemented and wired up the progress display and the range slider and quickly completed the task.
Faster than expected
I was able to code a straightforward solution relatively quickly. I was expecting at least some trouble with timers, but I had none.
The expectations for this task were to test a timer process that updates the elapsed time and runs concurrently with the user’s interactions while being performant and making clear that the signal is a timer tick. I think my solution fulfils all of these requirements. The code is here.
CRUD
The next task, CRUD, seemed straightforward. Many applications are a more complicated version of this exercise: you have a list of something, and you need to add new things, edit the existing information and finally delete some of it.
A new challenge
I got to work confidently and quickly realised that this exercise had a novel component: the list of people can be filtered.
The first thing that came to mind was maintaining a filtered list of people, updated every time the full list or the filter changed. I gave it a little more thought and decided against it because I didn’t want to keep two separate lists in sync.
The next idea was to have a function that generated the filtered
list
and use that as the source for the UI component instead. I wasn’t sure it would
work, and I was a bit surprised when it did. It then dawned on me that something
like @db/state
is just a reader macro for (deref db/state)
, so it’s just a
function call! The only downside is that the filtering function is called every
time anything in the state changes, which is inefficient. I could solve this by
having another atom just for the list of people and the filter prefix, but I
decided not to spend time on that.
The joy of Flexbox
The rest of the exercise felt very easy. The spec mentioned the challenge of “building a non-trivial layout”, but thanks to Flexbox, that’s (usually) not a problem. I haven’t tried the latest layout engine added to the browser: CSS Grid, but it didn’t seem necessary this time.
An issue with <select>
I spent some time trying to figure out why sometimes when I selected a person from the list, the change event wasn’t fired. To reproduce this behaviour, you need to:
- Select a person on the list
- Focus the “Filter Prefix” field
- The selected person stays selected, but the list is no longer focused.
- Filter the list, making sure the selected person stays visible (the filtering will clear the “Forename” and “Surname” fields).
- Click on the selected person again: the change event won’t fire, and the “Forename” and “Surname” fields remain empty.
I tried a few things on my code, but this is a known issue with React’s Select.
In the end, the exercise was less complicated than I expected. I remember using very rigid patterns of MVC back in the Adobe Flex + Cairngorm days, but none of that felt necessary this time. The pattern I’ve been using of defining events that mutate the state directly and dereferencing the state directly from the views was enough.
Check out the app and the code. I think my implementation is easy to follow and fulfils the requirements spelt out in the 7 GUIs spec.
Circle Drawer
Coming soon…