A pattern of connected spheres

A unix philosophy for web development

Joeri Sebrechts

Web components have their malcontents. While frameworks have done their best to provide a place for web components to fit into their architecture, the suit never fits quite right, and framework authors have not been shy about expressing their disappointment. Here's Ryan Carniato of SolidJS explaining what's wrong with web components:

The collection of standards (Custom Elements, HTML Templates, Shadow DOM, and formerly HTML Imports) put together to form Web Components on the surface seem like they could be used to replace your favourite library or framework. But they are not an advanced templating solution. They don't improve your ability to render or update the DOM. They don't manage higher-level concerns for you like state management.
Ryan Carniato

While this criticism is true, perhaps it's besides the point. Maybe web components were never meant to solve those problems anyway. Maybe there are ways to solve those problems in a way that dovetails with web components as they exist. In the main components tutorial I've already explained what they can do, now let's see what can be done about the things that they can't do.

The Unix Philosophy

The Unix operating system carries with it a culture and philosophy of system design, which carries over to the command lines of today's Unix-like systems like Linux and MacOS. This philosophy can be summarized as follows:

What if we look at the various technologies that comprise web components as just programs, part of a Unix-like system of web development that we collectively call the browser platform? In that system we can do better than text and use the DOM as the universal interface between programs, and we can extend the system with a set of single purpose independent "programs" (functions) that fully embrace the DOM by augmenting it instead of replacing it.

In a sense this is the most old-school way of building web projects, the one people who "don't know any better" automatically gravitate to. What us old-school web developers did before Vue and Solid and Svelte, before Angular and React, before Knockout and Ember and Backbone, before even jQuery, was have a bunch of functions in utilities.js that we copied along from project to project. But, you know, sometimes old things can become new again.

In previous posts I've already covered a html() function for vanilla entity encoding, and a signal() function that provides a tiny signals implementation that can serve as a lightweight system for state management. That still leaves a missing link between the state managed by the signals and the DOM that is rendered from safely entity-encoded HTML. What we need is a bind() function that can bind data to DOM elements and bind DOM events back to data.

Finding inspiration

In order to bind a template to data, we need a way of describing that behavior in the HTML markup. Well-trodden paths are often the best starting place to look for inspiration. I like Vue's template syntax, because it is valid HTML but just augmented, and because it is proven. Vue's templates only pretend to be HTML because they're actually compiled to JavaScript behind the scenes, but let's start there as an API. This is what it looks like:

<img :src="imageSrc" />
Bind src to track the value of the imageSrc property of the current component. Vue is smart enough to set a property if one exists, and falls back to setting an attribute otherwise. (If that confuses you, read about attributes and properties first.)
<button @click="doThis"></button>
Bind the click event to the doThis method of the current component.

By chance I came across this article about making a web component base class. In the section Declarative interactivity the author shows a way to do the Vue-like event binding syntax on a vanilla web component. This is what inspired me to develop the concept into a generic binding function and write this article.

Just an iterator

The heart of the binding function is an HTML fragment iterator. After all, before we can bind attributes we need to first find the ones that have binding directives.

This code will take an HTML template element, clone it to a document fragment, and then iterate over all the nodes in the fragment, discovering their attributes. Then for each attribute a check is made to see if it's a binding directive (@ or :). The node is then bound to data according to the directive attribute (shown here as TODO's), and the attribute is removed from the node. At the end the bound fragment is returned for inserting into the DOM.

The benefit of using a fragment is that it is disconnected from the main DOM, while still offering all of the DOM API's. That means we can easily create a node iterator to walk over it and discover all the attributes with binding directives, modify those nodes and attributes in-place, and still be sure we're not causing DOM updates in the main page until the fragment is inserted there. This makes the bind function very fast.

If you're thinking "woah dude, that's a lot of code and a lot of technobabble, I ain't reading all that," then please, I implore you to read through the code line by line, and you'll see it will all make sense.

Of course, we also need to have something to bind to, so we need to add a second parameter. At the same time, it would be nice to just be able to pass in a string and have it auto-converted into a template. The beginning of our bind function then ends up looking like this:

That just leaves us the TODO's. We can make those as simple or complicated as we want. I'll pick a middle ground.

Binding to events

This 20 line handler binds events to methods, signals or properties:

That probably doesn't explain much, so let me give an example of what this enables:

If you're not familiar with the signal() function, check out the tiny signals implementation in the previous post. For now you can also just roll with it.

Not a bad result for 20 lines of code.

Binding to data

Having established the pattern for events that automatically update properties, we now reverse the polarity to make data values automatically set element properties or attributes.

The getPropertyForAttribute function is necessary because the attributes that contain the directives will have names that are case-insensitive, and these must be mapped to property names that are case-sensitive. Also, the :text and :html shorthand notations replace the role of v-text and v-html in Vue's template syntax.

When the value of the target's observed property changes, we need to update the bound element's property or attribute. This means a triggering 'change' event is needed that is then subscribed to. A framework's templating system will compare state across time, and detect the changed values automatically. Lacking such a system we need a light-weight alternative.

When the property being bound to is a signal, this code registers an effect on the signal. When the property is just a value, it registers an event listener on the target object, making it the responsibility of that target object to dispatch the 'change' event when values change. This approach isn't going to get many points for style, but it does work.

Check out the completed bind.js code.

Bringing the band together

In the article Why I don't use web components Svelte's Rich Harris lays out the case against web components. He demonstrates how this simple 9 line Svelte component <Adder a={1} b={2}/> becomes an incredible verbose 59 line monstrosity when ported to a vanilla web component.

Now that we have assembled our three helper functions html(), signal() and bind() on top of the web components baseline, at a total budget of around 150 lines of code, how close can we get for a web component <x-adder a="1" b="2"></x-adder>?

To be fair, that's still twice the lines of code, but it describes clearly what it does, and really that is all you need. And I'm just shooting in the wind here, trying stuff out. Somewhere out there could be a minimal set of functions that transforms web components into something resembling a framework, and the idea excites me! Who knows, maybe in a few years the web community will return to writing projects in vanilla web code, dragging along the modern equivalent of utilities.js from project to project...


What do you think?