Skip to main content
Data binding is the mechanism that connects V2 form components to live data. Instead of hard-coding values in props, you write JavaScript expressions in bindings that are evaluated against a data context. When the underlying data changes, bindings are re-evaluated and the UI updates automatically. This guide covers the full binding system: how bindings work, the shape of the data context, loop scoping with for, computed bindings that reference named scripts, and the reactivity model that drives updates. For the scripting language details and sandbox limitations, see the Scripting guide.

How Bindings Work

Every UINode can have a bindings object that maps prop names to JavaScript expressions. At render time, each expression is evaluated with the current data context injected as ctx. The result replaces (or supplements) the corresponding value in props.
{
  "component": "card:label",
  "props": {
    "visible": true
  },
  "bindings": {
    "label": "ctx.dataObjects?.[0]?.path ?? 'No data'"
  }
}
In this example, visible is a static prop (always true), while label is a dynamic binding that evaluates against the data context. If both props and bindings define the same key, the binding takes precedence.

Expression Evaluation

Binding expressions are evaluated by wrapping them in an immediately-invoked function:
(function() {
  const ctx = { /* current data context */ };
  return (/* your expression */);
})()
This means your expression is a single JavaScript expression (not a statement block). You can use:
  • Optional chaining: ctx.dataObjects?.[0]?.path
  • Nullish coalescing: ctx.value ?? 'default'
  • Ternary operators: ctx.count > 0 ? 'Has items' : 'Empty'
  • Template literals: `Total: ${ctx.count} items`
  • Array methods: ctx.dataObjects?.filter(o => o.path === 'invoice').length
  • Arithmetic: ctx.amount * 1.1
Binding expressions should be pure — they should not modify any state, make API calls, or produce side effects. The runtime may re-evaluate them at any time when the data context changes.

Data Context Shape

The ctx object passed to binding expressions contains the current data context. At the root level, it has the following shape:
interface DataContextV2 {
  dataObjects?: DataObject[];      // All data objects in scope
  tagMetadataMap?: Map<string, any>; // Tag metadata keyed by taxonomy path
  $item?: any;                      // Current item in a for loop
  $index?: number;                  // Current index in a for loop
  $parent?: DataContextV2;          // Parent context (in nested loops)
  $root?: DataContextV2;            // Root context (always the top-level)
  [key: string]: any;               // Additional keys injected by the runtime
}

Root Context

At the top level (outside any for loop), ctx contains:
PropertyTypeDescription
ctx.dataObjectsDataObject[]All data objects from the workspace store
ctx.tagMetadataMapMap<string, any>Tag metadata from the project store, keyed by taxonomy path

DataObject Shape

Each data object in ctx.dataObjects has the following properties (among others):
{
  uuid: string;           // Unique identifier
  path: string;           // Taxonomy path (e.g., "invoice", "lineItem")
  parent?: { uuid: string }; // Parent reference
  attributes?: DataAttribute[]; // Child attributes
}

DataAttribute Shape

{
  uuid: string;
  path: string;       // Full path (e.g., "invoice/totalAmount")
  value: any;          // The attribute value
  dataObjectId: number; // Parent data object ID
}

Accessing Data in Expressions

{
  "bindings": {
    "label": "ctx.dataObjects?.find(o => o.path === 'invoice')?.attributes?.find(a => a.path === 'invoice/invoiceNumber')?.value ?? 'N/A'"
  }
}
For complex lookups, consider using a named script via computed instead of a long inline expression.

Loop Scoping

The for configuration on a UINode renders the node once for each item in a source collection. Inside the loop, the data context is extended with loop-specific variables.
{
  "component": "card:cardPanel",
  "for": {
    "source": "ctx.dataObjects?.filter(o => o.path === 'lineItem')",
    "itemAs": "$item",
    "indexAs": "$index",
    "key": "$item.uuid"
  },
  "bindings": {
    "title": "'Line Item #' + ($index + 1)"
  },
  "children": [
    {
      "component": "card:label",
      "bindings": {
        "label": "$item.attributes?.find(a => a.path === 'lineItem/description')?.value ?? 'No description'"
      }
    }
  ]
}

Loop Variables

VariableTypeDescription
$itemanyThe current element from the source array. The name is configurable via itemAs.
$indexnumberThe zero-based index of the current element. The name is configurable via indexAs.
$parentDataContextV2The parent data context (the context from before the loop started).
$rootDataContextV2The root-level data context (always the top-level context).

Nested Loops

When you nest for iterators, each level creates a new scope. Use $parent to access the outer loop’s context:
{
  "component": "card:cardPanel",
  "for": {
    "source": "ctx.dataObjects?.filter(o => o.path === 'invoice')",
    "itemAs": "$item",
    "key": "$item.uuid"
  },
  "bindings": {
    "title": "'Invoice: ' + $item.uuid.slice(0, 8)"
  },
  "children": [
    {
      "component": "card:label",
      "for": {
        "source": "ctx.dataObjects?.filter(o => o.path === 'lineItem' && o.parent?.uuid === $item.uuid)",
        "itemAs": "$item",
        "indexAs": "$lineIndex",
        "key": "$item.uuid"
      },
      "bindings": {
        "label": "'Line ' + ($lineIndex + 1) + ' of invoice ' + $parent.$item?.uuid?.slice(0, 8)"
      }
    }
  ]
}
When a nested loop uses the same itemAs name as an outer loop, the inner variable shadows the outer one. Use $parent.$item to access the outer item. For clarity, consider using distinct names like $invoice and $lineItem.

Key Expressions

The key field in ForConfig is evaluated per item and must produce a unique, stable value. This helps the renderer efficiently update the DOM when items are added, removed, or reordered. Good key expressions:
  • "$item.uuid" — unique and stable
  • "$item.id" — unique within a document
Avoid using $index as a key, since it changes when items are reordered.

Computed Bindings

The computed object maps prop names to named scripts from the scripts or scriptModules registry. The script is called with the current data context and its return value becomes the prop.
{
  "version": "2",
  "scripts": {
    "totalAmount": "function(ctx) { return ctx.dataObjects?.filter(o => o.path === 'lineItem').reduce((sum, o) => { const amt = o.attributes?.find(a => a.path === 'lineItem/amount'); return sum + (Number(amt?.value) || 0); }, 0).toFixed(2) ?? '0.00'; }"
  },
  "nodes": [
    {
      "component": "card:label",
      "computed": {
        "label": "totalAmount"
      }
    }
  ]
}

When to Use computed vs bindings

Use bindings when…Use computed when…
The expression is simple and self-containedThe logic is complex or multi-step
The expression is only used in one placeThe same logic is reused across multiple nodes
You want inline visibility of the logicYou want to name and document the logic

Reactivity Model

V2 forms use Vue’s reactivity system under the hood. The data context is provided via Vue’s provide/inject mechanism, and bindings are evaluated inside Vue computed properties. This means:
  1. Automatic updates: When dataObjects or tagMetadataMap change in the store, all bindings that reference them are automatically re-evaluated.
  2. Efficient updates: Vue only re-evaluates bindings whose dependencies have changed. If a binding only reads ctx.dataObjects[0].path, it will not re-evaluate when a different data object changes.
  3. Synchronous evaluation: Binding expressions are evaluated synchronously. Async operations (API calls, etc.) should be performed in event handlers, not bindings.

Data Flow

Workspace Store (data objects, attributes)
    ↓ (Vue reactive provide)
Schema Root (root data context)
    ↓ (provide/inject)
Schema Node (evaluates bindings against ctx)
    ↓ (resolved props)
Card Component (renders with resolved props)

Triggering Updates

The data context updates automatically when:
  • Data objects are added, removed, or modified in the workspace store
  • Attributes are updated via the data attribute editor or via kodexa.data.setAttribute
  • Tag metadata changes in the project store
You do not need to manually trigger re-evaluation. The Vue reactivity system handles this.

Common Patterns

Conditional Display Based on Data

{
  "component": "card:label",
  "if": "ctx.dataObjects?.some(o => o.path === 'invoice')",
  "bindings": {
    "label": "'Found ' + ctx.dataObjects.filter(o => o.path === 'invoice').length + ' invoice(s)'"
  }
}

Default Values

{
  "bindings": {
    "label": "ctx.$item?.attributes?.find(a => a.path === 'invoice/vendor')?.value || 'Unknown Vendor'"
  }
}

Derived Styling

{
  "bindings": {
    "backgroundColor": "ctx.$item?.attributes?.find(a => a.path === 'lineItem/amount')?.value > 10000 ? '#fef3c7' : '#ffffff'"
  }
}

Combining Static Props with Bindings

{
  "component": "card:cardPanel",
  "props": {
    "showHeader": true,
    "useTabs": true,
    "groupTaxon": "invoice"
  },
  "bindings": {
    "title": "'Invoices (' + (ctx.dataObjects?.filter(o => o.path === 'invoice').length ?? 0) + ')'"
  }
}

Next Steps