Go to file
Buddy c961d01fc4 Add typed WebComponent mixin factory
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.
2026-11-06 00:08:56 -08:00
src Add typed WebComponent mixin factory 2026-11-06 00:08:56 -08:00
test Add typed WebComponent mixin factory 2026-11-06 00:08:56 -08:00
.gitignore Initial commit 2026-11-05 07:53:57 +00:00
LICENSE Add typed WebComponent mixin factory 2026-11-06 00:08:56 -08:00
oxfmt.config.ts Add typed WebComponent mixin factory 2026-11-06 00:08:56 -08:00
oxlint.config.ts Add typed WebComponent mixin factory 2026-11-06 00:08:56 -08:00
package-lock.json Add typed WebComponent mixin factory 2026-11-06 00:08:56 -08:00
package.json Add typed WebComponent mixin factory 2026-11-06 00:08:56 -08:00
README.md Add typed WebComponent mixin factory 2026-11-06 00:08:56 -08:00
tsconfig.json Add typed WebComponent mixin factory 2026-11-06 00:08:56 -08:00
tsconfig.test.json Add typed WebComponent mixin factory 2026-11-06 00:08:56 -08:00
vitest.config.ts Add typed WebComponent mixin factory 2026-11-06 00:08:56 -08:00

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. onPropChange fires once per real change from either side.
  • Attribute names: an explicit attribute is used verbatim; otherwise kebab(key), with disable- for disable() and data- in front when dataPrefix: true.
  • Codecs: string, number, enable, disable, choice([...]), json<T>(), prop({ default, decode, encode }).
  • Shadow DOM: shadow attaches a root and clones template into it; css is a shared adopted stylesheet. An existing declarative shadow root is used instead (and shadow/template are ignored). root is the shadow root (even when closed) or the element itself.
  • Light DOM: template is appended on first connect; css is added once per element type, as a <style> in <head> or adopted into the enclosing shadow root.
  • signal aborts on disconnect; a new one is created on each connect. events are listened on the host with it.
  • base extends another autonomous element class; prop names that collide with its members (or HTMLElement's, like title/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.