View API (vanilla)
The vanilla API for building custom collaborative elements. For usage with side-by-side examples, see Custom elements; for the React equivalent, see the React API.
import { playhtml, html, svg, repeat, classMap, styleMap, nothing } from "playhtml";
playhtml.register(elementId, init)
Section titled “playhtml.register(elementId, init)”Binds an initializer to one element by id and returns a handle. Callable before or after playhtml.init() and before or after the element exists in the DOM — binding happens once both are present (like customElements.define).
playhtml.register<T, U, V>(
elementId: string,
init: ElementInitializer<T, U, V>,
): PlayElementHandle<T, U, V>;
- The element needs a stable, unique
id(it is theelementId). Thecan-playattribute is optional —registerimplies it. - Re-registering the same id replaces its initializer.
playhtml.define(capabilityName, init)
Section titled “playhtml.define(capabilityName, init)”Registers a reusable capability under an attribute name. Every element carrying [capabilityName] binds — including ones added later, or rendered by a view. The imperative counterpart of init({ extraCapabilities }).
playhtml.define<T, U, V>(capabilityName: string, init: ElementInitializer<T, U, V>): void;
Defining a name that collides with a built-in capability throws. Each bound element still needs a unique id.
playhtml.getHandle(elementId, capability?)
Section titled “playhtml.getHandle(elementId, capability?)”Returns a handle for any bound element. Because data is keyed by capability and id, pass the capability name when one element carries more than one.
playhtml.getHandle(elementId: string, capability?: string): PlayElementHandle;
The init object (ElementInitializer)
Section titled “The init object (ElementInitializer)”interface ElementInitializer<T, U, V> {
defaultData: T | ((element: HTMLElement) => T);
defaultLocalData?: U | ((element: HTMLElement) => U);
myDefaultAwareness?: V | ((element: HTMLElement) => V);
// Declarative render path — returns a lit-html template. Mutually exclusive
// with updateElement / onClick / onDrag (put events in the template).
view?: (ctx: ViewContext<T, U, V>) => unknown;
// Imperative alternative — mutate the DOM yourself on each change.
updateElement?: (ctx: ViewContext<T, U, V>) => void;
// Lifecycle. May return a cleanup, run on removal / unregister().
onMount?: (ctx: SetupContext<T, U, V>) => void | (() => void);
resetShortcut?: ModifierKey;
debounceMs?: number;
}
defaultData must be an object (or a function that returns one), not a bare value like 0 or "". An object shape lets you add fields later without migrating old data. Use { count: 0 }, not 0.
A valid initializer provides exactly one update path — view or updateElement. view is purely additive; existing updateElement capabilities are unchanged.
The view context
Section titled “The view context”view (and updateElement) receive:
interface ViewContext<T, U, V> {
data: T; // shared, synced state (read-only snapshot)
localData: U; // per-user, per-tab, un-synced state
awareness: V[]; // every connected user's awareness value
awarenessByStableId: Map<string, V>;
element: HTMLElement; // the mount-point element
setData(next: T | ((draft: T) => void)): void;
setLocalData(next: U | ((draft: U) => void)): void; // re-renders in view mode
setMyAwareness(next: V): void;
requestUpdate(): void; // re-run the view now (clock-driven views)
}
setData/setLocalData/setMyAwareness must not be called synchronously during a render — playhtml rejects it with a console error (it’s a re-render loop). Drive writes from @event handlers in the template or from onMount.
The onMount context is the same minus the live values, plus getters: getData(), getLocalData(), getAwareness(), getElement(), and the same setData / setLocalData / setMyAwareness / requestUpdate.
PlayElementHandle
Section titled “PlayElementHandle”Returned by register and getHandle; reads/writes resolve the live handler lazily, so a handle obtained before binding works once it binds.
interface PlayElementHandle<T, U, V> {
id: string;
getElement(): HTMLElement | null; // null until bound
getData(): T | undefined; // read-only snapshot
setData(next: T | ((draft: T) => void)): void;
setLocalData(next: U | ((draft: U) => void)): void;
setMyAwareness(next: V): void;
requestUpdate(): void; // no-op without a view
unregister(): void; // detach + run onMount cleanup; data is kept
}
A write through a handle whose element hasn’t bound yet is dropped (with a dev-mode warning); reads return undefined.
Re-exported lit-html helpers
Section titled “Re-exported lit-html helpers”playhtml re-exports the lit-html pieces a view needs. unsafeHTML is intentionally not exported, so interpolated values stay auto-escaped.
| Export | Use |
|---|---|
html | the tagged template for view output |
svg | SVG fragments (e.g. <path> inside <svg>) |
repeat(items, keyFn, template) | keyed lists — key by a stable unique id |
classMap(obj) | conditional classes |
styleMap(obj) | conditional inline styles (safer than a style string) |
nothing | render nothing (or just return null / undefined) |
See the lit-html templating guide for the full template syntax.