Programming
TABLE DRIVEN WEB DEVELOPMENT
I've occasionally made reference, in the course of my blogging about Web Component development (and this applies to React as well), to "table driven development," and I had an opportunity to explain it in more detail this week. Table Driven Development is nothing more than identifying what is the minimum amount of syntax you need to express the data you use in your web page?
Let's start with a real-life example. This is a tab-based control from the Patternfly library, taken from a real project:
<div class="pf-c-toggle-group">
<div class="pf-c-toggle-group__item">
<button
class="pf-c-toggle-group__button ${this.mode === Flavors.Vanilla
? "pf-m-selected"
: ""}"
type="button"
@click=${() => { this.mode = Flavors.Vanilla; }}>
<span class="pf-c-toggle-group__text">${msg("Vanilla")}</span>
</button>
</div>
<div class="pf-c-divider pf-m-vertical" role="separator"></div>
<div class="pf-c-toggle-group__item">
<button
class="pf-c-toggle-group__button ${this.mode === Flavors.Strawberry
? "pf-m-selected"
: ""}"
type="button"
@click=${() => { this.mode = Flavors.Strawberry; }}>
<span class="pf-c-toggle-group__text">${msg("Strawberry")}</span>
</button>
</div>
<div class="pf-c-divider pf-m-vertical" role="separator"></div>
<div class="pf-c-toggle-group__item">
<button
class="pf-c-toggle-group__button ${this.mode === Flavors.Chocolate
? "pf-m-selected"
: ""}"
type="button"
@click=${() => { this.mode = Flavors.Chocolate; }}>
<span class="pf-c-toggle-group__text">${msg("Chocolate")}</span>
</button>
</div>
</div>
(Example 1)
This is a wall of code, the meaning of which is obscured by the surrounding decorations. When we work in Web Components, we are already committed to the Lisp philosophy of "writing code that writes code." Whether its Lit or React, we are working in an environment where the code we write generates HTML, CSS, and JavaScript that the browser then interprets. Web Developers do more metaprogramming than any C# hacker.
So the first thing would be to extract a single button into a function, and inject that into a
map()
. But why stop there? Why not take this entire construct and wrap it into a component?
And this is where we start talking about Table Driven Development. Because ultimately our code will look more like this:
const flavors: [string, string] = [
[Flavors.Vanilla, msg("Vanilla")],
[Flavors.Strawberry, msg("Strawberry")],
[Flavors.Chocolate, msg("Chocolate")]
];
return html`<my-toggle-group
.options=${flavors}
value=${this.mode}
@my-toggle=${(ev) => { this.flavor = ev.detail.flavor }
></my-toggle-group>`
(Example 2)
Alternatively, you could make it look like this:
return html`
<my-toggle-group value=${this.mode} @my-toggle=${(ev) => { this.flavor = ev.detail.flavor }>
<option value=${Flavors.Vanilla}>Vanilla</option>
<option value=${Flavors.Strawberry}>Strawberry</option>
<option value=${Flavors.Chocolate}>Chocolate</option>
</my-toggle-group>`
(Example 3)
Which one you prefer is a matter of personal taste. I actually like Example 3 more because it kinda emphasizes the HTML-ness of our project, although replacing the contents of the host element with your template while preserving the information from the host element is a somewhat advanced topic.
But the first one does show the minimum amount of syntax needed to reveal the data that we're going to turn into HTML & CSS source code, which is a huge win in its own right. Natually, you could split the difference:
<my-toggle-group value=${this.mode} @my-toggle=${(ev) => { this.flavor = ev.detail.flavor }>
${flavors.map(([key, label]) => `<option value=${key}>${label}</option>`)}
</my-toggle-group>`
(Example 4)
This does obscure the results in your source code, but it also preserves the appearance in view source
and so on. To make things easier for you (and harder to mess up), the source file for
my-toggle-group
could export that mapping function:
<my-toggle-group value=${this.mode} @my-toggle=${(ev) => { this.flavor = ev.detail.flavor }>
${toggleGroupOption(flavors)}
</my-toggle-group>`
(Example 5)
Although that starts to obscure how my-toggle-group
takes its list of buttons to show.
All four of my alternatives are to be preferred to the wall-of-code of the original example. Aside from the data declaration itself, which is four lines, the only "decorating" code you have to write is three more lines: the opening tag, the content line, and the closing tag.
I prefer the ones where the values look like HTML, but that's not always possible; not every
attribute can be a property or translated into one, and the Lit thing of using JSON.Stringify
to
transport properties across attributes makes inspect
worthlessly hard to read. Fortunately, Lit
has the property is not an attribute
setting and can just access the referenced component's
JavaScript directly without having to use the attribute. This sort of interface, with complex
objects rather than just their keys, being passed back and forth can't be supported with the native
option
object; you would have to create a web component to hold your complex value, and that
starts to get messy.
Table-driven web development understands first and foremost that web pages are full of repetition and clutter; every object could have classes describing its CSS or providing a hook for Javascript, and with Web Components they'll have attributes and properties for modifying the behavior of the component. Every form is a list of fields and how to process them; if you have a lot of forms, it's not enough to use a forms manager, you should consider looking at your uses and encapsulate every collection of label + input + error messages + tooltips + etc into a function that generates the code you want, and then write a function, perhaps a recursive one, that takes a table (perhaps a nested table) of POJOs describing your form... and just spit out the form. Separate out "maintaining the data we collect" from "how to collect a piece of data."
Your first job as a developer is to make code legible to your peers, and then make it usable to the programs that will run it. I believe most re-writes are triggered by unreadability, and ultimately fail because re-writes are hard to get correct. Web Components and CSS Libraries can create a ton of clutter, and you do yourself a disservice leaving that visible everywhere when what you want to say is "Here, pick a favorite flavor."