@vielzeug/codex 26.10.16 → 26.10.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/data/catalog.json +44 -44
- package/data/llms-full.txt +34 -8
- package/data/llms.txt +1 -1
- package/data/manifest.json +1 -1
- package/data/packages/archive.json +2 -2
- package/data/packages/ore.json +1 -1
- package/data/search.json +3 -3
- package/package.json +3 -2
package/data/packages/ore.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"apiSource": "export type { ComponentDefinition } from './component-types';\nexport { createContext, type InjectionKey, inject, injectStrict, provide } from './context';\nexport { define } from './define';\n// Near-universal template directives: used in most non-trivial components (lists,\n// conditionals, and class/style maps. Kept in the main entry alongside\n// `html`/`define` rather than a separate sub-path: tree-shaking already means an unused export\n// costs nothing in a bundled consumer, so splitting these off only adds an extra import line\n// for functionality most components need on day one. `unsafeHtml()` and `live()` remain here\n// too: their explicit names make their specialized behavior clear without a second import path.\nexport { classMap } from './directives/classMap';\nexport { each } from './directives/each';\nexport { type LiveBinding, live } from './directives/live';\nexport { type ModelOptions, model } from './directives/model';\nexport { styleMap } from './directives/styleMap';\nexport { unsafeHtml } from './directives/unsafe-html';\nexport { when } from './directives/when';\nexport { OreApiError, OreError, type OreErrorPhase, OreInternalError, OreLifecycleError } from './errors';\nexport { type FormFieldHandle, type FormFieldOptions, type FormFieldValidity, useField } from './forms/field';\nexport {\n type AttributeBindings,\n type BindOptions,\n bind,\n type HostBindConfig,\n type HostBindingValue,\n} from './host-bind';\nexport type { InferProps, PropDef, PropInputDefs, PropsDef } from './props';\nexport { prop } from './props';\n// Lifecycle hooks: plain functions, called during setup() or a composable it invokes.\nexport {\n getHost,\n type OnFormResetCallback,\n type OnMountedCallback,\n onCleanup,\n onElement,\n onEvent,\n onFormReset,\n onMounted,\n watchEffect,\n} from './runtime';\nexport { type ComponentSlots, useSlots } from './slots';\nexport { html } from './template/instantiator';\nexport { type HTMLResult, type Ref, type RefCallback, ref } from './template/result';\nexport { type CSSResult, css } from './utils/css';\nexport type { MaybeReactive } from './utils/dom';\nexport { type EmitFn, useEmit } from './utils/emit';\n\nexport { createId } from './utils/id';\n",
|
|
3
3
|
"docs": {
|
|
4
4
|
"index": "---\ntitle: 'Ore: Web component authoring with signals'\ndescription: Functional custom-element authoring with typed props, reactive templates, lifecycle helpers, and testing utilities.\npackage: ore\ncategory: ui-primitives\nkeywords: [web-components, custom-elements, reactive, templates, signals, lifecycle]\nrelated: [ripple, refine, orbit]\nexports: [define, prop, html, css, ref, createContext, inject, injectStrict, provide, onMounted, onCleanup, onEvent, onElement, onFormReset, watchEffect, useEmit, useSlots, getHost, bind, each, when, classMap, styleMap, live, model, unsafeHtml, useField, createId, OreError, OreApiError, OreInternalError, OreLifecycleError, AttributeBindings, BindOptions, MaybeReactive]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"ore\" />\n\n## Why Ore?\n\nOre keeps custom elements functional and signal-driven while giving you direct control over templates, lifecycle hooks, host bindings, and form-associated behavior.\n\n```ts\n// Before: vanilla custom element boilerplate\nclass MyCounter extends HTMLElement {\n #count = 0;\n connectedCallback() {\n this.attachShadow({ mode: 'open' });\n this.#render();\n }\n #render() {\n this.shadowRoot!.innerHTML = `<button>${this.#count}</button>`;\n this.shadowRoot!.querySelector('button')!.onclick = () => {\n this.#count++;\n this.#render();\n };\n }\n}\ncustomElements.define('my-counter', MyCounter);\n\n// After: Ore\nimport { signal } from '@vielzeug/ripple';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('my-counter', {\n setup() {\n const count = signal(0);\n return html`<button @click=${() => count.value++}>${count}</button>`;\n },\n});\n```\n\n| Feature | Ore | Lit | Stencil |\n| -------------------------- | ------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------ |\n| Bundle size | <PackageInfo package=\"ore\" type=\"size\" /> | ~12 kB | ~60 kB+ toolchain |\n| Signal-first runtime | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> (separate signals package) | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Functional component setup | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Typed prop helpers | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Host binding helpers | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial | Partial |\n| Form-associated helpers | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual | Partial |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Ore when** you want typed, signal-driven custom elements with minimal runtime overhead and no framework lock-in.\n\n**Consider Lit when** you need a mature ecosystem with wide community adoption and don't need signal-based reactivity.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/ore @vielzeug/ripple\n```\n\n```sh [npm]\nnpm install @vielzeug/ore @vielzeug/ripple\n```\n\n```sh [yarn]\nyarn add @vielzeug/ore @vielzeug/ripple\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { computed, signal } from '@vielzeug/ripple';\nimport { bind, css, define, html, onMounted, prop } from '@vielzeug/ore';\n\ndefine('my-counter', {\n props: {\n label: prop.string('Count'),\n step: prop.number(1),\n },\n styles: [\n css`\n :host {\n display: inline-grid;\n gap: 0.5rem;\n }\n `,\n ],\n setup(props) {\n const count = signal(0);\n const doubled = computed(() => count.value * 2);\n\n bind({ class: { 'is-positive': () => count.value > 0 } });\n\n onMounted(() => console.log('mounted'));\n\n return html`\n <button @click=${() => (count.value += props.step.value)}>${props.label}: ${count}</button>\n <p>Doubled: ${doubled}</p>\n `;\n },\n});\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- Signal-first runtime with `signal`, `computed`, `watch`, `batch` from `@vielzeug/ripple`: import them directly\n- Functional component authoring via `define(tag, { props, setup, styles, formAssociated })`\n- Props via `prop.*` helpers (`prop.string`, `prop.number`, `prop.bool`, `prop.oneOf`, `prop.json`, `prop.data`) or raw `PropDef` objects\n- `setup(props)` takes only props and returns an `HTMLResult` directly: `return html\\`...\\``\n- Lifecycle hooks: `onMounted`, `onCleanup`, `onEvent`, `onElement`, `watchEffect`: plain functions imported from `@vielzeug/ore`, called directly from `setup()` or any composable it calls\n- Directives: `each` (transactional keyed list reconciliation), `classMap`, `styleMap`, `when`, `live`, `model`, `unsafeHtml`\n- Host bindings via `bind({ attr, class, style, on })`: pass `{ target: el }` to bind any off-host element\n- Reactive ARIA sync via explicit `aria-*` keys in `bind({ attr }, { target })`, with automatic cleanup on disconnect\n- Context via `provide(key, value)` / `inject(key)`; typed emit/slots via `useEmit<Emits>()` / `useSlots<SlotNames>()`\n- Form-associated `useField()` and observer helpers are root exports\n- Testing utilities (`@vielzeug/ore/testing`): `mount`, `renderHook`, `flush`, `cleanup`\n- Generic testing utilities (scoped queries, named event dispatchers, and async waits) are exported by `@vielzeug/assay`\n\n</div>\n\n## Package Entry Points\n\n| Import | Purpose |\n| --------------------------- | ----------------------------------------------------------------------------- |\n| `@vielzeug/ore` | All browser runtime APIs: components, directives, `useField`, and observers |\n| `@vielzeug/ore/testing` | Ore-specific mounting, lifecycle flushing, hooks, cleanup, and form internals |\n| `@vielzeug/assay` | Generic DOM events, scoped queries, and async waiting |\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Refine](../refine/index.md) for prebuilt accessible components powered by Ore.\n- [Ripple](../ripple/index.md) for reactive state used inside Ore components.\n- [Forge](../forge/index.md) for typed form state that integrates with Ore.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
-
"api": "---\ntitle: 'Ore: API Reference'\ndescription: Complete API reference for @vielzeug/ore and @vielzeug/ore/testing.\n---\n\n[[toc]]\n\n## API Overview\n\nAll browser-runtime symbols below are imported from `@vielzeug/ore`. Lifecycle/context/binding functions (`onMounted`, `onCleanup`, `onEvent`, `onElement`, `watchEffect`, `bind`, `provide`, `useEmit`, `useSlots`, `getHost`) resolve the active component through an implicit \"current component\" context: they work when called synchronously during `setup()`, or from any composable function `setup()` calls (transitively), but throw if called outside that window.\n\n> `watchEffect` is not named `watch`: `@vielzeug/ripple` already exports a `watch(source, callback)` with different semantics (explicit source + old/new value pair), and the two are frequently imported in the same file.\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| ---------------------- | ----------------------------------------------------- | -------------- | -------------------------------------------------------------------------- |\n| `define()` | Create and register a custom element | Sync | Tag must contain a hyphen; call before first use |\n| `html` | Tagged template literal returning HTMLResult | Sync | Expressions must be signals, functions, or primitives |\n| `prop.*` | Typed prop helpers (string, bool, number, …) | Sync | Prop values are signals: read `.value` |\n| `provide()`/`inject()` | Context API for parent-to-descendant sharing | Setup only | Must be called synchronously during `setup()` |\n| `ref()` | Reactive reference to a DOM element | Sync | Value is null until after first mount |\n| `createContext()` | Create a unique typed injection key | Sync | Context is scoped to the component tree |\n| `each()` | Keyed list rendering with DOM diffing | Sync | Duplicate keys report `ore:error`; plain `T[]` is a one-time static render |\n| `when()` | Conditional branch rendering | Sync | Getter-fn computed disposed on cleanup; static bool skips subscription |\n| `live(signal)` | One-way binding that skips stale writes during input | Sync | Use for controlled inputs alongside a manual `@input` handler |\n| `model(signal)` | Two-way form-control binding (value/checked + write-back) | Sync | Plain `value`/`checked` attribute only; pass `get` to keep numeric signals numeric |\n| `onMounted(fn)` | DOM-ready callback | Setup only | Must be called synchronously during `setup()` |\n| `onCleanup(fn)` | Register teardown | Setup only | Called on component disconnect |\n| `onEvent(target, …)` | Scoped event listener with auto-cleanup | Setup only | No-ops on null target; removed on disconnect |\n| `useField(options)` | Wire signal to form `ElementInternals` | Setup only | Requires `formAssociated: true` on the component definition |\n| `onFormReset(fn)` | Run work when the ancestor `<form>` resets | Setup only | Fires every reset (not one-shot); only for `formAssociated: true` components |\n| `useEmit<Emits>()` | Typed `emit()` bound to the current host | Setup only | Call once per component; returns `dispatchEvent`'s boolean (`false` if a listener called `preventDefault()`) |\n| `useSlots<SlotNames>()`| Reactive slot presence/element signals | Setup only | Safe to call more than once: the underlying registry is created once |\n| `getHost()` | The current component's host element | Setup only | Prefer a higher-level helper (`bind`, …) when one exists |\n\n## Package Entry Points\n\n| Import | Purpose |\n| ------------------------- | ------------------------------------------------------------------ |\n| `@vielzeug/ore` | All browser runtime APIs, including directives, fields, and lifecycle helpers |\n| `@vielzeug/ore/testing` | Ore-specific mounting, lifecycle, hook, cleanup, and form test support |\n| `@vielzeug/assay` | Generic DOM events, scoped queries, and async waiting |\n\n## Core Component API\n\n### `define(tag, definition)`\n\n```ts\ndefine<Props>(tag: string, definition: ComponentDefinition<Props>): CustomElementConstructor;\n```\n\nReturns the registered class (for `instanceof` or subclassing); registration itself is the only global side effect.\n\nThe `setup()` function receives only typed prop signals:\n\n```ts\nsetup(props) {\n return html`<div>${props.label}</div>`;\n}\n```\n\nEverything else: lifecycle hooks, host bindings, context, slots, emit: is a plain function imported from `@vielzeug/ore`, called directly from `setup()` (or a composable it calls):\n\n```ts\nimport { define, html, onMounted, useEmit, useSlots } from '@vielzeug/ore';\n\ndefine('my-card', {\n setup(_props) {\n const emit = useEmit<{ close: undefined }>();\n const slots = useSlots<'header' | 'footer'>();\n\n onMounted(() => console.log('mounted'));\n\n // emit() returns dispatchEvent's boolean: false if a listener called preventDefault()\n const notCancelled = emit('close');\n\n return html`${when(slots.has('header'), () => html`<slot name=\"header\"></slot>`)}`;\n },\n});\n```\n\n`useEmit<Emits>()` and `useSlots<SlotNames>()` are factory hooks: call them once per setup run to get a typed\n`emit`/`slots` bound to the current host. `useSlots()` is safe to call more than once within that setup run.\n\n### ComponentDefinition\n\n```ts\ntype ComponentDefinition<Props> = {\n formAssociated?: boolean;\n props?: PropsDef<Props>;\n setup: (props: InferProps<PropsDef<Props>>) => HTMLResult | null;\n shadow?: Partial<ShadowRootInit> | false; // false = light DOM (no shadow root)\n styles?: (string | CSSStyleSheet | CSSResult)[];\n};\n```\n\n## Runtime Helpers\n\n`onMounted`, `onCleanup`, `onEvent`, `onElement`, and `watchEffect` are plain functions imported from `@vielzeug/ore`. Call them directly during `setup()`.\n\n```ts\nimport { html, onCleanup, onEvent, onMounted } from '@vielzeug/ore';\n\nsetup(props) {\n onMounted(() => {\n // DOM is ready; return a function for mount-scoped cleanup\n return () => { /* cleanup on unmount */ };\n });\n\n onCleanup(() => { /* called on disconnect */ });\n\n onEvent(window, 'keydown', (e) => { /* auto-removed on disconnect */ });\n\n return html`...`;\n}\n```\n\nBecause these resolve the active component through an implicit context (rather than a value threaded through parameters), composable helper functions can call them directly too: no need to pass hooks in as options:\n\n```ts\nimport { onCleanup } from '@vielzeug/ore';\n\nfunction useMyHelper() {\n onCleanup(() => { /* teardown */ });\n}\n\n// In setup:\nsetup(_props) {\n useMyHelper();\n return html`...`;\n}\n```\n\n## Props API\n\n| Helper | Signature | Notes |\n| ----------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------ |\n| `prop.string(defaultValue?)` | `PropDef<string>` | Reflects by default |\n| `prop.bool(defaultValue?)` | `PropDef<boolean>` | Any non-null attribute value other than `\"false\"` parses as `true`; `\"false\"` or absent attribute is `false` |\n| `prop.number(defaultValue?)` | `PropDef<number>` | Returns default (not NaN) and warns in dev when attribute is not a valid number |\n| `prop.oneOf(allowed, defaultValue?)` | `PropDef<T>` | Restricts to a fixed set of string or number members; `parse` falls back to `defaultValue` (or `undefined` when omitted) for anything outside the set. Numeric members match the attribute parsed as a number |\n| `prop.json(defaultValue)` | `PropDef<T>` | JSON.parse from attribute; `reflect: false` |\n| `prop.data<T>(defaultValue?)` | `PropDef<T>` | JS-only: never reads/writes an attribute; use for objects, arrays, callbacks, or any non-serialisable value |\n\n> **Choosing the right prop helper:**\n>\n> - **`prop.oneOf`**: the honest way to type a union-valued prop: the restriction is enforced at runtime (`parse` falls back for anything outside the set), not just in a type parameter a raw attribute value could violate. Pass an `as const` array of the allowed string or number members.\n> - **`prop.json`**: value can be declared in HTML (`<my-el config='{\"x\":1}'>`); attribute string is `JSON.parse`d.\n> - **`prop.data`**: value is always set from JavaScript (objects, arrays, callbacks, class instances); the attribute is never read. Use this for both data and function props.\n\nWhen you need custom parsing or `reflect: false`, use a raw `PropDef` object:\n\n```ts\nprops: {\n items: { default: [], parse: () => [], reflect: false },\n}\n```\n\nUse `prop.data` for props that hold JS-only values (including callbacks) that cannot be serialised through an HTML attribute:\n\n```ts\ndefine('data-grid', {\n props: {\n getRowKey: prop.data<(row: unknown) => string>(),\n columns: prop.data<DataGridColumn[]>([]),\n onSort: prop.data<(key: string) => void>(),\n },\n setup(props) {\n // Set from JS: grid.getRowKey = (row) => row.id\n return html`...`;\n },\n});\n```\n\n## Template and Directives\n\n### `html`\n\nTagged template literal that returns an `HTMLResult`. Supports text interpolation, ordinary attributes (`attr=`),\nboolean attributes (`?attr=`), explicit property bindings (`.prop=`), events (`@event=` with optional\n`.stop`/`.prevent`/`.once`/`.capture` modifiers), refs (`ref=`), and nested templates.\n\n`.prop=${value}` always writes the element property (never an attribute): the explicit channel for structured\nvalues (arrays, objects, callbacks). Prototype-polluting names (`__proto__`, `constructor`, `prototype`) are\nrejected at compile time.\n\n`@event.stop`, `.prevent`, `.once`, and `.capture` are shorthand for the matching listener behavior\n(`stopPropagation`/`preventDefault` applied before the handler runs; `once`/`capture` passed as listener\noptions). An unknown modifier throws at compile time.\n\n### `css`\n\nTagged template literal that returns a `CSSResult` for use in `styles`.\n\n### Directives\n\n| Directive | Purpose |\n| -------------------------------------- | ----------------------------------------------------------------------------------------------------- |\n| `each(source, key, render, fallback?)` | Keyed reactive list; render receives `Readable<T>` and `Readable<number>`; plain `T[]` is a one-time static snapshot |\n| `when(condition, truthy, falsy?)` | Conditional rendering |\n| `classMap(record)` | Reactive class string from object map |\n| `styleMap(record)` | Reactive inline style string from object map |\n| `live(signal)` | One-way binding that skips stale writes during active user input; use with `@input` handler |\n| `model(signal, options?)` | Two-way form-control binding: writes the signal to `value`/`checked` and writes user input back on the control's change event |\n| `unsafeHtml(value)` | HTML rendering sink; sanitize untrusted values before calling |\n\n`each()` treats number and string keys as distinct. A reconciliation failure (a duplicate key, or a render function that throws) reports `ore:error` and preserves the last valid list: entries created during the failed pass are rolled back, so the previous list stays exactly on screen.\n\n### `model(signal, options?)`\n\nTwo-way binding for a form control: `value=${model(signal)}`. It writes the signal to the control's `value`/`checked` property on every change and writes user input back into the signal on the control's change event (`'change'` for checkboxes, radios, and `<select>`; `'input'` otherwise). This is the complete controlled-input loop that the `live()` + manual `@input` handler pair used to require.\n\n```ts\nconst text = signal('');\nconst agree = signal(false);\nconst count = signal(0);\n\nhtml`\n <input value=${model(text)} />\n <input type=\"checkbox\" checked=${model(agree)} />\n <input type=\"number\" value=${model(count, { get: (el) => Number(el.value) })} />\n`;\n```\n\n`options` overrides the defaults for non-native controls: `event` sets the write-back event name and `get` reads the current value out of the element (default `el.checked` for checkboxes/radios, `el.value` otherwise). Because a native text input's `value` is always a string, pass a `get` like `(el) => Number(el.value)` to keep a numeric signal numeric. `model()` can only drive a plain `value`/`checked` attribute binding; using it on a boolean (`?checked=`) or other attribute throws.\n\n### `unsafeHtml`\n\n`unsafeHtml()` is an explicit HTML injection sink. It has no global sanitizer: sanitize untrusted\ncontent before passing it to the directive, so the trust boundary remains at the call site.\n\n```ts\nimport { unsafeHtml } from '@vielzeug/ore';\n\nconst safeArticle = sanitize(userSuppliedArticle);\n\nreturn html`<article>${unsafeHtml(safeArticle)}</article>`;\n```\n\n## Host Bindings\n\n`bind(config, options?)` is a plain function imported from `@vielzeug/ore`:\n\n```ts\nbind({\n attr: { role: 'button', 'aria-expanded': () => String(open.value) },\n class: { 'is-open': open },\n style: { '--height': () => height.value + 'px' },\n on: { click: handleClick },\n});\n```\n\n`bind()` auto-registers cleanup with the component scope: no manual `onCleanup` needed. Returns a cleanup function for early teardown.\n\n### Off-host bindings\n\nPass `{ target: el }` as a second argument to bind to any element other than the host:\n\n```ts\nbind(\n { attr: { 'aria-expanded': () => String(isOpen.value) } },\n { target: triggerEl },\n);\n```\n\nEvent listener options (`once`, `capture`, `passive`) are also accepted in the second argument. Cleanup is auto-registered with the component scope when called during setup.\n\n### Reactive ARIA attributes\n\nUse explicit `aria-*` keys in `bind({ attr }, { target })`:\n\n```ts\nconst stopAria = bind(\n {\n attr: {\n 'aria-controls': panelId,\n 'aria-expanded': () => isOpen.value,\n 'aria-haspopup': 'listbox',\n },\n },\n { target: triggerEl },\n);\n```\n\nStatic values are applied once. Getters and signals create reactive effects. Calling `stopAria()` stops updates. Values of `null`, `undefined`, or `false` remove the attribute.\n\n## Slots\n\n- `slots.has(name?)`: `Readable<boolean>`: whether the named (or default) slot has assigned content\n- `slots.elements(name?)`: `Readable<Element[]>`: the assigned elements for the slot\n\nSlot signals update reactively when assigned content changes, including when slots are inserted dynamically (via `when()` or `each()`) after mount.\n\n## Context API\n\n- `createContext<T>(description)`: Create a unique typed injection key; the description is diagnostic only\n- `provide(key, value)`: Provide a value to descendants\n- `inject(key)`: Resolve from nearest ancestor; returns `undefined` if not found\n- `inject(key, fallback)`: Resolve with a fallback value\n- `injectStrict(key)`: Resolve or throw if absent\n\n`provide()` and `inject()` must be called synchronously during `setup()`. Calling them outside a setup context throws\n`'Lifecycle hooks must be called during component setup'`. Context resolution walks the ancestor chain including shadow\nDOM boundaries. `inject()` resolves and caches its result once per consumer: provide a `Readable` (signal/computed)\nrather than a raw value if descendants need to observe later changes; re-calling `provide()` with a new raw value\nafterward is not seen by consumers that already resolved it (a dev-mode warning fires when a key is provided twice on\nthe same element). `provide()` registers cleanup automatically: context keys are removed from the registry when the\nproviding component disconnects, so reconnecting the same element runs `setup()` fresh without spurious \"overwriting\"\nwarnings or stale keys leaking to descendants.\n\n## Utilities\n\n- `ref<T>()`: Create a `Signal<T | null>` element reference. Set to the element via `ref=` in templates.\n- `createId(prefix = 'id')`: Generate a collision-resistant ID with a semantic prefix, a per-runtime tag, and a monotonic counter (for example, `'field-a3k21'`).\n\n## Form-Associated API\n\nImport from `@vielzeug/ore`.\n\n### `useField(options)`\n\nWire a form-associated element to `ElementInternals`. Requires `formAssociated: true` on the component definition. The `disabled` state tracking via `internals.states` (CustomStateSet) is skipped with a dev warning if the API is unavailable in the current environment.\n\n```ts\ntype FormFieldValidity = { flags: ValidityStateFlags; message: string };\n\ntype FormFieldOptions<T> = {\n disabled?: Readable<boolean>;\n /**\n * When true, a null/undefined value is submitted as '' instead of null,\n * keeping the field's key present in FormData even when the value is absent.\n * Only applies to the default toFormValue; ignored if toFormValue is provided.\n * @default false\n */\n emptyStringForNull?: boolean;\n /** Called when the ancestor <form> resets (see onFormReset): restore local field state here. */\n onReset?: () => void;\n toFormValue?: (value: T) => File | FormData | string | null;\n /**\n * Recomputed reactively and passed straight to internals.setValidity().\n * null (or omitted) = always valid. Flags and message travel together because\n * the platform's setValidity() contract couples them (flags without a\n * non-empty message throw); a single { flags, message } | null input makes a\n * desynced pair unrepresentable.\n */\n validity?: Readable<FormFieldValidity | null>;\n value: Signal<T> | Readable<T>;\n};\n\ntype FormFieldHandle = {\n checkValidity(): boolean;\n readonly internals: ElementInternals;\n reportValidity(): boolean;\n /** Set (non-empty message) or clear (empty string) a custom validity error. */\n setCustomValidity(message: string): void;\n};\n```\n\nPass a single `validity` signal to make `required`-style constraints participate in native constraint validation\nthrough `checkValidity()` and `reportValidity()`. The `{ flags, message }` pair is the one validity input: the old\nsplit `validity` + `validationMessage` options are gone, since they could drift apart into a state the platform\nrejects.\n\n```ts\nconst isBlank = (v: string) => v.trim() === '';\n\nuseField({\n validity: computed(() =>\n required.value && isBlank(value.value)\n ? { flags: { valueMissing: true }, message: 'This field is required.' }\n : null,\n ),\n value,\n});\n```\n\n## Testing APIs\n\nImport from `@vielzeug/ore/testing`.\n\n| API | Purpose |\n| ------------------------ | ------------------------------------------------------------------------------------------ |\n| `mount(setup, options?)` | Mount a component and return a test fixture |\n| `cleanup()` | Remove all mounted elements and reset test state |\n| `install(afterEach)` | Register auto-cleanup after each test |\n| `flush(options?)` | Drain reactive updates and animation frames |\n| `renderHook(setup)` | Run lifecycle hooks in isolation; overload accepts `propDefs` as first arg for typed props |\n| `OreTimeoutError` | Error thrown when `flush()` cannot settle tracked Ore work |\n\n> **Test isolation:** `cleanup()` removes mounted elements and resets Ore's cross-test caches and counters. Call it in `afterEach`, or use `install(afterEach)`.\n\nImport `within`, named dispatchers such as `fireClick`, and waits such as `waitUntil` or `waitForEvent` from\n`@vielzeug/assay`.\n\n> **Form-associated component testing:** jsdom implements none of the `ElementInternals` form-association API.\n> `@vielzeug/assay` exports `installFormInternalsPolyfill()` for this: it polyfills\n> `setFormValue`/`setValidity`/`checkValidity`/`reportValidity`/`validationMessage`/`validity`/`states`, mixes\n> `checkValidity`/`reportValidity`/`validity`/`validationMessage` onto the host element itself (real browsers do\n> this for any `formAssociated: true` element), makes `FormData` collect a form-associated element's set value, and\n> makes `<form>.reset()` invoke `formResetCallback()`. Every patch is a guarded no-op when its target already\n> exists, and the function returns an `uninstall()` that restores every patched global. Call it directly from a\n> test setup (alongside `install(afterEach)`) only when the suite tests form-associated components: the patches\n> are global, so suites without them shouldn't carry them. A downstream package (e.g. a component library built\n> on `ore`) should rely on it instead of hand-rolling its own copy.\n\n#### `Fixture` interface\n\n```ts\ninterface Fixture<T extends HTMLElement = HTMLElement> {\n [Symbol.dispose](): void; // Delegates to dispose(): enables `using` declarations\n element: T;\n readonly disposed: boolean; // true after dispose() has been called\n readonly shadow: ShadowRoot | null;\n get<E extends Element>(selector: string): E;\n query<E extends Element>(selector: string): E | null;\n queryAll<E extends Element>(selector: string): E[];\n getByText<E extends Element>(text: string, selector?: string): E;\n queryByText<E extends Element>(text: string, selector?: string): E | null;\n queryAllByText<E extends Element>(text: string, selector?: string): E[];\n getByTestId<E extends Element>(testId: string): E;\n queryByTestId<E extends Element>(testId: string): E | null;\n queryAllByTestId<E extends Element>(testId: string): E[];\n attr(name: string, value: string | number | boolean): Promise<void>;\n attrs(record: Record<string, string | number | boolean>): Promise<void>;\n flush(options?: FlushOptions): Promise<void>;\n act(fn: () => unknown): Promise<void>;\n dispose(): void; // Removes the component from the DOM: idempotent\n}\n```\n\n#### `renderHook`\n\nUseful for testing composable lifecycle hooks (`onMounted`, `watchEffect`, `inject`, etc.) without a template. `onMounted`/`onCleanup`/`watchEffect`/... work exactly as inside a real `setup()`, since they resolve the same implicit current-component context:\n\n```ts\n// Without props\nconst { result, flush, dispose } = await renderHook(() => {\n const count = signal(0);\n onMounted(() => {\n count.value = 1;\n });\n return count;\n});\nexpect(result.value).toBe(1);\n\n// With typed props (prop-defs overload)\nconst { result } = await renderHook({ label: prop.string('hello'), count: prop.number(0) }, (props) => props.label);\nexpect(result.value).toBe('hello');\n```\n\n## Ripple Primitives\n\nOre does **not** re-export reactive primitives. Import them directly from `@vielzeug/ripple`:\n\n```ts\nimport { batch, computed, signal, watch } from '@vielzeug/ripple';\n```\n\nSee the [Ripple documentation](/ripple/) for the full API.\n\n## Lifecycle Events\n\n| Event | When |\n| ------------- | ------------------------------------------------------------------------- |\n| `ore:error` | When a lifecycle callback fails: bubbles, composed; detail is `OreLifecycleError` |\n\n## Types\n\n```ts\ntype PropDef<T> = {\n readonly default: T;\n readonly parse: (value: string | null) => T;\n reflect?: boolean;\n};\n\ntype PropsDef<T extends Record<string, unknown>> = {\n [K in keyof Required<T>]: PropDef<T[K & keyof T]>;\n};\n\ntype PropInputDefs = Record<string, PropDef<unknown>>;\n\n/**\n * Infer reactive props type from a PropInputDefs map.\n * Each entry becomes Readable<T> keyed by prop name.\n */\ntype InferProps<D extends PropInputDefs> = {\n readonly [K in keyof D]-?: Readable<InferPropValue<D[K]>>;\n};\n\n// Runtime hooks: all plain functions imported from '@vielzeug/ore', not fields on an object.\ntype OnMountedCallback = () => Cleanup | undefined;\ntype OnFormResetCallback = () => void;\n\ndeclare function onMounted(fn: OnMountedCallback): void; // DOM-ready callback; runs after each connection's render\ndeclare function onCleanup(fn: Cleanup): void; // Register teardown; called on disconnect\ndeclare function onElement<T extends HTMLElement>(\n ref: Readable<T | null>,\n callback: (el: T) => Cleanup | undefined,\n): () => void;\ndeclare function onEvent<K extends keyof HTMLElementEventMap>(\n target: EventTarget | null | undefined,\n event: K,\n listener: (e: HTMLElementEventMap[K]) => void,\n options?: AddEventListenerOptions,\n): void;\ndeclare function onEvent(\n target: EventTarget | null | undefined,\n event: string,\n listener: EventListener,\n options?: AddEventListenerOptions,\n): void;\ndeclare function onFormReset(fn: OnFormResetCallback): void; // Runs on every ancestor <form> reset; formAssociated only\ndeclare function watchEffect(fn: () => Cleanup | undefined): () => void; // Scoped reactive effect; auto-cleaned on disconnect\ndeclare function bind(config: HostBindConfig, options?: BindOptions): () => void; // Bindings for host or any target element\ndeclare function provide<T>(key: InjectionKey<T>, value: T): void; // Register a context value on the host element\ndeclare function inject<T>(key: InjectionKey<T>): T | undefined;\ndeclare function inject<T>(key: InjectionKey<T>, fallback: T): T;\ndeclare function getHost(): HTMLElement; // The current component's host element\ndeclare function useEmit<Emits extends Record<string, unknown> = Record<string, never>>(): EmitFn<Emits>;\ndeclare function useSlots<SlotNames extends string = string>(): ComponentSlots<SlotNames>;\n\ntype ComponentDefinition<Props extends Record<string, unknown> = Record<never, never>> = {\n formAssociated?: boolean;\n props?: PropsDef<Props>;\n setup: (props: InferProps<PropsDef<Props>>) => HTMLResult | null;\n shadow?: Partial<ShadowRootInit> | false; // false = light DOM\n styles?: (string | CSSStyleSheet | CSSResult)[];\n};\n\ntype MaybeReactive<T> = T | Readable<T> | (() => T); // Plain value, signal, or getter: accepted by when(), each(), classMap(), and bind()\n\ntype HostBindingValue = MaybeReactive<string | number | boolean | null | undefined>;\n\ntype AttributeBindings = Record<string, HostBindingValue>;\n\ntype HostBindConfig = {\n attr?: AttributeBindings;\n class?: (() => Record<string, boolean>) | Record<string, Readable<boolean> | (() => boolean) | boolean>;\n on?: Record<string, ((event: Event) => void) | undefined>;\n style?: Record<string, HostBindingValue>;\n};\n\ntype BindOptions = AddEventListenerOptions & {\n target?: Element;\n};\n\ntype ComponentSlots<S extends string = string> = {\n elements(name?: S): Readable<Element[]>;\n has(name?: S): Readable<boolean>;\n};\n\ntype Ref<T extends Element> = Signal<T | null>;\n\ntype RefCallback<T extends Element> = (el: T | null) => void;\n\ntype InjectionKey<T> = symbol & { readonly __ore_injection_key?: T };\n\ninterface HTMLResult {\n mount(\n parent: ParentNode,\n anchor: Node | null,\n registerCleanup: (fn: () => void) => void,\n ): Node[];\n}\n\ntype CSSResult = {\n content: string;\n toString(): string;\n};\n\ntype LiveBinding<T> = { readonly source: Readable<T> };\n\ntype ModelOptions<T = unknown> = {\n /** Write-back event (default: 'change' for checkbox/radio/select, 'input' otherwise). */\n event?: string;\n /** Value reader (default: el.checked for checkbox/radio, el.value otherwise). */\n get?: (el: HTMLElement) => T;\n};\n\ntype EmitFn<T extends Record<string, unknown>> = {\n <K extends KeysWithoutDetail<T>>(event: K): boolean;\n <K extends Exclude<keyof T, KeysWithoutDetail<T>>>(event: K, detail: T[K]): boolean;\n};\n// KeysWithoutDetail is an internal helper type, not exported.\n\ntype FormFieldValidity = { flags: ValidityStateFlags; message: string };\n\ntype FormFieldOptions<T = unknown> = {\n disabled?: Readable<boolean>;\n emptyStringForNull?: boolean;\n onReset?: () => void;\n toFormValue?: (value: T) => File | FormData | string | null;\n validity?: Readable<FormFieldValidity | null>;\n value: Signal<T> | Readable<T>;\n};\n\ntype FormFieldHandle = {\n checkValidity: () => boolean;\n readonly internals: ElementInternals;\n reportValidity: () => boolean;\n setCustomValidity: (message: string) => void;\n};\n\n/** Phase in which a OreError occurred. */\ntype OreErrorPhase = 'each-reconcile' | 'form-reset' | 'mounted' | 'setup';\n```\n\n## Errors\n\n`OreError` is the base class for every Ore error class: `err instanceof OreError` catches all of them.\n\n- **`OreApiError`**: thrown when the `ore` API itself is misused: calling `define()` with a duplicate tag, calling a lifecycle hook (`inject`, `onMounted`, `onCleanup`, `onEvent`, …) outside of `setup()`, or passing an invalid prop definition to `define()`.\n- **`OreInternalError`**: thrown when an Ore invariant fails, indicating a package bug rather than invalid application code.\n- **`OreLifecycleError`**: reported in the `ore:error` event when component `setup()`, a mounted callback, a form-reset callback, or `each()` reconciliation fails. Extends `OreError` with:\n - `component: string`: the element's local name\n - `phase: OreErrorPhase`: `'setup'` | `'mounted'` | `'form-reset'` | `'each-reconcile'`\n - `cause: Error`: the original error thrown by `setup()`\n- **`OreTimeoutError`**: thrown by `flush()` (from `@vielzeug/ore/testing`) when pending Ore work does not settle before its timeout.\n\nLifecycle failures dispatch a bubbling, composed `ore:error` event whose `detail` is the `OreLifecycleError`. Setup\nfailures still rethrow their original error; mounted and form-reset callback failures are reported through the same\nevent so their remaining callbacks can continue.\n",
|
|
5
|
+
"api": "---\ntitle: 'Ore: API Reference'\ndescription: Complete API reference for @vielzeug/ore and @vielzeug/ore/testing.\n---\n\n[[toc]]\n\n## API Overview\n\nAll browser-runtime symbols below are imported from `@vielzeug/ore`. Lifecycle/context/binding functions (`onMounted`, `onCleanup`, `onEvent`, `onElement`, `watchEffect`, `bind`, `provide`, `useEmit`, `useSlots`, `getHost`) resolve the active component through an implicit \"current component\" context: they work when called synchronously during `setup()`, or from any composable function `setup()` calls (transitively), but throw if called outside that window.\n\n> `watchEffect` is not named `watch`: `@vielzeug/ripple` already exports a `watch(source, callback)` with different semantics (explicit source + old/new value pair), and the two are frequently imported in the same file.\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| ---------------------- | ----------------------------------------------------- | -------------- | -------------------------------------------------------------------------- |\n| `define()` | Create and register a custom element | Sync | Tag must contain a hyphen; call before first use |\n| `html` | Tagged template literal returning HTMLResult | Sync | Expressions must be signals, functions, or primitives |\n| `prop.*` | Typed prop helpers (string, bool, number, …) | Sync | Prop values are signals: read `.value` |\n| `provide()`/`inject()` | Context API for parent-to-descendant sharing | Setup only | Must be called synchronously during `setup()` |\n| `ref()` | Reactive reference to a DOM element | Sync | Value is null until after first mount |\n| `createContext()` | Create a unique typed injection key | Sync | Context is scoped to the component tree |\n| `each()` | Keyed list rendering with DOM diffing | Sync | Duplicate keys report `ore:error`; plain `T[]` is a one-time static render |\n| `when()` | Conditional branch rendering | Sync | Getter-fn computed disposed on cleanup; static bool skips subscription |\n| `live(signal)` | One-way binding that skips stale writes during input | Sync | Use for controlled inputs alongside a manual `@input` handler |\n| `model(signal)` | Two-way form-control binding (value/checked + write-back) | Sync | Plain `value`/`checked` attribute only; pass `get` to keep numeric signals numeric |\n| `onMounted(fn)` | DOM-ready callback | Setup only | Must be called synchronously during `setup()` |\n| `onCleanup(fn)` | Register teardown | Setup only | Called on component disconnect |\n| `onEvent(target, …)` | Scoped event listener with auto-cleanup | Setup only | No-ops on null target; removed on disconnect |\n| `useField(options)` | Wire signal to form `ElementInternals` | Setup only | Requires `formAssociated: true` on the component definition |\n| `onFormReset(fn)` | Run work when the ancestor `<form>` resets | Setup only | Fires every reset (not one-shot); only for `formAssociated: true` components |\n| `useEmit<Emits>()` | Typed `emit()` bound to the current host | Setup only | Call once per component; returns `dispatchEvent`'s boolean (`false` if a listener called `preventDefault()`) |\n| `useSlots<SlotNames>()`| Reactive slot presence/element signals | Setup only | Safe to call more than once: the underlying registry is created once |\n| `getHost()` | The current component's host element | Setup only | Prefer a higher-level helper (`bind`, …) when one exists |\n\n## Package Entry Points\n\n| Import | Purpose |\n| ------------------------- | ------------------------------------------------------------------ |\n| `@vielzeug/ore` | All browser runtime APIs, including directives, fields, and lifecycle helpers |\n| `@vielzeug/ore/testing` | Ore-specific mounting, lifecycle, hook, cleanup, and form test support |\n| `@vielzeug/assay` | Generic DOM events, scoped queries, and async waiting |\n\n## Core Component API\n\n### `define(tag, definition)`\n\n```ts\ndefine<Props>(tag: string, definition: ComponentDefinition<Props>): CustomElementConstructor;\n```\n\nReturns the registered class (for `instanceof` or subclassing); registration itself is the only global side effect.\n\nThe `setup()` function receives only typed prop signals:\n\n```ts\nsetup(props) {\n return html`<div>${props.label}</div>`;\n}\n```\n\nEverything else: lifecycle hooks, host bindings, context, slots, emit: is a plain function imported from `@vielzeug/ore`, called directly from `setup()` (or a composable it calls):\n\n```ts\nimport { define, html, onMounted, useEmit, useSlots } from '@vielzeug/ore';\n\ndefine('my-card', {\n setup(_props) {\n const emit = useEmit<{ close: undefined }>();\n const slots = useSlots<'header' | 'footer'>();\n\n onMounted(() => console.log('mounted'));\n\n // emit() returns dispatchEvent's boolean: false if a listener called preventDefault()\n const notCancelled = emit('close');\n\n return html`${when(slots.has('header'), () => html`<slot name=\"header\"></slot>`)}`;\n },\n});\n```\n\n`useEmit<Emits>()` and `useSlots<SlotNames>()` are factory hooks: call them once per setup run to get a typed\n`emit`/`slots` bound to the current host. `useSlots()` is safe to call more than once within that setup run.\n\n### ComponentDefinition\n\n```ts\ntype ComponentDefinition<Props> = {\n formAssociated?: boolean;\n props?: PropsDef<Props>;\n setup: (props: InferProps<PropsDef<Props>>) => HTMLResult | null;\n shadow?: Partial<ShadowRootInit> | false; // false = light DOM (no shadow root)\n styles?: (string | CSSStyleSheet | CSSResult)[];\n};\n```\n\n## Runtime Helpers\n\n`onMounted`, `onCleanup`, `onEvent`, `onElement`, and `watchEffect` are plain functions imported from `@vielzeug/ore`. Call them directly during `setup()`.\n\n```ts\nimport { html, onCleanup, onEvent, onMounted } from '@vielzeug/ore';\n\nsetup(props) {\n onMounted(() => {\n // DOM is ready; return a function for mount-scoped cleanup\n return () => { /* cleanup on unmount */ };\n });\n\n onCleanup(() => { /* called on disconnect */ });\n\n onEvent(window, 'keydown', (e) => { /* auto-removed on disconnect */ });\n\n return html`...`;\n}\n```\n\nBecause these resolve the active component through an implicit context (rather than a value threaded through parameters), composable helper functions can call them directly too: no need to pass hooks in as options:\n\n```ts\nimport { onCleanup } from '@vielzeug/ore';\n\nfunction useMyHelper() {\n onCleanup(() => { /* teardown */ });\n}\n\n// In setup:\nsetup(_props) {\n useMyHelper();\n return html`...`;\n}\n```\n\n## Props API\n\n| Helper | Signature | Notes |\n| ----------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------ |\n| `prop.string(defaultValue?)` | `PropDef<string>` | Reflects by default |\n| `prop.bool(defaultValue?)` | `PropDef<boolean>` | Any non-null attribute value other than `\"false\"` parses as `true`; `\"false\"` or absent attribute is `false` |\n| `prop.number(defaultValue?)` | `PropDef<number>` | Returns default (not NaN) and warns in dev when attribute is not a valid number |\n| `prop.oneOf(allowed, defaultValue?)` | `PropDef<T>` | Restricts to a fixed set of string or number members; `parse` falls back to `defaultValue` (or `undefined` when omitted) for anything outside the set. Numeric members match the attribute parsed as a number |\n| `prop.json(defaultValue)` | `PropDef<T>` | JSON.parse from attribute; `reflect: false` |\n| `prop.data<T>(defaultValue?)` | `PropDef<T>` | JS-only: never reads/writes an attribute; use for objects, arrays, callbacks, or any non-serialisable value |\n\n> **Choosing the right prop helper:**\n>\n> - **`prop.oneOf`**: the honest way to type a union-valued prop: the restriction is enforced at runtime (`parse` falls back for anything outside the set), not just in a type parameter a raw attribute value could violate. Pass an `as const` array of the allowed string or number members.\n> - **`prop.json`**: value can be declared in HTML (`<my-el config='{\"x\":1}'>`); attribute string is `JSON.parse`d.\n> - **`prop.data`**: value is always set from JavaScript (objects, arrays, callbacks, class instances); the attribute is never read. Use this for both data and function props.\n\nWhen you need custom parsing or `reflect: false`, use a raw `PropDef` object:\n\n```ts\nprops: {\n items: { default: [], parse: () => [], reflect: false },\n}\n```\n\nUse `prop.data` for props that hold JS-only values (including callbacks) that cannot be serialised through an HTML attribute:\n\n```ts\ndefine('data-grid', {\n props: {\n getRowKey: prop.data<(row: unknown) => string>(),\n columns: prop.data<DataGridColumn[]>([]),\n onSort: prop.data<(key: string) => void>(),\n },\n setup(props) {\n // Set from JS: grid.getRowKey = (row) => row.id\n return html`...`;\n },\n});\n```\n\n## Template and Directives\n\n### `html`\n\nTagged template literal that returns an `HTMLResult`. Supports text interpolation, ordinary attributes (`attr=`),\nboolean attributes (`?attr=`), explicit property bindings (`.prop=`), events (`@event=` with optional\n`.stop`/`.prevent`/`.once`/`.capture` modifiers), refs (`ref=`), and nested templates.\n\n`.prop=${value}` always writes the element property (never an attribute): the explicit channel for structured\nvalues (arrays, objects, callbacks). Prototype-polluting names (`__proto__`, `constructor`, `prototype`) and\nraw-HTML-parsing names (`innerHTML`, `outerHTML`, `srcdoc`) are rejected at compile time: render sanitized\nHTML through the `unsafeHtml()` directive instead of a property binding.\n\n`@event.stop`, `.prevent`, `.once`, and `.capture` are shorthand for the matching listener behavior\n(`stopPropagation`/`preventDefault` applied before the handler runs; `once`/`capture` passed as listener\noptions). An unknown modifier throws at compile time.\n\n### `css`\n\nTagged template literal that returns a `CSSResult` for use in `styles`.\n\n### Directives\n\n| Directive | Purpose |\n| -------------------------------------- | ----------------------------------------------------------------------------------------------------- |\n| `each(source, key, render, fallback?)` | Keyed reactive list; render receives `Readable<T>` and `Readable<number>`; plain `T[]` is a one-time static snapshot |\n| `when(condition, truthy, falsy?)` | Conditional rendering |\n| `classMap(record)` | Reactive class string from object map |\n| `styleMap(record)` | Reactive inline style string from object map |\n| `live(signal)` | One-way binding that skips stale writes during active user input; use with `@input` handler |\n| `model(signal, options?)` | Two-way form-control binding: writes the signal to `value`/`checked` and writes user input back on the control's change event |\n| `unsafeHtml(value)` | HTML rendering sink; sanitize untrusted values before calling |\n\n`each()` treats number and string keys as distinct. A reconciliation failure (a duplicate key, or a render function that throws) reports `ore:error` and preserves the last valid list: entries created during the failed pass are rolled back, so the previous list stays exactly on screen.\n\n### `model(signal, options?)`\n\nTwo-way binding for a form control: `value=${model(signal)}`. It writes the signal to the control's `value`/`checked` property on every change and writes user input back into the signal on the control's change event (`'change'` for checkboxes, radios, and `<select>`; `'input'` otherwise). This is the complete controlled-input loop that the `live()` + manual `@input` handler pair used to require.\n\n```ts\nconst text = signal('');\nconst agree = signal(false);\nconst count = signal(0);\n\nhtml`\n <input value=${model(text)} />\n <input type=\"checkbox\" checked=${model(agree)} />\n <input type=\"number\" value=${model(count, { get: (el) => Number(el.value) })} />\n`;\n```\n\n`options` overrides the defaults for non-native controls: `event` sets the write-back event name and `get` reads the current value out of the element (default `el.checked` for checkboxes/radios, `el.value` otherwise). Because a native text input's `value` is always a string, pass a `get` like `(el) => Number(el.value)` to keep a numeric signal numeric. `model()` can only drive a plain `value`/`checked` attribute binding; using it on a boolean (`?checked=`) or other attribute throws.\n\n### `unsafeHtml`\n\n`unsafeHtml()` is an explicit HTML injection sink. It has no global sanitizer: sanitize untrusted\ncontent before passing it to the directive, so the trust boundary remains at the call site.\n\n```ts\nimport { unsafeHtml } from '@vielzeug/ore';\n\nconst safeArticle = sanitize(userSuppliedArticle);\n\nreturn html`<article>${unsafeHtml(safeArticle)}</article>`;\n```\n\n## Host Bindings\n\n`bind(config, options?)` is a plain function imported from `@vielzeug/ore`:\n\n```ts\nbind({\n attr: { role: 'button', 'aria-expanded': () => String(open.value) },\n class: { 'is-open': open },\n style: { '--height': () => height.value + 'px' },\n on: { click: handleClick },\n});\n```\n\n`bind()` auto-registers cleanup with the component scope: no manual `onCleanup` needed. Returns a cleanup function for early teardown.\n\n### Off-host bindings\n\nPass `{ target: el }` as a second argument to bind to any element other than the host:\n\n```ts\nbind(\n { attr: { 'aria-expanded': () => String(isOpen.value) } },\n { target: triggerEl },\n);\n```\n\nEvent listener options (`once`, `capture`, `passive`) are also accepted in the second argument. Cleanup is auto-registered with the component scope when called during setup.\n\n### Reactive ARIA attributes\n\nUse explicit `aria-*` keys in `bind({ attr }, { target })`:\n\n```ts\nconst stopAria = bind(\n {\n attr: {\n 'aria-controls': panelId,\n 'aria-expanded': () => isOpen.value,\n 'aria-haspopup': 'listbox',\n },\n },\n { target: triggerEl },\n);\n```\n\nStatic values are applied once. Getters and signals create reactive effects. Calling `stopAria()` stops updates. Values of `null`, `undefined`, or `false` remove the attribute.\n\n## Slots\n\n- `slots.has(name?)`: `Readable<boolean>`: whether the named (or default) slot has assigned content\n- `slots.elements(name?)`: `Readable<Element[]>`: the assigned elements for the slot\n\nSlot signals update reactively when assigned content changes, including when slots are inserted dynamically (via `when()` or `each()`) after mount.\n\n## Context API\n\n- `createContext<T>(description)`: Create a unique typed injection key; the description is diagnostic only\n- `provide(key, value)`: Provide a value to descendants\n- `inject(key)`: Resolve from nearest ancestor; returns `undefined` if not found\n- `inject(key, fallback)`: Resolve with a fallback value\n- `injectStrict(key)`: Resolve or throw if absent\n\n`provide()` and `inject()` must be called synchronously during `setup()`. Calling them outside a setup context throws\n`'Lifecycle hooks must be called during component setup'`. Context resolution walks the ancestor chain including shadow\nDOM boundaries. `inject()` resolves and caches its result once per consumer: provide a `Readable` (signal/computed)\nrather than a raw value if descendants need to observe later changes; re-calling `provide()` with a new raw value\nafterward is not seen by consumers that already resolved it (a dev-mode warning fires when a key is provided twice on\nthe same element). `provide()` registers cleanup automatically: context keys are removed from the registry when the\nproviding component disconnects, so reconnecting the same element runs `setup()` fresh without spurious \"overwriting\"\nwarnings or stale keys leaking to descendants.\n\n## Utilities\n\n- `ref<T>()`: Create a `Signal<T | null>` element reference. Set to the element via `ref=` in templates.\n- `createId(prefix = 'id')`: Generate a collision-resistant ID with a semantic prefix, a per-runtime tag, and a monotonic counter (for example, `'field-a3k21'`).\n\n## Form-Associated API\n\nImport from `@vielzeug/ore`.\n\n### `useField(options)`\n\nWire a form-associated element to `ElementInternals`. Requires `formAssociated: true` on the component definition. The `disabled` state tracking via `internals.states` (CustomStateSet) is skipped with a dev warning if the API is unavailable in the current environment.\n\n```ts\ntype FormFieldValidity = { flags: ValidityStateFlags; message: string };\n\ntype FormFieldOptions<T> = {\n disabled?: Readable<boolean>;\n /**\n * When true, a null/undefined value is submitted as '' instead of null,\n * keeping the field's key present in FormData even when the value is absent.\n * Only applies to the default toFormValue; ignored if toFormValue is provided.\n * @default false\n */\n emptyStringForNull?: boolean;\n /** Called when the ancestor <form> resets (see onFormReset): restore local field state here. */\n onReset?: () => void;\n toFormValue?: (value: T) => File | FormData | string | null;\n /**\n * Recomputed reactively and passed straight to internals.setValidity().\n * null (or omitted) = always valid. Flags and message travel together because\n * the platform's setValidity() contract couples them (flags without a\n * non-empty message throw); a single { flags, message } | null input makes a\n * desynced pair unrepresentable.\n */\n validity?: Readable<FormFieldValidity | null>;\n value: Signal<T> | Readable<T>;\n};\n\ntype FormFieldHandle = {\n checkValidity(): boolean;\n readonly internals: ElementInternals;\n reportValidity(): boolean;\n /** Set (non-empty message) or clear (empty string) a custom validity error. */\n setCustomValidity(message: string): void;\n};\n```\n\nPass a single `validity` signal to make `required`-style constraints participate in native constraint validation\nthrough `checkValidity()` and `reportValidity()`. The `{ flags, message }` pair is the one validity input: the old\nsplit `validity` + `validationMessage` options are gone, since they could drift apart into a state the platform\nrejects.\n\n```ts\nconst isBlank = (v: string) => v.trim() === '';\n\nuseField({\n validity: computed(() =>\n required.value && isBlank(value.value)\n ? { flags: { valueMissing: true }, message: 'This field is required.' }\n : null,\n ),\n value,\n});\n```\n\n## Testing APIs\n\nImport from `@vielzeug/ore/testing`.\n\n| API | Purpose |\n| ------------------------ | ------------------------------------------------------------------------------------------ |\n| `mount(setup, options?)` | Mount a component and return a test fixture |\n| `cleanup()` | Remove all mounted elements and reset test state |\n| `install(afterEach)` | Register auto-cleanup after each test |\n| `flush(options?)` | Drain reactive updates and animation frames |\n| `renderHook(setup)` | Run lifecycle hooks in isolation; overload accepts `propDefs` as first arg for typed props |\n| `OreTimeoutError` | Error thrown when `flush()` cannot settle tracked Ore work |\n\n> **Test isolation:** `cleanup()` removes mounted elements and resets Ore's cross-test caches and counters. Call it in `afterEach`, or use `install(afterEach)`.\n\nImport `within`, named dispatchers such as `fireClick`, and waits such as `waitUntil` or `waitForEvent` from\n`@vielzeug/assay`.\n\n> **Form-associated component testing:** jsdom implements none of the `ElementInternals` form-association API.\n> `@vielzeug/assay` exports `installFormInternalsPolyfill()` for this: it polyfills\n> `setFormValue`/`setValidity`/`checkValidity`/`reportValidity`/`validationMessage`/`validity`/`states`, mixes\n> `checkValidity`/`reportValidity`/`validity`/`validationMessage` onto the host element itself (real browsers do\n> this for any `formAssociated: true` element), makes `FormData` collect a form-associated element's set value, and\n> makes `<form>.reset()` invoke `formResetCallback()`. Every patch is a guarded no-op when its target already\n> exists, and the function returns an `uninstall()` that restores every patched global. Call it directly from a\n> test setup (alongside `install(afterEach)`) only when the suite tests form-associated components: the patches\n> are global, so suites without them shouldn't carry them. A downstream package (e.g. a component library built\n> on `ore`) should rely on it instead of hand-rolling its own copy.\n\n#### `Fixture` interface\n\n```ts\ninterface Fixture<T extends HTMLElement = HTMLElement> {\n [Symbol.dispose](): void; // Delegates to dispose(): enables `using` declarations\n element: T;\n readonly disposed: boolean; // true after dispose() has been called\n readonly shadow: ShadowRoot | null;\n get<E extends Element>(selector: string): E;\n query<E extends Element>(selector: string): E | null;\n queryAll<E extends Element>(selector: string): E[];\n getByText<E extends Element>(text: string, selector?: string): E;\n queryByText<E extends Element>(text: string, selector?: string): E | null;\n queryAllByText<E extends Element>(text: string, selector?: string): E[];\n getByTestId<E extends Element>(testId: string): E;\n queryByTestId<E extends Element>(testId: string): E | null;\n queryAllByTestId<E extends Element>(testId: string): E[];\n attr(name: string, value: string | number | boolean): Promise<void>;\n attrs(record: Record<string, string | number | boolean>): Promise<void>;\n flush(options?: FlushOptions): Promise<void>;\n act(fn: () => unknown): Promise<void>;\n dispose(): void; // Removes the component from the DOM: idempotent\n}\n```\n\n#### `renderHook`\n\nUseful for testing composable lifecycle hooks (`onMounted`, `watchEffect`, `inject`, etc.) without a template. `onMounted`/`onCleanup`/`watchEffect`/... work exactly as inside a real `setup()`, since they resolve the same implicit current-component context:\n\n```ts\n// Without props\nconst { result, flush, dispose } = await renderHook(() => {\n const count = signal(0);\n onMounted(() => {\n count.value = 1;\n });\n return count;\n});\nexpect(result.value).toBe(1);\n\n// With typed props (prop-defs overload)\nconst { result } = await renderHook({ label: prop.string('hello'), count: prop.number(0) }, (props) => props.label);\nexpect(result.value).toBe('hello');\n```\n\n## Ripple Primitives\n\nOre does **not** re-export reactive primitives. Import them directly from `@vielzeug/ripple`:\n\n```ts\nimport { batch, computed, signal, watch } from '@vielzeug/ripple';\n```\n\nSee the [Ripple documentation](/ripple/) for the full API.\n\n## Lifecycle Events\n\n| Event | When |\n| ------------- | ------------------------------------------------------------------------- |\n| `ore:error` | When a lifecycle callback fails: bubbles, composed; detail is `OreLifecycleError` |\n\n## Types\n\n```ts\ntype PropDef<T> = {\n readonly default: T;\n readonly parse: (value: string | null) => T;\n reflect?: boolean;\n};\n\ntype PropsDef<T extends Record<string, unknown>> = {\n [K in keyof Required<T>]: PropDef<T[K & keyof T]>;\n};\n\ntype PropInputDefs = Record<string, PropDef<unknown>>;\n\n/**\n * Infer reactive props type from a PropInputDefs map.\n * Each entry becomes Readable<T> keyed by prop name.\n */\ntype InferProps<D extends PropInputDefs> = {\n readonly [K in keyof D]-?: Readable<InferPropValue<D[K]>>;\n};\n\n// Runtime hooks: all plain functions imported from '@vielzeug/ore', not fields on an object.\ntype OnMountedCallback = () => Cleanup | undefined;\ntype OnFormResetCallback = () => void;\n\ndeclare function onMounted(fn: OnMountedCallback): void; // DOM-ready callback; runs after each connection's render\ndeclare function onCleanup(fn: Cleanup): void; // Register teardown; called on disconnect\ndeclare function onElement<T extends HTMLElement>(\n ref: Readable<T | null>,\n callback: (el: T) => Cleanup | undefined,\n): () => void;\ndeclare function onEvent<K extends keyof HTMLElementEventMap>(\n target: EventTarget | null | undefined,\n event: K,\n listener: (e: HTMLElementEventMap[K]) => void,\n options?: AddEventListenerOptions,\n): void;\ndeclare function onEvent(\n target: EventTarget | null | undefined,\n event: string,\n listener: EventListener,\n options?: AddEventListenerOptions,\n): void;\ndeclare function onFormReset(fn: OnFormResetCallback): void; // Runs on every ancestor <form> reset; formAssociated only\ndeclare function watchEffect(fn: () => Cleanup | undefined): () => void; // Scoped reactive effect; auto-cleaned on disconnect\ndeclare function bind(config: HostBindConfig, options?: BindOptions): () => void; // Bindings for host or any target element\ndeclare function provide<T>(key: InjectionKey<T>, value: T): void; // Register a context value on the host element\ndeclare function inject<T>(key: InjectionKey<T>): T | undefined;\ndeclare function inject<T>(key: InjectionKey<T>, fallback: T): T;\ndeclare function getHost(): HTMLElement; // The current component's host element\ndeclare function useEmit<Emits extends Record<string, unknown> = Record<string, never>>(): EmitFn<Emits>;\ndeclare function useSlots<SlotNames extends string = string>(): ComponentSlots<SlotNames>;\n\ntype ComponentDefinition<Props extends Record<string, unknown> = Record<never, never>> = {\n formAssociated?: boolean;\n props?: PropsDef<Props>;\n setup: (props: InferProps<PropsDef<Props>>) => HTMLResult | null;\n shadow?: Partial<ShadowRootInit> | false; // false = light DOM\n styles?: (string | CSSStyleSheet | CSSResult)[];\n};\n\ntype MaybeReactive<T> = T | Readable<T> | (() => T); // Plain value, signal, or getter: accepted by when(), each(), classMap(), and bind()\n\ntype HostBindingValue = MaybeReactive<string | number | boolean | null | undefined>;\n\ntype AttributeBindings = Record<string, HostBindingValue>;\n\ntype HostBindConfig = {\n attr?: AttributeBindings;\n class?: (() => Record<string, boolean>) | Record<string, Readable<boolean> | (() => boolean) | boolean>;\n on?: Record<string, ((event: Event) => void) | undefined>;\n style?: Record<string, HostBindingValue>;\n};\n\ntype BindOptions = AddEventListenerOptions & {\n target?: Element;\n};\n\ntype ComponentSlots<S extends string = string> = {\n elements(name?: S): Readable<Element[]>;\n has(name?: S): Readable<boolean>;\n};\n\ntype Ref<T extends Element> = Signal<T | null>;\n\ntype RefCallback<T extends Element> = (el: T | null) => void;\n\ntype InjectionKey<T> = symbol & { readonly __ore_injection_key?: T };\n\ninterface HTMLResult {\n mount(\n parent: ParentNode,\n anchor: Node | null,\n registerCleanup: (fn: () => void) => void,\n ): Node[];\n}\n\ntype CSSResult = {\n content: string;\n toString(): string;\n};\n\ntype LiveBinding<T> = { readonly source: Readable<T> };\n\ntype ModelOptions<T = unknown> = {\n /** Write-back event (default: 'change' for checkbox/radio/select, 'input' otherwise). */\n event?: string;\n /** Value reader (default: el.checked for checkbox/radio, el.value otherwise). */\n get?: (el: HTMLElement) => T;\n};\n\ntype EmitFn<T extends Record<string, unknown>> = {\n <K extends KeysWithoutDetail<T>>(event: K): boolean;\n <K extends Exclude<keyof T, KeysWithoutDetail<T>>>(event: K, detail: T[K]): boolean;\n};\n// KeysWithoutDetail is an internal helper type, not exported.\n\ntype FormFieldValidity = { flags: ValidityStateFlags; message: string };\n\ntype FormFieldOptions<T = unknown> = {\n disabled?: Readable<boolean>;\n emptyStringForNull?: boolean;\n onReset?: () => void;\n toFormValue?: (value: T) => File | FormData | string | null;\n validity?: Readable<FormFieldValidity | null>;\n value: Signal<T> | Readable<T>;\n};\n\ntype FormFieldHandle = {\n checkValidity: () => boolean;\n readonly internals: ElementInternals;\n reportValidity: () => boolean;\n setCustomValidity: (message: string) => void;\n};\n\n/** Phase in which a OreError occurred. */\ntype OreErrorPhase = 'each-reconcile' | 'form-reset' | 'mounted' | 'setup';\n```\n\n## Errors\n\n`OreError` is the base class for every Ore error class: `err instanceof OreError` catches all of them.\n\n- **`OreApiError`**: thrown when the `ore` API itself is misused: calling `define()` with a duplicate tag, calling a lifecycle hook (`inject`, `onMounted`, `onCleanup`, `onEvent`, …) outside of `setup()`, or passing an invalid prop definition to `define()`.\n- **`OreInternalError`**: thrown when an Ore invariant fails, indicating a package bug rather than invalid application code.\n- **`OreLifecycleError`**: reported in the `ore:error` event when component `setup()`, a mounted callback, a form-reset callback, or `each()` reconciliation fails. Extends `OreError` with:\n - `component: string`: the element's local name\n - `phase: OreErrorPhase`: `'setup'` | `'mounted'` | `'form-reset'` | `'each-reconcile'`\n - `cause: Error`: the original error thrown by `setup()`\n- **`OreTimeoutError`**: thrown by `flush()` (from `@vielzeug/ore/testing`) when pending Ore work does not settle before its timeout.\n\nLifecycle failures dispatch a bubbling, composed `ore:error` event whose `detail` is the `OreLifecycleError`. Setup\nfailures still rethrow their original error; mounted and form-reset callback failures are reported through the same\nevent so their remaining callbacks can continue.\n",
|
|
6
6
|
"usage": "---\ntitle: 'Ore: Usage Guide'\ndescription: Practical Ore usage patterns for components, props, templates, slots, context, forms, Sentinel integration, and tests.\n---\n\n[[toc]]\n\n## Basic Usage\n\n`define(tag, definition)` creates and registers a custom element. Call it from the module or browser bootstrap responsible for registration.\n\nYour `setup()` function receives typed prop signals and returns an `HTMLResult` directly. Its state belongs to the\ncurrent connection: disconnect disposes it, and reconnecting the same element runs setup again.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('status-chip', {\n setup() {\n const online = signal(true);\n\n return html`\n <button @click=${() => (online.value = !online.value)}>${() => (online.value ? 'Online' : 'Offline')}</button>\n `;\n },\n});\n```\n\nEverything besides `props`: lifecycle hooks, host bindings, context, slots, emit: is a plain function imported from `@vielzeug/ore`, called directly from `setup()` (or a composable it calls):\n\n```ts\nimport { define, getHost, html, bind, useEmit, useSlots } from '@vielzeug/ore';\n\ndefine('my-widget', {\n setup(_props) {\n const el = getHost(); // the host HTMLElement\n const emit = useEmit<{ close: undefined }>(); // typed event emitter\n const slots = useSlots<'header'>(); // reactive slot observation\n\n bind({ attr: { role: 'group' } }); // host binding helper (attr, class, style, on)\n\n return html`<slot></slot>`;\n },\n});\n```\n\n## signals and effects\n\nOre does not re-export ripple primitives: import them directly from `@vielzeug/ripple`.\n\n```ts\nimport { batch, computed, effect, signal, watch } from '@vielzeug/ripple';\n\nconst count = signal(0);\nconst doubled = computed(() => count.value * 2);\n\neffect(() => {\n console.log('doubled =', doubled.value);\n});\n\nwatch(count, (next, prev) => {\n console.log('count changed', prev, '->', next);\n});\n\nbatch(() => {\n count.value = 1;\n count.value = 2;\n});\n```\n\n## onMounted and lifecycle\n\nUse `onMounted()` for DOM-dependent initialization that must run after the template is mounted. Use `onElement(ref, cb)` for work tied to a specific DOM node. `onEvent()` attaches a listener that is automatically removed on disconnect.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, onElement, onEvent, onMounted, ref, useSlots } from '@vielzeug/ore';\n\ndefine('deferred-init', {\n setup(_props) {\n const tabIndex = signal(0);\n const inputRef = ref<HTMLInputElement>();\n const slots = useSlots<'items'>();\n\n onMounted(() => {\n const items = slots.elements('items').value;\n console.log('Found', items.length, 'items');\n });\n\n onElement(inputRef, (input) => {\n input.focus();\n });\n\n onEvent(window, 'keydown', (e: KeyboardEvent) => {\n if (e.key === 'Escape') tabIndex.value = 0;\n });\n\n return html`<div><slot name=\"items\"></slot><input ref=${inputRef} /></div>`;\n },\n});\n```\n\n## prop definitions\n\nUse `prop.*` helpers for common cases, or raw `PropDef` objects for custom parsing or `reflect: false`.\n\n```ts\nimport { define, html, prop } from '@vielzeug/ore';\n\ndefine('x-button', {\n props: {\n label: prop.string('Button'),\n disabled: prop.bool(false),\n variant: prop.oneOf(['primary', 'secondary'] as const, 'primary'),\n count: prop.number(0),\n },\n setup(props) {\n return html`\n <button ?disabled=${props.disabled} data-variant=${props.variant}>${props.label} (${props.count})</button>\n `;\n },\n});\n```\n\n## template bindings\n\n`html` supports text, attributes, booleans, properties, events, refs, and nested templates. Event bindings accept\n`.stop`, `.prevent`, `.once`, and `.capture`: the first two apply before the handler runs, while the latter two use\nnative listener options. Unknown modifiers throw while Ore compiles the template.\n\n```ts\nimport { computed, signal } from '@vielzeug/ripple';\nimport { define, html, model, ref } from '@vielzeug/ore';\n\ndefine('profile-name', {\n setup() {\n const name = signal('Alice');\n const inputRef = ref<HTMLInputElement>();\n\n return html`\n <label title=${computed(() => 'Current: ' + name.value)}>Name</label>\n <input\n ref=${inputRef}\n value=${model(name)}\n aria-label=${() => 'Current name ' + name.value} />\n <p>Hello ${name}</p>\n `;\n },\n});\n```\n\n## directives\n\nOre exports `each`, `classMap`, `styleMap`, `when`, `live`, `model`, and `unsafeHtml` from `@vielzeug/ore`.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { classMap, define, each, html, styleMap, when } from '@vielzeug/ore';\n\ndefine('task-list', {\n setup() {\n const tasks = signal([{ id: 1, text: 'Write tests' }]);\n const active = signal(true);\n\n return html`\n <ul\n class=\"${classMap({ ready: () => tasks.value.length > 0 })}\"\n style=${styleMap({ opacity: () => (active.value ? 1 : 0.5) })}>\n ${when(\n () => active.value,\n () => html`<li>Active</li>`,\n () => html`<li>Paused</li>`,\n )}\n ${each(\n tasks,\n (task) => task.id,\n (task) => html`<li>${() => task.value.text}</li>`,\n )}\n </ul>\n `;\n },\n});\n```\n\n### each() API\n\n`each(source, key, render, fallback?)` takes positional arguments:\n\n- **source**: signal, getter, or plain array\n- **key**: function returning a unique string or number per item; number and string keys remain distinct\n- **render**: receives reactive `item` and `index` signals\n- **fallback**: optional, rendered when the list is empty\n\n```ts\neach(\n items,\n (item) => item.id,\n (item, index) => html`<li>#${index}: ${() => item.value.label}</li>`,\n () => html`<li>No items</li>`,\n);\n```\n\nEach reactive reconciliation is transactional. Ore creates missing entries before committing updates or removing stale\nentries; if a render callback throws, entries created during that pass are rolled back and the last successful list\nremains on screen. Ore reports the failure through `ore:error` with phase `each-reconcile`.\n\n## model: two-way form bindings\n\nUse `model(signal)` for a controlled input: it writes the signal to the control's `value`/`checked` property and\nwrites user input back on the control's change event (`'change'` for checkboxes, radios, and selects; `'input'`\notherwise). This replaces the `live()` + manual `@input` handler pair with one binding.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, model } from '@vielzeug/ore';\n\ndefine('live-search', {\n setup() {\n const query = signal('');\n const agree = signal(false);\n\n return html`\n <input value=${model(query)} />\n <input type=\"checkbox\" checked=${model(agree)} />\n `;\n },\n});\n```\n\nA native text input's `value` is always a string, so pass a `get` reader to keep a numeric signal numeric, and an\n`event` name for custom controls that emit their own change event:\n\n```ts\nconst count = signal(0);\n\nhtml`\n <input type=\"number\" value=${model(count, { get: (el) => Number(el.value) })} />\n <my-picker value=${model(pick, { event: 'pick-change', get: (el) => (el as MyPicker).selection })} />\n`;\n```\n\n## live: one-way bindings that preserve user edits\n\nUse `live(signal)` when a binding must skip a stale app-state write while the user is mid-edit, without writing\nuser input back. `model()` covers the common controlled-input case; `live()` is for one-way bindings that only\nneed the stale-write guard.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, live } from '@vielzeug/ore';\n\ndefine('live-search', {\n setup() {\n const query = signal('');\n\n return html`\n <input value=${live(query)} @input=${(e: Event) => (query.value = (e.target as HTMLInputElement).value)} />\n `;\n },\n});\n```\n\n## host bindings\n\n`bind()` wires reactive attrs, classes, styles, and events to the host element.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { bind, define, html } from '@vielzeug/ore';\n\ndefine('x-toggle', {\n setup(_props) {\n const open = signal(false);\n\n bind({\n attr: { 'aria-expanded': () => String(open.value), role: 'button', tabindex: 0 },\n class: { 'is-open': open },\n on: { click: () => (open.value = !open.value) },\n });\n\n return html`<slot></slot>`;\n },\n});\n```\n\nThe `bind` config supports `attr`, `class`, `style`, and `on` sections.\n\n## ARIA bindings\n\nUse explicit `aria-*` keys in `bind({ attr: config }, { target })` to reactively sync ARIA attributes to any element.\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { bind, define, html, onMounted } from '@vielzeug/ore';\n\ndefine('x-disclosure', {\n setup(_props) {\n const open = signal(false);\n const panelId = 'disclosure-panel';\n\n bind({\n attr: { role: 'button', tabindex: 0 },\n on: { click: () => (open.value = !open.value) },\n });\n\n onMounted(() => {\n const trigger = document.querySelector('#trigger') as HTMLElement;\n if (trigger) {\n // bind() registers cleanup automatically when called inside setup\n bind(\n {\n attr: {\n 'aria-controls': panelId,\n 'aria-expanded': () => String(open.value),\n 'aria-haspopup': 'region',\n },\n },\n { target: trigger },\n );\n }\n });\n\n return html`<slot></slot>`;\n },\n});\n```\n\nStatic values are applied once. Getter functions create reactive effects. Setting a value to `null`, `undefined`, or `false` removes the attribute.\n\n`bind()` always returns a cleanup function. Use it to stop syncing early when a trigger element can be swapped out:\n\n```ts\nonMounted(() => {\n const trigger = document.querySelector('#trigger') as HTMLElement;\n const stopAria = bind({ attr: { 'aria-expanded': () => String(open.value) } }, { target: trigger });\n\n // Stop syncing when the trigger is replaced\n onCleanup(stopAria);\n});\n```\n\n### Binding a non-host element with `bind()`\n\nPass `{ target: el }` as a second argument to bind attributes, classes, styles, or events to any element:\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { bind, define, html, onMounted, ref } from '@vielzeug/ore';\n\ndefine('button-wrapper', {\n setup(_props) {\n const visible = signal(false);\n const btnRef = ref<HTMLButtonElement>();\n\n onMounted(() => {\n const btn = btnRef.value;\n if (!btn) return;\n\n bind(\n {\n attr: { 'aria-pressed': () => String(visible.value) },\n on: { click: () => (visible.value = !visible.value) },\n },\n { target: btn },\n );\n });\n\n return html`<button ref=${btnRef}>Toggle</button>`;\n },\n});\n```\n\n## slots and emits\n\n```ts\nimport { define, html, useEmit, useSlots, when } from '@vielzeug/ore';\n\ndefine('card-with-footer', {\n setup(_props) {\n const slots = useSlots<'header' | 'footer'>();\n const emit = useEmit<{ action: undefined }>();\n\n return html`\n <div class=\"card\">\n <slot name=\"header\"></slot>\n <slot></slot>\n ${when(slots.has('footer'), () => html`<footer><slot name=\"footer\"></slot></footer>`)}\n </div>\n <button @click=${() => emit('action')}>Go</button>\n `;\n },\n});\n```\n\nPass a `SlotNames` type parameter to `useSlots<SlotNames>()` to get typed `slots.has()` and `slots.elements()` calls.\n\n## context provide/inject\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { createContext, define, html, injectStrict, provide } from '@vielzeug/ore';\n\nconst COUNT_CTX = createContext<ReturnType<typeof signal<number>>>('count');\n\ndefine('count-provider', {\n setup(_props) {\n const count = signal(0);\n provide(COUNT_CTX, count);\n\n return html`<button @click=${() => count.value++}><slot></slot></button>`;\n },\n});\n\ndefine('count-consumer', {\n setup() {\n const count = injectStrict(COUNT_CTX);\n\n return html`<p>Count: ${count}</p>`;\n },\n});\n```\n\n`provide()` registers cleanup automatically: context keys are removed from the registry when the providing component disconnects. On reconnect, `setup()` runs fresh and `provide()` re-registers without spurious \"overwriting\" warnings. Provide a `Readable` (signal/computed) rather than a raw value if descendants need to observe later changes: `inject()` resolves and caches the value once per consumer connection.\n\n## form-associated elements\n\n```ts\nimport { signal } from '@vielzeug/ripple';\nimport { define, html, prop } from '@vielzeug/ore';\nimport { useField } from '@vielzeug/ore';\n\ndefine('rating-input', {\n formAssociated: true,\n setup() {\n const value = signal(0);\n const field = useField({ value });\n\n return html`\n <button @click=${() => (value.value = 1)}>1</button>\n <button @click=${() => (value.value = 2)}>2</button>\n <button @click=${() => (value.value = 3)}>3</button>\n <button @click=${() => field.reportValidity()}>Validate</button>\n <p>Current: ${value}</p>\n `;\n },\n});\n```\n\n## Sentinel Observers\n\nUse `@vielzeug/sentinel` for subscribable browser and DOM observations. Create element-dependent Sentinels inside `onMounted()` and dispose them with the component.\n\n```ts\nimport { define, html, onCleanup, onMounted, ref, watchEffect } from '@vielzeug/ore';\nimport { fromSubscribable } from '@vielzeug/ripple';\nimport { createElementSize, SentinelUnavailableError } from '@vielzeug/sentinel';\n\ndefine('x-observed', {\n setup(_props) {\n const boxRef = ref<HTMLDivElement>();\n\n onMounted(() => {\n const element = boxRef.value;\n if (!element) return;\n\n try {\n const size = createElementSize(element);\n const sizeState = fromSubscribable(size, { signal: size.disposalSignal });\n\n watchEffect(() => {\n console.log(sizeState.value?.width);\n });\n\n onCleanup(() => size.dispose());\n } catch (error) {\n if (!(error instanceof SentinelUnavailableError)) throw error;\n }\n });\n\n return html`<div ref=${boxRef}>Observe me</div>`;\n },\n});\n```\n\n## testing utilities\n\nImport from `@vielzeug/ore/testing`.\n\n```ts\nimport { afterEach, describe, expect, it } from 'vitest';\nimport { signal } from '@vielzeug/ripple';\nimport { fireClick } from '@vielzeug/assay';\nimport { html } from '@vielzeug/ore';\nimport { cleanup, mount } from '@vielzeug/ore/testing';\n\ndescribe('my-counter', () => {\n afterEach(cleanup);\n\n it('increments on click', async () => {\n let count!: ReturnType<typeof signal<number>>;\n const { query, act } = await mount(() => {\n count = signal(0);\n return html`<button @click=${() => count.value++}>${count}</button>`;\n });\n\n expect(query('button')?.textContent).toBe('0');\n\n await act(() => fireClick(query('button')!));\n\n expect(query('button')?.textContent).toBe('1');\n });\n});\n```\n\n## Framework Integration\n\nOre components are standard custom elements and work natively in any framework.\n\n::: code-group\n\n```tsx [React]\n// React 19+ supports custom elements natively.\nimport './x-toggle'; // wherever define('x-toggle', { ... }) is called\n\nfunction App() {\n return <x-toggle aria-label=\"Open menu\" />;\n}\n```\n\n```ts [Vue 3]\n<script setup lang=\"ts\">\nimport './x-toggle'; // wherever define('x-toggle', { ... }) is called\nimport { ref } from 'vue';\n\nconst open = ref(false);\n</script>\n\n<template>\n <x-toggle :aria-label=\"'Open menu'\" @click=\"open = !open\" />\n</template>\n```\n\n```svelte [Svelte]\n<script>\n import './x-toggle'; // wherever define('x-toggle', { ... }) is called\n\n function handleClick() {\n console.log('toggled');\n }\n</script>\n\n<x-toggle aria-label=\"Open menu\" on:click={handleClick} />\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\n### With Ripple\n\nImport ripple primitives directly from `@vielzeug/ripple` for standalone reactive state outside components.\n\n```ts\nimport { signal, computed } from '@vielzeug/ripple';\nimport { define, html } from '@vielzeug/ore';\n\n// Shared state created outside any component\nconst theme = signal<'light' | 'dark'>('light');\nconst isDark = computed(() => theme.value === 'dark');\n\ndefine('theme-toggle', {\n setup() {\n return html`\n <button @click=${() => (theme.value = isDark.value ? 'light' : 'dark')}>\n ${() =>\n isDark.value ? '<ore-icon name=\"sun\" size=\"16\"></ore-icon>' : '<ore-icon name=\"moon\" size=\"16\"></ore-icon>'}\n </button>\n `;\n },\n});\n```\n\n### With Forge\n\nUse `@vielzeug/forge` for typed form state. `useField()` remains intentionally narrow: it connects a form-associated\ncustom element to native `ElementInternals` without imposing submission, validation, or dirty-state policy.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\nimport { define, html } from '@vielzeug/ore';\n\ndefine('signup-form', {\n setup(_props) {\n const form = createForm({ initialValues: { email: '' } });\n\n return html`\n <form\n @submit=${(event: SubmitEvent) => {\n event.preventDefault();\n void form.submit(async (values) => {\n console.log(values);\n });\n }}>\n <slot></slot>\n </form>\n `;\n },\n});\n```\n\n## Best Practices\n\n- Setup returns `html\\`...\\`` directly: not a function wrapping the template.\n- Use `watchEffect()` for reactive subscriptions tied to component lifetime: it auto-registers cleanup on disconnect.\n- Use `onElement(ref, cb)` instead of `onMounted` when the work is tied to a single DOM node.\n- Bind host attributes and classes via `bind()` rather than mutating the element directly.\n- Provide context at the nearest ancestor: avoid global context singletons.\n- Call `onCleanup()` for every resource allocated in `setup()` (WebSockets, intervals, external subscriptions).\n- Use `model(signal)` for two-way form inputs; use `live(signal)` for a one-way binding that must not clobber user-in-progress edits.\n- Extract composable helper functions freely: `onMounted`/`onCleanup`/`bind`/... resolve the active component through implicit context, so they work from any function called (transitively) during `setup()`, with no need to pass them in as parameters.\n- Test component mounting and lifecycle with `@vielzeug/ore/testing`; import generic DOM events, queries, and waits\n from `@vielzeug/assay`.\n",
|
|
7
7
|
"examples": "---\ntitle: 'Ore: Examples'\ndescription: Practical examples and recipes for ore.\n---\n\n## Examples\n\n- [Counter Component](./examples/counter-component.md)\n- [Typed Props And Emits](./examples/typed-props-and-emits.md)\n- [Observers In onMounted()](./examples/observers-in-onmount.md)\n- [Search List With Directives](./examples/search-list-with-directives.md)\n- [Context Provider And Consumer](./examples/context-provider-and-consumer.md)\n- [Prop Helpers And Raw PropDef](./examples/propsof-builder-api.md)\n- [Form Associated Rating Input](./examples/form-associated-rating-input.md)\n- [Test Example With @vielzeug/ore/testing](./examples/test-example-at-vielzeug-ore-testing.md)\n"
|
|
8
8
|
},
|