CxJS

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>
 
Store content
{}

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
Store content
{
  "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>
Full name:
Total: $0.00

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>
HelperDescription
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

Legacy

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" />