Appearance
Document Bootstrap
Bootstrap a web component by cloning a <template>, discovering bindings through a plugin system, appending the content to the component element, and returning a reactive render function powered by Vue's effect(). This lifecycle draws on a five-phase model (init, descend, load, attach, detach) refined into four plugin methods: discover, initialize, update, and cleanup.
When to use
Use this pattern to bootstrap web components from <template> elements with reactive rendering. It suits custom elements that need reactive data binding and automatic re-rendering without a framework.
The pattern
Using bindTemplate
Create a template in your HTML, define a custom element class with reactive data, and call bindTemplate with the template and the element instance. It returns a render function that sets up reactive updates.
html
<template name="my-app-template">
<div>
<h1>{{ data.title }}</h1>
<button on:click="data.count++">
Clicked {{ data.count }} times
</button>
<input
.value="data.name"
on:input="data.name = event.target.value"
>
<p @if="data.name">Hello, {{ data.name }}!</p>
</div>
</template>
<my-app></my-app>
<script type="module">
import {
bindTemplate, reactive,
} from "@chriscalo/web-component-kit";
class MyApp extends HTMLElement {
connectedCallback() {
this.data = reactive({
title: "My Reactive App",
count: 0,
name: "",
});
const render = bindTemplate(
document.querySelector(
'template[name="my-app-template"]'
),
this,
);
render();
}
}
customElements.define("my-app", MyApp);
</script>Waiting for components
Use componentsReady to wait until custom elements are defined before interacting with them.
javascript
export async function componentsReady(...names) {
await Promise.all(
names.map(name => customElements.whenDefined(name))
);
}
// Usage
await componentsReady("my-app", "ui-icon");The bindTemplate implementation
bindTemplate accepts a template (selector or element) and an instance (typically a custom element). It clones the template, runs plugin discovery, appends the content, and returns a render() function that sets up a Vue effect() to re-run updates whenever reactive data changes.
javascript
export function bindTemplate(templateSelector, instance) {
// Resolve template
const templateEl =
typeof templateSelector === "string"
? document.querySelector(templateSelector)
: templateSelector;
const root = templateEl.content.cloneNode(true);
const boundElements = new Map();
// Discovery phase — each plugin finds its bindable
// elements and initializes them
for (const [name, plugin] of bindingPlugins) {
const elements = plugin.discover(root);
for (const element of elements) {
const domEl = element.element || element;
if (!boundElements.has(domEl)) {
boundElements.set(domEl, []);
}
const metadata = plugin.initialize(element, instance);
boundElements.get(domEl).push({
plugin,
metadata,
element: domEl,
});
}
}
// Attach cloned content to the instance element
instance.appendChild(root);
// Return reactive render function
let renderEffect;
return function render() {
if (renderEffect) {
renderEffect.stop?.();
}
renderEffect = effect(() => {
for (const [element, bindings] of boundElements) {
for (const { plugin, metadata } of bindings) {
plugin.update(element, instance, metadata);
}
}
});
};
}Lifecycle phases
The plugin lifecycle maps to an earlier five-phase component model.
| Phase | Plugin method | Purpose |
|---|---|---|
| init | discover | Find bindable elements in the template clone |
| descend | (implicit) | TreeWalker traversal during discover |
| load | initialize | Set up bindings and return metadata |
| attach | update | Update DOM when data changes |
| detach | cleanup | Remove event listeners and free resources |
Trade-offs
- Dual-purpose instance. The
instanceparameter serves as both the data context (expressions evaluate viawith (instance)) and the DOM container (content is appended withinstance.appendChild). This is concise but couples data and DOM concerns. - Vue reactivity dependency. The render function wraps all updates in a Vue
effect(), so@vue/reactivitymust be available (typically imported from a CDN). This gives automatic re-rendering but adds a runtime dependency. - Plugin ordering matters. Plugins are discovered in registration order. The conditional plugin skips itself when re-binding nested elements to prevent infinite recursion.
- No virtual DOM. Updates modify the real DOM directly. For large component trees, this may be less efficient than diffing approaches, but for prototyping and small apps the simplicity is worthwhile.