Joy DOM

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 ComponentScopethis 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):

TypeNullableThrowing
IntgetIntOrNull(key)getInt(key)
LonggetLongOrNull(key)getLong(key)
DoublegetDoubleOrNull(key)getDouble(key)
FloatgetFloatOrNull(key)getFloat(key)
BooleangetBooleanOrNull(key)getBoolean(key)
StringgetStringOrNull(key)getString(key)

Built-in types (div, span, p, h1h6, 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

On this page