JoyDom
Render a joy-dom Spec into Compose UI.
components (built with the components DSL) registers a renderer for any node type. A registered type wins over the built-in renderer, so it can override the built-ins (div, span, p, h1–h6, img) too. painterFactory resolves img.src strings to a androidx.compose.ui.graphics.painter.Painter; defaults to CoilPainterFactory, which loads each src through Coil 3 (a factory returning null renders an empty Box). onEvent is the single callback invoked whenever a node event binding fires (onclick/onfocus/onblur/onchange); it receives the full EventPayload and the host switches on name to decide what to do. When null (the default) a bound node is still interactive (clickable/focusable) — the dispatch is just dropped.
The composable wraps its content in BoxWithConstraints so width-based breakpoints re-evaluate when the parent's constraints change.
Known v1 limits (documented also in the module's README):
align-contentis not modelled in the serialization schema, so multi-line flex containers stack lines from the cross-axis start (cross gap between lines).CSS margin collapsing is not implemented (margin renders as outer padding).
onchangeis parsed and carried but has no native trigger for built-ins (the subset has no form inputs); only custom components can dispatch it (viaemit).
Render a view template — a document carrying state, defs and handlers alongside layout — with the logic layer wired up.
This is the store-backed entry: a com.j0y.joy.dom.mutation.JoyStore owns the state, every binding that fires is dispatched into it, a bound handler mutates state and the document re-resolves and recomposes. The view never writes; it only fires.
onEvent differs from the Spec overload's on purpose: it receives an com.j0y.joy.dom.mutation.EmittedEvent, and only for what the document did not consume — a binding whose name matches no handler (delivered exactly as the plain overload would) or an event action a handler reached. onError reports an interaction that stopped early; unlike a resolve failure this is not a rollback, so whatever the interaction applied before the failing step is already committed.
A document with no handlers region is inert: every binding flows straight out to onEvent and nothing else happens, which is precisely the plain overload's behaviour.
JoyDom(view = template, initialState = seed, onEvent = { host ->
when (host.name) {
"saveRequested" -> save() // the outbound half of the effect loop
}
})Hoist the store with rememberJoyDomStore instead when the host needs to read the state, inspect the session log, or drive undo through rewind.
Render a hoisted JoyDomStore. The store's callbacks were fixed when it was created (see rememberJoyDomStore); this overload only draws it and feeds every fired binding back in.
Lower-level entry: render a pre-resolved ResolvedSpec without re-running cascade / breakpoint evaluation. Useful when the caller already has the resolved tree (e.g. from a test fixture or a memoised resolution) and wants to skip viewport probing.