Data Binding
Data binding connects your UI to the store, enabling automatic synchronization between widgets and application state. When store data changes, bound widgets update automatically. When users interact with widgets, their changes flow back to the store.
Accessor Chains
The primary way to bind data in CxJS is through accessor chains created with createModel. Pass an accessor directly to widget properties for two-way binding:
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<TextField value={m.name} placeholder="Enter your name" />
<div text={m.name} />
</div>
<div class="flex flex-col gap-2">
<Slider value={m.volume} />
<div text={m.volume} />
</div>
<div class="p-3 bg-muted rounded text-sm">
<strong>Store content</strong>
<pre class="mt-2" text={(data) => JSON.stringify(data, null, 2)} />
</div>
</div> {}When you assign m.name to the value property, CxJS creates a two-way binding. The TextField displays the current value and writes changes back to the store.
Default Values with bind
Use bind to provide a default value when the store path is undefined:
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<TextField value={bind(m.username, "Guest")} placeholder="Username" />
<div>
<strong>Username: </strong>
<span text={bind(m.username, "Guest")} />
</div>
</div>
<div class="flex flex-col gap-2">
<NumberField value={bind(m.count, 0)} placeholder="Count" />
<div>
<strong>Count: </strong>
<span text={bind(m.count, 0)} />
</div>
</div>
<div class="p-3 bg-muted rounded text-sm">
<strong>Store content</strong>
<pre class="mt-2" text={(data) => JSON.stringify(data, null, 2)} />
</div>
</div> {
"username": "Guest",
"count": 0
}When the widget initializes, if the store path is undefined, the default value is automatically written to the store.
Computed Values with expr
Use expr to compute values from one or more store paths. The function recalculates whenever any of its dependencies change:
<div class="flex flex-col gap-4">
<div class="grid grid-cols-2 gap-2">
<TextField value={m.firstName} placeholder="First name" />
<TextField value={m.lastName} placeholder="Last name" />
</div>
<div>
<strong>Full name: </strong>
<span
text={expr(m.firstName, m.lastName, (first, last) =>
`${first || ""} ${last || ""}`.trim(),
)}
/>
</div>
<div class="grid grid-cols-2 gap-2">
<NumberField value={m.price} placeholder="Price" format="currency;USD" />
<NumberField value={m.quantity} placeholder="Quantity" />
</div>
<div>
<strong>Total: </strong>
<span
text={expr(m.price, m.quantity, (price, qty) => {
let total = (price || 0) * (qty || 0)
return `$${total.toFixed(2)}`
})}
/>
</div>
</div> The expr function takes accessor chains as arguments, followed by a compute function that receives the current values:
expr(m.firstName, m.lastName, (first, last) => `${first} ${last}`);
Computed Values with computable
For complex calculations, use computable instead of expr. It works the same way but adds memoization — the result is cached and only recalculated when dependencies actually change:
import { computable } from "cx/data";
// Memoized computation - result cached until items or taxRate changes
const total = computable(m.items, m.taxRate, (items, taxRate) => {
const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
return subtotal * (1 + taxRate);
});
Use computable when the calculation is expensive or when the same value is used in multiple places.
Formatting Values
Use format to apply format strings to bound values:
import { format } from "cx/ui";
<span text={format(m.price, "currency;USD")} />
<span text={format(m.date, "d;yyyyMMdd")} />
Use tpl to combine multiple values into formatted text:
import { tpl } from "cx/ui";
// Positional placeholders
<p text={tpl(m.firstName, m.lastName, "{0} {1}")} />
// With formatting
<p text={tpl(m.age, "{0:n;0} years old")} />
// With null fallback
<p text={tpl(m.name, "Hello, {0|Guest}!")} />
See Formatting for the complete format syntax reference.
Expression Helpers
CxJS provides type-safe helper functions for common boolean expressions. These return Selector<boolean> and are useful for properties like visible, disabled, and readOnly:
import { truthy, isEmpty, greaterThan } from "cx/ui";
<div visible={truthy(m.user.name)}>User has a name</div>
<div visible={isEmpty(m.items)}>No items available</div>
<div visible={greaterThan(m.user.age, 18)}>User is an adult</div>
| Helper | Description |
|---|---|
truthy(accessor) | Evaluates truthiness |
falsy(accessor) | Evaluates falsiness |
isTrue(accessor) | Strict true check |
isFalse(accessor) | Strict false check |
hasValue(accessor) | Checks for non-null/undefined |
isEmpty(accessor) | Checks for empty strings/arrays |
isNonEmpty(accessor) | Checks for non-empty strings/arrays |
equal(accessor, value) | Loose equality comparison |
notEqual(accessor, value) | Loose inequality comparison |
strictEqual(accessor, value) | Strict equality comparison |
strictNotEqual(accessor, value) | Strict inequality comparison |
greaterThan(accessor, value) | Numeric greater than |
lessThan(accessor, value) | Numeric less than |
greaterThanOrEqual(accessor, value) | Numeric greater than or equal |
lessThanOrEqual(accessor, value) | Numeric less than or equal |
format(accessor, formatString) | Formats value using format strings |
Legacy Binding Syntax
The following binding methods are supported for backwards compatibility but are not recommended for new code.
String-based bind
Before typed models, bindings used string paths:
import { bind } from "cx/data";
// Legacy string-based binding
<TextField value={bind("user.name")} />
// Modern accessor chain (preferred)
<TextField value={m.user.name} />
String-path templates
The tpl function also supports string-path syntax:
import { tpl } from "cx/data";
// Legacy string-path template
<div text={tpl("Hello, {user.name}!")} />
// Modern typed accessor (preferred)
<div text={tpl(m.user.name, "Hello, {0}!")} />
Attribute suffixes
In older CxJS code, you may see attribute suffixes like -bind, -expr, and -tpl. These require Babel plugins and are not supported in the TypeScript-first approach:
// Legacy attribute suffixes (requires Babel plugin)
<TextField value-bind="user.name" />
<div text-tpl="Hello, {user.name}!" />
<div visible-expr="{items.length} > 0" />