← LearnData binding basicsBuild a working Counter screen from the ground up, layering in the three kinds of @{binding} one at a time: value, visibility, event. When you finish you have a spec + layout + ViewModel triple that demonstrates all three, and a mental model for where to add the fourth binding you will inevitably need.~10 min read
Start with a skeletonEverything in this walkthrough flows through the spec. Start by declaring what state and events the Counter needs. `count` is an Int so the UI can show a number; `statusVisibility` is a String — not a Bool — because visibility values in JsonUI are always strings ('visible' / 'invisible' / 'gone'). The two handlers do not have bodies yet; we will add them after the layout lands.
counter.spec.json
// counter.spec.json (minimum){ "metadata": { "name": "Counter", "platforms": ["web"], "layoutFile": "counter" }, "stateManagement": { "uiVariables": [ { "name": "count", "type": "Int", "initial": "0" }, { "name": "statusVisibility", "type": "String", "initial": "\"gone\"" } ], "eventHandlers": [ { "name": "onIncrement" }, { "name": "onDecrement" } ] }}Bind a valueThe simplest binding. `@{count}` in a Label's `text` attribute pulls whatever the ViewModel's `count` field currently holds. If the ViewModel sets count to 3, the label renders '3'. If it later sets count to 0, the label re-renders as '0'. No manual subscribe, no useState — the layout just refers to the field and the generator does the rest.
counter.json
// docs/screens/layouts/counter.json{ "type": "View", "orientation": "vertical", "child": [ { "type": "Label", "fontSize": 48, "text": "@{count}" } ]}Bind visibilityVisibility bindings let an element appear and disappear in response to state. Here the status label is hidden while the counter is at zero and shows up the moment it isn't. Three rules about visibility: (1) the value is a string, not a bool; (2) 'gone' removes the element from layout, 'invisible' keeps its space, 'visible' shows it; (3) derive the visibility in the ViewModel — never put `@{count > 0 ? ...}` in the layout (it won't compile on any platform — see /concepts/data-binding for why).
layout + VM excerpt
// Add a second label that appears only when count > 0{ "type": "Label", "fontSize": 18, "fontColor": "#2563EB", "visibility": "@{statusVisibility}", "text": "status_ok"} // In the ViewModel:// statusVisibility = count > 0 ? 'visible' : 'gone'// recomputed on every onIncrement / onDecrement.Bind eventsEvents are just ViewModel methods. `onClick: "@{onIncrement}"` on a Button tells the generator to call `data.onIncrement()` when the button is tapped on any platform. The ViewModel owns what that method does — typically mutate state then re-publish via updateData. The layout stays dumb; buttons do not know what happens when they are pressed, only who gets told.
layout excerpt
// Two buttons, each bound to a ViewModel event{ "type": "View", "orientation": "horizontal", "child": [ { "type": "Button", "text": "btn_minus", "onClick": "@{onDecrement}" }, { "type": "Button", "text": "btn_plus", "onClick": "@{onIncrement}" } ]}When it does not work
Build error: "unknown binding 'count'".The `@{count}` reference points at a uiVariable the ViewModel does not declare. Add it to `stateManagement.uiVariables` in the spec (not just the layout's data block) — the generator derives the ViewModel base from the spec, so any binding that is not spec-declared is treated as a typo.
statusVisibility never changes — the label is stuck in its initial state.You updated count but forgot to re-derive statusVisibility. When a derived visibility field depends on another field, the ViewModel must recompute and re-publish it every time the source changes. Typically you do this inside the same updateData call: `this.updateData({ count: next, statusVisibility: next > 0 ? 'visible' : 'gone' })`.
Button tap does nothing at runtime.Check three things: (1) the spec declared `onIncrement` in eventHandlers, (2) the layout wired `onClick: "@{onIncrement}"` exactly — not `"onIncrement"` or `"@{data.onIncrement}"`, (3) the ViewModel exposes `onIncrement` (or passes it via `initializeEventHandlers` + `updateData`). Missing any one of the three makes the tap no-op silently on web and crashes at launch on iOS — neither is what you want.
Keep going
Data binding as contractThe why. Longer essay on the discipline behind these rules./concepts/data-binding
Your first screenBeyond Counter — lay out a real screen with Header / Hero / Collection./learn/first-screen