WebComponent(spec) builds a custom-element base class from a spec: shadow root or declarative shadow DOM, template, shared stylesheet (adopted, or a <style> once per root for light DOM), reflected props with two-way attribute sync, a per-connection AbortSignal, host event wiring, and typed element refs. Types derive exact observedAttributes, prop value types, ref element types from selectors, and reject prop names that collide with the base class or mixin members. Tooling: strict tsconfig, type-aware oxlint, oxfmt, and Vitest browser mode (Chromium) with type tests. |
||
|---|---|---|
| src | ||
| test | ||
| .gitignore | ||
| LICENSE | ||
| oxfmt.config.ts | ||
| oxlint.config.ts | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| tsconfig.test.json | ||
| vitest.config.ts | ||
wc-mixin
A typed mixin factory that removes custom-element boilerplate.
import { WebComponent, disable, enable, number, string, type PropChange } from 'wc-mixin';
const SPEC = {
shadow: 'open', // omit for light DOM
template: '<input class="name"><button>Go</button>',
css: ':host { display: block; }',
events: ['click'],
element: { input: 'input.name' }, // refs.input: HTMLInputElement | null
elements: { buttons: 'button' }, // refs.buttons: HTMLButtonElement[]
props: {
userId: number(), // user-id number | undefined
min: number({ default: 1 }), // min number
ready: enable({ attribute: 'is-ready' }), // is-ready boolean (default false)
fetch: disable(), // disable-fetch boolean (default true)
},
} as const;
class ExampleElement extends WebComponent(SPEC) {
override onConnect() {
window.addEventListener('resize', () => this.layout(), { signal: this.signal });
}
override handleEvent(event: PointerEvent) {}
override onPropChange(change: PropChange<typeof SPEC>) {
if (change.name === 'min') change.value; // number
}
layout() {}
}
customElements.define('example-element', ExampleElement);
ExampleElement.observedAttributes; // readonly ('user-id' | 'min' | 'is-ready' | 'disable-fetch')[]
- Props are stored as values and reflected to attributes; attribute changes decode back.
onPropChangefires once per real change from either side. - Attribute names: an explicit
attributeis used verbatim; otherwisekebab(key), withdisable-fordisable()anddata-in front whendataPrefix: true. - Codecs:
string,number,enable,disable,choice([...]),json<T>(),prop({ default, decode, encode }). - Shadow DOM:
shadowattaches a root and clonestemplateinto it;cssis a shared adopted stylesheet. An existing declarative shadow root is used instead (andshadow/templateare ignored).rootis the shadow root (even when closed) or the element itself. - Light DOM:
templateis appended on first connect;cssis added once per element type, as a<style>in<head>or adopted into the enclosing shadow root. signalaborts on disconnect; a new one is created on each connect.eventsare listened on the host with it.baseextends another autonomous element class; prop names that collide with its members (orHTMLElement's, liketitle/hidden) are type errors.- Don't declare a subclass field with a prop's name; it would shadow the reflected accessor.
Scripts
npm run lint (oxlint, type-aware), npm run format / format:check (oxfmt), npm run typecheck, npm test (Vitest in headless Chromium + type tests), npm run build.