Skip to content

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";

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 the elementId). The can-play attribute is optional — register implies it.
  • Re-registering the same id replaces its initializer.

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;
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.

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.

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.

playhtml re-exports the lit-html pieces a view needs. unsafeHTML is intentionally not exported, so interpolated values stay auto-escaped.

ExportUse
htmlthe tagged template for view output
svgSVG 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)
nothingrender nothing (or just return null / undefined)

See the lit-html templating guide for the full template syntax.