# wc-mixin
A typed mixin factory that removes custom-element boilerplate.
```ts
import { WebComponent, disable, enable, number, string, type PropChange } from 'wc-mixin';
const SPEC = {
shadow: 'open', // omit for light DOM
template: '',
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) {
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()`, `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 `