Skip to content

Web Components

October 27, 2022 | 08:37 PM

I’ve spent some time this past week trying to get slotted web components to work. In doing so I think I’ve unlocked an understanding of the quirks involved, along with a deeper understanding of the young Greenwood project that I used as my virtual lab bench 🔬

Table of contents

Open Table of contents

My goals

I wanted to rebuild my blog as the means to learn about web components, while avoiding large frameworks, and generate static HTML wherever possible.

I really wanted separation of concerns, (content 📝, layout 📐, style 🎨). I wanted to write my blog in markdown[.md], layout the form of the page with modular custom components[.html], and isolate their style[.css] from the rest of my page. Those were my goals, I don’t know why I was so obsessed with those goals but I really clung to them all the way through and I think it helped me learn more.

I am Tyler Durden 🤜👨‍💻

(I kicked my own ass) I thought Server Side Rendering (SSR) was pretty cool when I recognized how efficient it would be for consumers. I was always annoyed by a long page load just for some JS animations that probably weren’t worth it to me. Likewise, I thought this renaissance of Static Site Generation was great for web usability. While Astro.build has been a breeze to use, I felt like tinkering with some web stuff while I was laid up post-op 🛌

Web Components

The web component suite of APIs is supported across major browsers, not including the Declarative Shadow DOM (DSD). I hadn’t heard of web components until I started poking around for ways to reduce the bloat in my blog.

That led me to Greenwood, which aims to be a bastion of web standards and fundamentals. I appreciate the K.I.S.S. principles. I used Greenwood’s first project walkthrough to start my website build, and the documentation led me down a pathway which introduced the prerender features, which are great 👍. This of course is SSR, which will move all the browser rendering of JS to the cloud, and just send the client the static HTML.

Therein lies the rub.

In order to feed web components data, you must expose <slot>s. In order to use a slot, you need to create a <template> and in order to use a template, you need a shadowroot. If none of that sounds familiar I’d like to recommend these two articles as a primer: [Shadow DOM, Declarative Shadow DOM]. Really great site there.

So I need a shadowroot 🦹 and the DSD is a non-starter; I want this site to work consistently across browsers. I needed to create a shadowroot imperatively, that is, through JS. ☢️ This is where I start to fall apart, all sense of logic goes out the window, and I just start flipping bits and reading debug logs late into the evening.

facepunch

I mentioned that I was using prerender on my site right? Yeah… So why did I think prerendering would help me get to where I wanted to be?! I was fighting with Greenwood until I read the source for the WCC web components compiler, which does the job of prerendering. 🤦‍♂️ It looks for instantiations of a template element in your custom HTMLElement, and uses that to declare <templates shadowroot="open"> in the output HTML. That is a declarative shadow DOM. It’s also kind of obvious if you think about it, nothing imperative was ever going to make it to the client by design! Some days I’m just dense… I’d like to blame the prescription drugs 💊 and the late hours, but I’d be lying if I said this doesn’t happen now and then. That’s why I do this stuff, to resharpen my constantly dulling edges. 🔪

Pivot

I needed the code to run in the browser, to imperatively create my shadowroot. That meant I had to give up on SSR 😟. So I consolidated my HTML and CSS back into JS as strings in an HTMLElement class. Everything worked offensively well, but this is also not where I wanted to end up with my workflow.

Fuck it, I’ll do it myself

Client side rendering is the inverse of the above scenario. Which meant cramming all of the code and style together into something relatively unwieldy by comparison. Specifically, I don’t like editing HTML (or CSS), wrapped in a string literal, wrapped in javascript 🤮. These are all mimetypes for a reason, and my tools give me rich features if they can distinguish the type of file I’m editing.

Simple, brute force, file templates

You’ll notice, if you’ve written a few simple web components, the JS is often boilerplate. ⭐️ That’s the case I wanted to solve for.

import { readFileSync, readdirSync, writeFileSync } from "node:fs";

// Read unique webcomponent names from src/component_parts/
const wcnames = new Set();
readdirSync("src/component_parts").forEach((file) => {
  const n = file.substring(0, file.lastIndexOf(".")) || file;
  wcnames.add(n);
});

// Output a single JS module for client side rendering for each webcomponent
wcnames.forEach((WCNAME) => {
  const CSS = readFileSync(`src/component_parts/${WCNAME}.css`);
  const HTML = readFileSync(`src/component_parts/${WCNAME}.html`);
  const TEMPLATE = `export default class ${WCNAME} extends HTMLElement {
  constructor() {
    super();
    // Attach the shadow dom
    const shadowRoot = this.attachShadow({ mode: "open" });

    // Create the template for the shadow dom
    const template = document.createElement("template");
    template.innerHTML = \`${HTML}\`;

    // Create the style for the shadow dom
    const style = document.createElement("style");
    style.textContent = \`${CSS}\`;

    // Append the style to the shadow dom
    shadowRoot.appendChild(style);

    // Clone the template into the shadow dom
    shadowRoot.appendChild(template.content.cloneNode(true));
  }
}
customElements.define("${WCNAME}-component", ${WCNAME});
`;

  const path = `src/components/${WCNAME}.mjs`;
  writeFileSync(path, TEMPLATE);
});

There are certainly more complicated web components out there that absolutely will not benefit from this basic 🥴 templating.

When is this useful? ✅ Static reusable components that are self contained/isolated, and can be enriched by using <slot>s and CSS variables.

When is this not at all useful? ⛔️ Dynamic updates to the component.

tmux-web-component-dev

Useful?

I didn’t set out with the intention of building things this way. I started out wanting to stay as native as possible and avoid the overhead and holy ✠ wars of the frameworks. I suspect if any Greenwood devs saw what I’ve done here they’d be scream crying at the monitor because I could have done something so much simpler with their toolset 😹 I’m not sharing my code because I think it’s useful, it’s just an artifact of sharing my experience.

Here’s what I really get out of this type of digital wandering

  1. I learned about web components to a reasonable depth (Templates, Shadow DOM & Custom Elements)
  2. I discovered caniuse.com
  3. I learned some new CSS things like :host and ::slotted
  4. I put my NeoVim LSP configuration through its paces and improved it
  5. I got back into using Obsidian, because I found Notion too heavy and mouse oriented for programming notes
  6. I got around to setting up sshfs to share my Obsidian folder across my network with my laptop so I could work from bed ♿️ (back is still healing)
  7. I occupied my mind through sleepless nights with something that stimulates me and enriches my abilities as an engineering leader (clearly not webdev 😜)

Bonus content - my raw notes 🗒

The Shadow DOM

Motivation Using a Shadow DOM should give me good LSP support in NeoVim, separation of concerns from the larger aggregate pages.

Important notes:

  • For styles to work with <slot> tags they all must be a part of a Shadow DOM
  • For a Shadow DOM to exist one must use a <template>
  • There are two ways to create a Shadow DOM
    • ✅ Imperative Shadow DOM (ISD) - well supported, done via JS
    • ⛔️ Declarative Shadow DOM (DSD) - WIP, Chrome only, done via HTML

Work arounds

  1. Astro is using this template-shadowroot package to polyfill.
    • Maybe I could make this a plugin and contribute back to #greenwood
  2. Just use ISD - I can probably still keep the code compartmentalized in HTML files with some elbow grease

The issue with 2 is that that rules out SSR. I can’t SSR imperatively. The rendering of JS => HTML declares the layout, making the Shadow DOM declarative.

In Conclusion

So in my pursuit of fast, I’ve dug too deep, SSR is a no go if I want to use slots, because slots require the ShadowDOM and DSD is not working in all browsers yet. What I’m left with is a choice.

  1. Continue with SSR methods w/o slots (no Shadow DOM)
  2. Static Site Generate as much as possible but ship JS to the client to use slots (Shadow DOM)

I’m going with 2️⃣, I think better edge caching is reducing the need for static site generation and SSR in general. Sure SSG is good if you don’t want to do anything with web components, but since I do, my only option is to let the browser have the JS. 🫡

Styling Details here

Good simple sandbox here CodePen