New Article Navigator

A few years back I created an interactive article navigator as an exercise in learning React. I thought I’d see what it was like doing it over now I’ve been writing UIs in React for a few years.

The navigator is the table of contents to the right (or below) this entry. Here is what it looked like:

(screensot)

When I came to update the blog recently I discovered it had stopped working. It is written in the React conventions of 2015 (React version 0.14), so debugging and fixing it would be a pain, and besides I do not like the UX of the old component: while it exercised React nicely, it is not really very useful for navigation, and neither intuitive nor accessible.

So I decided I might as well redesign and reimplement it it using the modern React conventions—and, I thought, I can do a clone in Svelte and see which produces the leaner JavaScript code.

Design

An important part of writing less code is designing your UI to go with the grain of HTML 5 (and its built-in ARIA support) rather than against. For my new nav I decided I wanted something more useful and less showy, and to directly use the HTML 5 elements details and summary to implement it.

The details disclosure element is available in all modern browsers and does exactly what I want: when closed it shows a summary, and the user can click on that to open it and show the details. A triangular icon (‘twistie’) to the left of the summary rotates to show whether the details are shown or not, a UI convention dating back to the 1980s.

Summary

Details details details

No JavaScript required, and accessible via default.

The resulting design looks like this:

(screensot)

There are a couple of complications.

  1. When the list is first displayed, I want the current entry to be visible and highlighted, and the previous and next entries (if any) to be visible. This requires making the year and month elements containing them open by default.

  2. I wanted the details of the closed elements to be loaded lazily, so that the entire site index is not needlessly included in every page view.

React 2022

A lot has changed since 2015. Apart from improvements to the React framework itself, the build system around it is infinitely more convenient.

  • The Create React App tool will set up a working React project in a single command, saving you days of faffing with Webpack or Rollup, Karma or Jasmine or Mocha or Jest, Typescript-in-Babel or Babel-in-Typescript, and what-have-you.
  • The React Storybook installer sets things up nicely for developing components bottom-up, and Mock Service Workers MSW facilitates the writing of tests and storybook stories for components that request data from the back-end.
  • We can use JavaScript with async/await and fetch to run requests.

All in all developing the component in 2022-style React feels a lot more productive than the lash-up I came up with in 2015.

My desire to have some nodes open by default made it seem at first I needed to track the state of the disclosure elements explicitly, which interfered with my use of toggle events to load content lazily. I couldn’t work around this by tracking click events instead, because that would have lost me the accessible-by-default behaviours. What worked in the end was making toggling the open nodes make that component uncontrolled (no value supplied for open) rather than controlled (open set to false).

Anyway I got something that looked nice in Storybook and was about to start working out how to integrate it in to the site. I ran yarn build to see how big the bundle was and it turned out to be [FX: record scratch] 288K bytes. Which seems ridiculous.

No React

OK, I thought, I better try the Svelte version and see if it is reasonably small. As it turned out, the timing is a but unfortunate and SvelteKit (the equivalent of Create React App) and Svelte Storybook are not at present talking to each other. While I am told the new beta of Storybook-Svelte works better, I nevertheless took this as a prompt to pause and reconsider my approach.

The thing is, without the excuse of trying out a new web framework, React or Svelte are overkill for this simple user interface. Last decade it would have been a trivial jQuery job. The addition of fetch and querySelectorAll functions to modern browsers means we do not even need jQuery.

So I went back to basics: what do I want the interaction on the page to be, and how can I realize it with the least code? Following the old doctrine of progressive enhancement, I first made it work without JavaScript: the Django template renders the list of details elements, only including the details of the years needed to display the current entry. The other years cannot load their content without JavaScript, but they can link to the index page for that year.

<details>
    <summary><a href="/pdc/2020/">2020</a></summary>
</details>

Now thinking about the JavaScript enhancements, I replaced the JSON back-end call with one returning the HTML for the details element. The client code can convert it to DOM nodes by assigning to the outerHTML attribute of the placeholder details element. This removes the need for client-side template-rendering.

How does the JavaScript code know which elements need the event handler added to? And how does it know the URL to request the data from? The answer to both of these is to add a data-nav attribute with the URL to the placeholder details element:

<details data-nav="/pdc/2020/nav">
    <summary><a href="/pdc/2020/">2020</a></summary>
</details>

This is destroyed when the outerHTML is assigned to, so we do not need our own code to track which nodes have been loaded so far.

The upshot of all this is that JavaScript code is very short. Here is the module in its entirety:

const handleToggle = async e => {
    const elt = e.currentTarget;
    const res = await fetch(elt.dataset.nav);
    elt.outerHTML = await res.text();
}

document.querySelectorAll('*[data-nav]').forEach(elt => {
    const s = elt.querySelector('summary');
    s.innerText = s.querySelector('a').innerText;
    elt.addEventListener('toggle', handleToggle);
})

The first two lines of the body of the forEach remove the fallback a tag, since it is not needed if the JavaScript is running.

This is a lot less code than 288K! To be fair, my version requires a modern web browser: a lot of the extra code in the React bundle is compatibility code for IE 11.

Conclusion

React and Svelte (and Vue, Angular, et al.) make sense when implementing complex user interfaces. For straightforward stuff using HTML 5 as your UI framework may prove simpler and more satisfactory to your readers as well.