Progressive Enhancement


Yeah, using Vue or custom elements in the frontend is cool they can greatly improve compartmentalization of code through the use of components, but what happens when the JS code just… doesn’t load. Be it the user blocking it, the browser bugging out, or the CDN dying, those things happen.

If a form relies on the fields being components, what happens? Does the form just not render, does it break in any other way? Yes, it does, and submission becomes impossible.

But it doesn’t have to.

Scenario

We want to create an input that also contains a character counter that tells the user how many characters they can still use. Think, the tweet editor on Twitter.

Vue implementation

Creating a component like that with Vue is extremely simple.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<div class="counter-input">
<label :for="name">{{ name }} ({{ count }})</label>
<input type="text" :name="name" :id="name" :maxlength="max" v-model="text" />
</div>
</template>

<script>
export default {
name: "Counter",
props: {
name: String,
max: Number,
},
data() {
return {
text: "",
};
},
computed: {
count: function () {
let l = this.text?.length ?? 0;
let m = this.max ?? 0;
return `${l}/${m}`;
},
},
};
</script>

It’s fully reactive, fairly small byte-wise, and does the job splendidly. And, above all, it’s fully reusable in the context of Vue. That is, it can be (re)used inside of other Vue components or Vue apps.

Since we’re talking about enhancement here, let’s not consider the context of full SPAs — that don’t work when JS fails regardless — and rather in the context of enhancing an existing SSR site — like Genfic — with a new Vue({}) here and there.

All that means, that wherever we need just a single input with a counter, we also need to load the full Vue framework, and the component. Should any part of this contraption fail, everything fails. Not to mention using a full framework when all we need is an input with character counter is overkill.

Webcomponents

Fear not, though, as webcomponents are here! A fully-native way to create reusable components in the form of what basically is custom HTML tags. Thanks to that, we can drop the overhead of any framework and just focus on the component itself. It could look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
customElements.define(
"x-counter",
class Counter extends HTMLElement {
constructor() {
super();

this.innerHTML = `
<div class="counter-input">
<label for="${this.name}">${this.name} (0/${this.max})</label>
<input type="text" name="${this.name}" id="${this.name}" maxLength="${this.max}" />
</div>
`;
}

connectedCallback() {
let label = this.querySelector("label");
let input = this.querySelector("input");

input.addEventListener("input", (e) => {
let count = e.currentTarget.value.length;
label.innerText = `${this.name} (${count}/${this.max})`;
});
}

static get observedAttributes() {
return ["name", "max"];
}

get name() {
return this.getAttribute("name");
}
set name(value) {
this.setAttribute("name", value);
}

get max() {
return this.getAttribute("max");
}
set max(value) {
this.setAttribute("max", value);
}
}
);

It certainly is an increase in complexity and size, but we still get a reusable component, this time without the overhead of having to use a framework.

Still, however, if the file that contains this component does not get loaded or breaks in any way, the whole form breaks. There’s no fallback or anything of the sort. The most we can offer is <noscript> :( </noscript>.

Progressive enhancement

If we take a look at how the web used to be, we’ll notice, that it already offered progressive enhancement. All the content, all the elements were already there, merely being enhanced with Javascript, was it to load.

If it didn’t? No big deal. Everything still works. A bit less smoothly, perhaps, with a bit less flair, but it does work.

What about compartmentalization, then? Well, are HTML tags not containers? If we could grab a list of tags we want to enhance and work in their context…

Turns out, we can:

1
2
3
4
<div class="counter-input">
<label for="count">Count</label>
<input type="text" name="count" id="count" maxlength="20" />
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(function () {
const elements = document.querySelectorAll(".counter-input");

for (let element of elements) {
let label = element.querySelector("label");
let input = element.querySelector("input");
let text = label.innerText;

label.innerText = `${text} (0/${input.maxLength})`;

input.addEventListener("input", (e) => {
let len = input.value?.length ?? 0;
label.innerText = `${text} (${len}/${input.maxLength})`;
});
}
})();

That’s it.

Half the lines needed for a native webcomponent, no reliance on any third-party code, a drop-in solution for wherever we need it to enhance the existing experience.

And when Javascript breaks? Well, the form is still there, firmly in place. Still completely usable, merely missing a character counter. Adding a progress bar, red highlight if the length exceeds the maximum, whatever else you might need, can also be done the same way.

“What about all this HTML you have to write”, you might ask. And you would be right to do so! It’s four whole lines as opposed to just one! Then again, it is quite a simple example, a more complex one will easily grow in size.

Fret not. Remember? We’re enhancing an existing SSR site here. Components can still be used, be it in the form of tag helpers, partials, or whatever your server-side templating engine offers.

With Twig, for example, we could create a partial like

1
2
3
4
5
{# counter.twig #}
<div class="counter-input">
<label for="{{name}}">{{name}}</label>
<input type="text" name="{{name}}" id="{{name}}" maxlength="{{max}}" />
</div>

that can be used with

1
{% include 'counter.twig' with { 'name': 'Count', 'max': 20 } %}

Problem solved.