Complete examples

← Extensions · Documentation

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.

Next reading

← Extensions · Documentation

AppSana — экраны под задачу в приложениях, которые у вас уже есть.