A year or two back, I started noodling on a homegrown personal wiki. It’s ugly and missing a lot of features, but at this point I actually do have some documents in it that I kinda sorta care about. This December I had some time on my hands, so six days ago, I blew off the dust and started working on the code again.
Because of Reasons, I had originally put the project under a hard restriction: no more than one third-party library in the runtime. That one library was ProseMirror, leaving no room for a framework. After firing up my editor and opening some files, I was immediately reminded why I had abandoned the code for over a year.
Let Us Praise The Framework
When I had last left the project, I had just finished building a vanilla JS combobox widget for quick search navigation. I was mostly pleased with the UX: the ARIA keyboard behavior, the highlighting / <mark>ing, etc. The code though… that I had written, umm, expediently.
The next feature on my list was a “mentions” widget for inserting internal links. This would be a harder problem, because this code needed to wire up to ProseMirror. I was hoping to reuse code from the original combobox, but unfortunately, it was a single, very messy class. The new mentions code would need parts of the Combobox (the Listbox, the key bindings, the highlighting behavior) but not other parts (the same Listbox children, the <input> field). After trying and failing to decompose the original code into reusable components, I took a step back.
At a high level, I knew what was wrong: the code was a hot mess. But knowing your code is a “mess” is about as helpful as your Little League coach telling you to “just hit the ball squarely.”
I had created a bad bespoke API, but specifically, what was wrong with it? What was missing? Here’s what I came up with:
- A concept of components as entities that naturally compose with each other.
- A formal notion of data that belongs to the component, and data that does not.
- A reactivity mechanism for updating the DOM, in response to data changing.
- A declarative approach for generating DOM and binding event listeners.
A framework, of course, does plenty more than that. But I believe those concepts above are what it takes to create the glide path of building an app as a family of components, and not as a rat’s nest of DOM mutations.
I Do Declare
Since I was using ProseMirror, I had seen code examples using Marijn Haverbeke‘s DOM utility library, crelt (for “create element”). This delightfully tiny helper library provides an el() function to create DOM declaratively. Or at least, in a declarative-ish manner. It looks like this:
const pageNav = el(
'nav',
{ class: 'page-nav' },
pageInfo.map(({ id, title }) =>
el('li',
el('a', { href: `/pages/${id}` }, title)
)
)
);
This is definitely better than giving yourself a repetitive stress injury from stamping out document.createElement() and element.appendChild() all day. Technically, crelt would be an additional, and therefore totally illegal for me, third party library. But the thing is only 28 lines long. Is that so bad? Does it really count as a “third party library”? What is a “library”? What are words even?
I gave el() an earnest try. It was definitely helpful. But in the end, I determined that what I was really looking for was something that looks a lot more like HTML itself. It was time to atone. Time to npm rm crelt, rededicate myself to my vows, and stride back into the wasteland, carrying nothing but my clay bowl and walking stick.
Literally, Just Use Tagged Templates
With crelt gone, and JSX clearly off the table. I was out of options. it was time to Use The Platform.
Template literals! What could be simpler? You make a string, pass in whatever expressions you need, and everything turns into DOM. Like this:
link.innerHTML = `<a href="${url}">${label}</a>`
Sweet!
counter.innerHTML = `<p>The count is: ${count || 0}</p>`
That works too! Yay numbers!
counter.innerHTML = `<p>The count is: ${count || undefined}</p>`
Well that’s just weird/buggy code in the first place.
nav.innerHTML = `<nav class="page-nav">
${pageInfo.map(({ id, title ) => ...)}
</nav>`
Ok, we’ll come back to that one…
button.innerHTML = `<button onclick={fn}>Close</button>`
Hmmm.
So it turns out what we need is tagged template literals. A tag function is a special function that receives a parsed version of the template literal, as an array of static strings, and a list of expressions. That format is honestly a bit gross but hey whatever! A strategy is emerging:
- Pass a template literal into our tag function. Store all the expressions in an array for later use.
- Reassemble the string, with some kind of placeholder everywhere an expression used to be. Each placeholder includes an index indicating which expression it represents. Something like:
<a href="__PLACEHOLDER_0__">__PLACEHOLDER_1__</a> - Let the browser do the thing it’s really good at: parsing this string for us. We’ll use
document.createTreeWalker(). - Process the resulting element nodes and text nodes. For each
__PLACEHOLDER_N__, extract the N, retrieve the corresponding expression, and handle each one we care about.
What does that code look like? We’ll cover that in Part II.