Custom components
Render the document's kebab-case node types with your own composables.
If a document references custom nodes (kebab-case type), register a composable for each with the components { } DSL and pass the result as components. Each component(type) { … } block is a JoyDomComponent whose body renders against a ComponentScope — this exposes the resolved node, its parent, a Children() composable, and emit:
@Composable
fun RatingScreen(spec: Spec) {
val registry = components {
component("rating-stars") {
val value = node.props.getIntOrNull("value") ?: 0
Row { repeat(5) { i -> Text(if (i < value) "★" else "☆") } }
}
default { Text("Unknown component") }
}
JoyDom(spec = spec, components = registry)
}Read a custom prop with the typed accessors below — node.props.getIntOrNull("value"), node.props.getStringOrNull("src"), etc. Raw access via node.props["value"] returns a JsonElement; the reserved keys (id, className, style, on*) stay typed on node.props. Node-level custom keys (siblings of type/props/children in the JSON) read off node["key"]. Children come off node.children; resolved style off node.style.
Reading prop values
The same accessor set works on anything backed by a Map<String, JsonElement> — a node's props, the node itself (node["key"]), and an event's params/payload. Each type has a nullable getXxxOrNull (returns null when the key is absent or holds the wrong type) and a throwing getXxx (throws NoSuchElementException when absent or mistyped):
| Type | Nullable | Throwing |
|---|---|---|
Int | getIntOrNull(key) | getInt(key) |
Long | getLongOrNull(key) | getLong(key) |
Double | getDoubleOrNull(key) | getDouble(key) |
Float | getFloatOrNull(key) | getFloat(key) |
Boolean | getBooleanOrNull(key) | getBoolean(key) |
String | getStringOrNull(key) | getString(key) |
Built-in types (div, span, p, h1–h6, img) render through the built-in renderer, but registering one of those names overrides it — component("img") { … } replaces the built-in image rendering. For img loading on built-in nodes, pass a painterFactory: @Composable (src: String) -> Painter? (see the API reference ↗).
Laying out children
A custom component can act as a container by calling Children(), which lays the node's children out with the same flow/flex dispatch a built-in would use:
components {
component("card") { Children() }
}To own placement instead, render each entry yourself with Child(child) — an element child still goes through the full node pipeline (styles, nested components, its own children), and a text run renders as a joy-dom text node:
components {
component("stack") {
Column { node.children.forEach { Child(it) } }
}
}Unregistered custom types render nothing
A custom node with no registered component (and no default {}) renders nothing — the node and
its whole subtree are hidden. Register a default {Children()} to fall back to rendering
children, or default {/* stub using node.type */} for a visible placeholder. Built-ins keep
their own renderer regardless.
What's next
- Events — dispatch from a custom component with
emit. - API reference (Dokka) ↗ — the full
ComponentScopeandcomponentsDSL surface.