Advent of JavaScript, Day 5
Challenge #5 is selecting episode checkboxes from a podcast. The catch is that Shift-Clicking a checkbox will select all the checkboxes between the first and the last selected checkbox:
With the project files downloaded and codesandbox
’d into a live CodeSandbox, I’m ready to get going!
User Requirements
Again, let’s start with the User Requirements and speculate how I can solve these:
- See the list of podcast episodes
There’s a provided app.js
with episodes
defined. The static HTML only has 10 <li>
s, so I’ll already need to convert these to templates.
I’ve been using Vue a bunch, so let’s mix things up a bit by using htm
.
- Check one episode, shift-click to select all the episodes in between
On the surface, this seems straightforward:
onClick
, check ifShift
is pressed.- If so, select all the episodes in between.
The UX question I have is what if a checkbox is checked, then focus goes elsewhere? In this case, does Shift+Click always select the boxes in-between?
The video explicitly states:
Track that first one, track the ending one, then checkbox all the ones in-between.
Wiring It Up
htm
First, I need to make sure app.js
is treated as an ES Module, so I can use import
within it:
<script src="app.js" type="module"></script>
Then, I can move episodes
to its own file and import
htm
already bound to preact
:
import {
html,
Component,
render,
} from 'https://unpkg.com/htm/preact/standalone.module.js'
import { episodes } from './episodes.js'
Rendering looks very similar to React:
render(
html`<${App} episodes=${episodes} />`,
document.querySelector('.wrapper')
)
Now to make this static content dynamic!
Components
Normally the 1st thing I do when I’m making a template Reactive (heh)
is put the entire static markup into App
’s render
method.
Then, the moment I want to loop over data (e.g. episodes.map
),
there’s an opportunity for a new component.
From there, any other components (e.g. Header
, Footer
, Nav
) are abstractions
to simplify the mental overhead of reading App
.
Interpolation
What’s pretty amazing about htm
is how natural it is coming from React.
Static Markup
<ul class="episodes">
<li>
<label for="episode-1">
<input type="checkbox" name="episode-1" id="episode-1" />
<span>1 || Trailer</span>
</label>
</li>
...
</ul>
Dynamic Markup
Wow, this just worked! 🔥
<ul class="episodes">
${episodes.map(({ id, name }) => html`
<li>
<label for="episode-${id}">
<input type="checkbox" name="episode-${id}" id="episode-${id}" />
<span>${id} || ${name}</span>
</label>
</li>
`)}
</ul>
Tracking checked
Since htm
is managing the HTML, that naturally means that checked
will also be managed via local state.
When onClick
is fired, I’ll check the following:
event.shiftKey === true
- Pull
id
from thedata-id
attribute (so I don’t have to do string splitting onid
orname
) - Set
lastChecked
and the updatedchecked
object with this value toggled
Bugs
For whatever reason, checked = { [id]: true, ... }
doesn’t seem to be updating immediately, but one render later:
I’m not sure what this bug is, but I generally make a rule of spend 30 minutes on something and, if you stop making progress, find another way.
Back to Vue
I’m going to try Vue again, since it’s worked so well in the previous examples.
Most likely, there’s a bug I introduced. But, it can sometimes be helpful to solve a problem another way, identify similarities, and make progress.
I forked the CodeSandbox and ready to start over 🙃
Luckily, it’s pretty straightforward to get the same result with Vue:
<ul class="episodes">
<li v-for="episode in episodes">
<label :for="'episode-' + episode.id">
<input
@click="toggleEpisode(episode, $event)"
:id="'episode-' + episode.id"
:checked="episode.checked"
:name="'episode-' + episode.id"
type="checkbox"
/>
<span>{{ episode.id }} || {{ episode.name }}</span>
</label>
</li>
</ul>
methods: {
toggleEpisode(episode, event) {
episode.checked = !episode.checked;
if (episode.checked && event.shiftKey) {
const { episodes, lastChecked = episode } = this;
const [start, end] = [
episodes.indexOf(lastChecked),
episodes.indexOf(episode)
].sort();
episodes.forEach((episode, i) => {
if (i >= start && i <= end) {
episode.checked = true;
}
});
}
this.lastChecked = episode;
}
}
Finishing Up
I learned that @click.prevent
actually breaks the checked
behavior!
This means it wasn’t a bug in htm
as expected, but a browser quirk!
I went back to my htm
sandbox, removed event.preventDefault()
, and was able to get it working.
When comparing the two, Vue was still easiest to get working from a static template.
I can see this being my default framework of choice for these challenges 💪