Complete examples
These examples combine the normative rules from the widget/action reference.
1. State, binding, and click
{
"id": "root",
"type": "Column",
"state": {
"title": "Counter",
"count": 0
},
"modifier": [
{ "name": "fillMaxWidth" },
{ "name": "paddingHorizontal", "value": "16" }
],
"children": [
{ "type": "Text", "params": { "text": "$root.title", "variant": "titleLarge" } },
{ "type": "Text", "params": { "text": "$root.count" } },
{
"type": "Button",
"onClick": {
"type": "jsUpdateState",
"path": "$root",
"jsCode": "function updateState(stateJson) { var s = JSON.parse(stateJson); s.count = s.count + 1; return JSON.stringify(s); }"
},
"children": [{ "type": "Text", "params": { "text": "Increment" } }]
}
]
}
2. Per-node state
{
"id": "root",
"type": "LazyColumn",
"state": { "selectedId": null },
"children": [
{
"id": "offer-42",
"type": "Card",
"state": {
"id": "offer-42",
"title": "Corner apartment",
"price": "25 000 000 ₽"
},
"children": [
{
"type": "Column",
"children": [
{ "type": "Text", "params": { "text": "$parent.title", "variant": "titleMedium" } },
{ "type": "Text", "params": { "text": "$parent.price" } },
{
"type": "Button",
"onClick": {
"type": "patch",
"patch": [{ "op": "add", "path": "$root/state/selectedId", "value": "offer-42" }]
},
"children": [{ "type": "Text", "params": { "text": "Select" } }]
}
]
}
]
}
]
}
The nested Text resolves $parent to the nearest state-owning ancestor (offer-42), skipping the stateless Column.
3. Layout, theme, and modifiers
{
"id": "root",
"type": "Column",
"state": {
"headline": "Find your next home",
"status": "Ready",
"loading": false
},
"theme": {
"colors": {
"primary": { "light": "#0077FF", "dark": "#80B7FF" }
}
},
"modifier": [
{ "name": "fillMaxWidth" },
{ "name": "paddingHorizontal", "value": "16" }
],
"children": [
{
"type": "Row",
"modifier": [{ "name": "fillMaxWidth" }],
"params": {
"horizontalArrangement": "SpaceBetween",
"verticalAlignment": "CenterVertically"
},
"children": [
{
"type": "Image",
"modifier": [
{ "name": "width", "value": "40" },
{ "name": "height", "value": "40" }
],
"params": {
"assetName": "ic_logo",
"contentDescription": "App logo"
}
},
{
"type": "Text",
"params": {
"text": "$root.headline",
"variant": "titleMedium",
"color": "$theme.colors.primary"
}
}
]
},
{ "type": "Spacer", "params": { "height": 16 } },
{ "type": "Divider" },
{
"type": "Box",
"modifier": [{ "name": "fillMaxWidth" }],
"params": {
"contentAlignment": "Center"
},
"children": [
{ "type": "CircularProgress", "params": { "visible": "$root.loading" } },
{ "type": "Text", "params": { "visible": false, "text": "$root.status" } }
]
}
]
}
4. Form controls
{
"id": "root",
"type": "Column",
"state": {
"form": {
"email": "",
"notify": true,
"channel": "push",
"budget": 50,
"agreed": false
},
"validation": {
"emailInvalid": false,
"emailMessage": ""
}
},
"children": [
{
"type": "TextField",
"params": {
"text": "$root.form.email",
"label": "Email",
"inputType": "email",
"isError": "$root.validation.emailInvalid",
"supportingText": "$root.validation.emailMessage"
}
},
{
"type": "Switch",
"params": {
"checked": "$root.form.notify",
"contentDescription": "Enable notifications"
}
},
{
"type": "RadioGroup",
"params": {
"value": "$root.form.channel",
"axis": "vertical"
},
"children": [
{
"type": "Radio",
"params": { "value": "push" },
"children": [{ "type": "Text", "params": { "text": "Push" } }]
},
{
"type": "Radio",
"params": { "value": "email" },
"children": [{ "type": "Text", "params": { "text": "Email" } }]
}
]
},
{
"type": "Slider",
"params": {
"value": "$root.form.budget",
"min": 0,
"max": 100,
"step": 5,
"contentDescription": "Budget"
}
},
{
"type": "Checkbox",
"params": { "checked": "$root.form.agreed" },
"children": [{ "type": "Text", "params": { "text": "I agree" } }]
}
]
}
5. HTTP, optimistic update, and rollback
{
"id": "settings",
"type": "Card",
"state": { "notifications": true },
"children": [
{
"type": "Switch",
"params": {
"checked": "$parent.notifications",
"contentDescription": "Enable notifications",
"onChange": {
"type": "http",
"method": "PUT",
"url": "/v1/settings/notifications",
"body": { "fromState": "$id.settings.notifications" },
"success": { "apply": "none" },
"onError": {
"type": "patch",
"patch": [{ "op": "add", "path": "$id.settings/state/notifications", "value": false }]
}
}
}
}
]
}
The switch writes before onChange; the request sees the new value. Failure rolls back.
Map a DTO before it hits state:
{
"type": "Button",
"onClick": {
"type": "http",
"method": "GET",
"url": "/v1/profile",
"success": {
"apply": "patch",
"from": "$response.data",
"jsMap": "function mapResponse(payloadJson, currentJson) { var p = JSON.parse(payloadJson); return JSON.stringify([{ op: 'replace', path: '$root/state/profile', value: { id: p.user_id, name: p.display_name, avatar: p.avatar_url } }]); }"
}
},
"children": [{ "type": "Text", "params": { "text": "Reload profile" } }]
}
6. Ordered submit and general error observer
{
"id": "root",
"type": "Column",
"state": {
"submitting": false,
"form": { "email": "ann@example.com" }
},
"observers": [
{
"id": "viewErrors",
"watch": "$error",
"action": {
"type": "async",
"actions": [
{ "type": "toast", "message": "$error.message", "style": "error" },
{
"type": "analytics",
"event": "bdui_action_failed",
"params": {
"action": "$error.actionType",
"code": "$error.code"
}
}
]
}
}
],
"children": [
{
"type": "Button",
"onClick": {
"type": "sync",
"actions": [
{ "type": "patch", "patch": [{ "op": "add", "path": "$root/state/submitting", "value": true }] },
{
"type": "http",
"method": "POST",
"url": "/v1/form",
"body": { "fromState": "$root.form" },
"success": {
"apply": "patch",
"from": "$response.ops"
},
"onError": {
"type": "patch",
"patch": [{ "op": "add", "path": "$root/state/submitting", "value": false }]
}
}
]
},
"params": {
"loading": "$root.submitting"
},
"children": [{ "type": "Text", "params": { "text": "Submit" } }]
}
]
}
The per-action handler clears loading; the root error observer reports the failure.
7. Custom widget and action
Host registration:
Bdui.init(
BduiConfig {
widgets.register("acme:Map", AcmeMapRenderer)
actions.register("acme:favoriteOffer", FavoriteOfferHandler)
}
)
Document:
{
"id": "offer-42",
"type": "Column",
"state": {
"id": "offer-42",
"title": "Flat",
"location": { "latitude": 55.75, "longitude": 37.61 }
},
"children": [
{
"type": "acme:Map",
"params": {
"latitude": "$parent.location.latitude",
"longitude": "$parent.location.longitude",
"markerTitle": "$parent.title"
}
},
{
"type": "Button",
"onClick": {
"type": "custom",
"name": "acme:favoriteOffer",
"args": { "offerId": "$id.offer-42.id" }
},
"children": [{ "type": "Text", "params": { "text": "Favorite" } }]
}
]
}
8. Local observer
{
"id": "offer-42",
"type": "Card",
"state": {
"id": "offer-42",
"liked": false,
"likes": 10
},
"observers": [
{
"id": "sendLike",
"watch": "$local.liked",
"trigger": "changed",
"skipInitial": true,
"action": {
"type": "http",
"method": "POST",
"url": "/v1/likes",
"body": { "fromState": "$local" },
"success": {
"apply": "patch",
"from": "$response.ops"
}
}
}
],
"children": [
{ "type": "Text", "params": { "text": "$parent.likes" } },
{ "type": "Switch", "params": { "checked": "$parent.liked", "contentDescription": "Like offer" } }
]
}
9. Streamed shell
Initial JSON:
{
"id": "root",
"type": "Column",
"state": { "title": "Offer", "price": null },
"children": [
{ "type": "Text", "params": { "text": "$root.title", "variant": "titleLarge" } },
{ "type": "Text", "params": { "text": "$root.price" } },
{
"id": "slot-gallery",
"type": "Box",
"params": { "contentAlignment": "Center" },
"children": [{ "type": "CircularProgress" }]
},
{
"id": "slot-reviews",
"type": "LazyColumn",
"children": []
}
]
}
Embed:
BduiView(
initialJson = shellJson,
streamUrl = "/v1/offers/42/bdui/stream",
modifier = Modifier.fillMaxWidth(),
)
Events:
id: 1
event: action
data: {"type":"patch","patch":[{"op":"merge","path":"$root/state","value":{"price":"25 000 000 ₽"}}]}
id: 2
event: action
data: {"type":"patch","patch":[{"op":"replace","path":"$id.slot-gallery","value":{"id":"slot-gallery","type":"Image","params":{"url":"https://cdn.example.com/hero.jpg","contentDescription":"Offer"}}}]}
id: 3
event: action
data: {"type":"patch","patch":[{"op":"add","path":"$id.slot-reviews/children/-","value":{"id":"review-1","type":"Text","params":{"text":"Great location"}}}]}
The tree keeps rendering during reconnect. The client sends Last-Event-ID: 3.
Those three events are also one atomic patch:
{
"type": "patch",
"patch": [
{ "op": "merge", "path": "$root/state", "value": { "price": "25 000 000 ₽" } },
{
"op": "replace",
"path": "$id.slot-gallery",
"value": {
"id": "slot-gallery",
"type": "Image",
"params": {
"url": "https://cdn.example.com/hero.jpg",
"contentDescription": "Offer"
}
}
},
{
"op": "add",
"path": "$id.slot-reviews/children/-",
"value": { "id": "review-1", "type": "Text", "params": { "text": "Great location" } }
}
]
}
10. RFC 6902 patch
Starting document:
{
"id": "root",
"type": "Column",
"state": {
"status": "loading",
"version": 3,
"draftTitle": "Offer",
"count": 0,
"c~d": 0
},
"params": { "enabled": true },
"children": [
{
"id": "slot-gallery",
"type": "Box",
"children": [{ "type": "CircularProgress" }]
},
{
"id": "slot-reviews",
"type": "LazyColumn",
"children": [
{ "id": "review-0", "type": "Text", "params": { "text": "Quiet street" } }
]
},
{
"id": "slot-footer",
"type": "Row",
"children": []
}
]
}
$id.* and $root name a widget node; state is /state/.... /children/- without $ is relative to optional targetId.
Mixed state and tree:
{
"type": "patch",
"patch": [
{ "op": "replace", "path": "$root/state/status", "value": "ready" },
{ "op": "merge", "path": "$root/state", "value": { "loading": false } },
{
"op": "add",
"path": "$id.slot-reviews/children/-",
"value": { "id": "review-1", "type": "Text", "params": { "text": "Great location" } }
}
]
}
Add a state object on a node that had none:
{
"type": "patch",
"patch": [
{ "op": "add", "path": "$id.slot-footer/state", "value": { "collapsed": false } }
]
}
Add a nested member (parent must exist):
{
"type": "patch",
"patch": [
{ "op": "add", "path": "$root/state/badge", "value": "new" }
]
}
Insert a child at index 0:
{
"type": "patch",
"patch": [
{
"op": "add",
"path": "$id.slot-reviews/children/0",
"value": { "id": "review-banner", "type": "Text", "params": { "text": "Latest reviews" } }
}
]
}
Append a child:
{
"type": "patch",
"patch": [
{
"op": "add",
"path": "$id.slot-reviews/children/-",
"value": { "id": "review-1", "type": "Text", "params": { "text": "Great location" } }
}
]
}
add on an existing object member replaces it:
{
"type": "patch",
"patch": [
{ "op": "add", "path": "$root/params/enabled", "value": false }
]
}
Remove an object member:
{
"type": "patch",
"patch": [
{ "op": "remove", "path": "$root/state/draftTitle" }
]
}
Remove an array element:
{
"type": "patch",
"patch": [
{ "op": "remove", "path": "$id.slot-reviews/children/0" }
]
}
Replace an existing value:
{
"type": "patch",
"patch": [
{ "op": "replace", "path": "$root/state/status", "value": "ready" }
]
}
Replace a named node:
{
"type": "patch",
"patch": [
{
"op": "replace",
"path": "$id.slot-gallery",
"value": {
"id": "slot-gallery",
"type": "Image",
"params": {
"url": "https://cdn.example.com/hero.jpg",
"contentDescription": "Offer"
}
}
}
]
}
Move an object member:
{
"type": "patch",
"patch": [
{ "op": "move", "from": "$root/state/draftTitle", "path": "$root/state/title" }
]
}
Move an array element (path is evaluated after from is removed):
{
"type": "patch",
"patch": [
{ "op": "move", "from": "$root/children/2", "path": "$root/children/0" }
]
}
Move a node by id:
{
"type": "patch",
"patch": [
{ "op": "move", "from": "$id.review-0", "path": "$id.slot-footer/children/-" }
]
}
Copy a child, then assign a unique id:
{
"type": "patch",
"patch": [
{ "op": "copy", "from": "$id.slot-reviews/children/0", "path": "$id.slot-reviews/children/-" },
{ "op": "replace", "path": "$id.slot-reviews/children/1/id", "value": "review-0-copy" }
]
}
test then mutate. "0" would fail here because count is the number 0:
{
"type": "patch",
"patch": [
{ "op": "test", "path": "$root/state/version", "value": 3 },
{ "op": "test", "path": "$root/state/count", "value": 0 },
{ "op": "replace", "path": "$root/state/status", "value": "ready" }
]
}
JSON Pointer escaping (a/b and c~d):
{
"type": "patch",
"patch": [
{ "op": "add", "path": "$root/state/a~1b", "value": true },
{ "op": "replace", "path": "$root/state/c~0d", "value": 1 }
]
}
merge into state vs into a widget:
{
"type": "patch",
"patch": [
{ "op": "merge", "path": "$root/state", "value": { "loading": false } },
{ "op": "merge", "path": "$id.slot-gallery", "value": { "params": { "contentAlignment": "Center" } } }
]
}
Failures (nothing commits): add under a missing parent, replace a missing path, or test with the wrong type.
{ "type": "patch", "patch": [{ "op": "add", "path": "$root/state/missing/child", "value": 1 }] }
{ "type": "patch", "patch": [{ "op": "replace", "path": "$root/state/absent", "value": true }] }
{ "type": "patch", "patch": [{ "op": "test", "path": "$root/state/count", "value": "0" }] }
See patch for field defaults and targetId vs $ paths.
12. Full-screen Scaffold result
Host the tree in BduiScreenHost. The caller stays an embeddable view; the destination covers the app window and returns JSON through $event.
{
"id": "root",
"type": "Column",
"state": { "picked": "Nothing selected" },
"children": [
{ "type": "Text", "params": { "text": "$root.picked" } },
{
"type": "Button",
"onClick": {
"type": "openScreen",
"initialJson": {
"id": "root",
"type": "Scaffold",
"state": { "title": "Choose a city" },
"topBar": {
"type": "TopAppBar",
"params": { "title": "$root.title" }
},
"children": [
{
"type": "Button",
"onClick": {
"type": "popScreen",
"result": { "title": "Almaty" }
},
"children": [{ "type": "Text", "params": { "text": "Almaty" } }]
}
]
},
"onResult": {
"type": "patch",
"patch": [
{ "op": "add", "path": "$root/state/picked", "value": "$event.title" }
]
}
},
"children": [{ "type": "Text", "params": { "text": "Pick a city" } }]
}
]
}
$root.title inside initialJson belongs to the opened screen. onResult runs on the caller after popScreen.