Single File Web Components

Single File Web Components are a neat idea that should standard in browsers. They’re single file, web component rendered as pure html, very similar to vue but without the compiler.

In this post, i’ll integrate sfwc into this blog, improve it’s original loading method, and some minor tweaks to make them more ergonomical.

The Single File WebComponent

<sfwc>
<template name="x-hello">
<div class="text-xl">Hello World</div>
</template>
<style>
x-hello {
color: deeppink;
}
</style>
<script>
customElements.define('x-hello', class extends HTMLElement {
#template = document.querySelector('template[name="x-hello"]');
constructor() {
super();
const content = this.#template.content.cloneNode(true);
this.appendChild(content)
}
});
</script>
</sfwc>

Improving the original Loader

The original SFWC loader uses <object> to load it, but i find it quite bad. It causes a new javascript scope to be created, without access to window and having to rely on document.top. That’s not a clean abstraction.

Since the sfwc is just code, we can load it with fetch() instead.

// sfwc_components.js
// Import as a raw static file
import xcounter from "./x-counter.sfwc?url&no-inline"
import xhello from "./x-hello.sfwc?url&no-inline"
// sfwc-components.js
async function loadSFWC(url, name) {
console.log("loadSFWC", url, name)
let response = await fetch(url)
let data = await response.text()
const parser = new DOMParser()
const doc = parser.parseFromString(data, "text/html")
const template = doc.querySelector("sfwc template")
const style = doc.querySelector("sfwc style")
const old_script = doc.querySelector("sfwc script")
const new_script = document.createElement("script")
new_script.textContent = old_script.textContent
new_script.type = "module"
const new_el = document.createElement("sfwc")
new_el.setAttribute("name", name)
new_el.appendChild(template)
new_el.appendChild(new_script)
if (style) {
new_el.appendChild(style)
}
document.body.appendChild(new_el)
}
loadSFWC(xcounter, "x-counter")
loadSFWC(xhello, "x-hello")

Result

Here’s our sfwc component x-hello

It worked! But with some caveats: document.currentScript isn’t available anymore

No problem, we can uniquely name our templates and use querySelector to get them.

Improving sfwc components

Don’t use the shadow DOM, use the light dom!

Using only the light dom, we allow the components to inherit styles from the current page, and makes it so much easier to integrate it. To use the light dom, just don’t attach a shadow!

Set the style scope to tagname

We also put all of our styles inside x-mycomponent selector, so we don’t pollute the global styles.

<style>
x-hello {
color: var(--color-error);
}
</style>

Add a importmap to allow runtime import of modules

And add a importmap to allow importing Javascript modules from http URLs, no installation/build steps needed

<script type="importmap">
{
"imports": {
"npm/": "https://esm.sh/",
"preact": "https://esm.sh/preact@10.7.2",
"preact/": "https://esm.sh/preact@10.7.2/",
"preact-render-to-string": "https://esm.sh/preact-render-to-string@5.2.0?external=preact"
}
}
</script>

Then we can import any library in our sfwc with:

<script type="module">
import {html} from "npm/lit";
import { h } from "preact";
import { useState } from "preact/hooks";
import { render } from "preact-render-to-string";
</script>

Examples

Counter with lit library

Footnotes

  1. Single File Web Components
  2. WebC