Skip to main content
V2 data forms are declarative JSON documents that describe a component tree. The renderer walks the tree top-down, resolving each node’s component from the registry, evaluating conditionals and bindings, and rendering the result. This page covers the top-level form shape, the UINode interface, conditional rendering, iteration, and styling.

The DataFormV2 Shape

Every V2 form has four top-level sections alongside the component tree.
{
  "version": "2",
  "nodes": [],
  "scripts": {
    "formatCurrency": "function(ctx) { return '$' + Number(ctx.value).toFixed(2); }"
  },
  "scriptModules": {
    "validateRow": {
      "source": "function(ctx) { return ctx.amount > 0; }",
      "description": "Validates that amount is positive",
      "inputs": { "amount": "number" },
      "returns": "boolean",
      "debounce": 300
    }
  },
  "bridge": {
    "permissions": ["data:read", "data:write", "navigation"],
    "apiBaseUrl": "/api",
    "maxExecutionMs": 2000
  }
}
FieldTypeDescription
version"2"Required flag that enables V2 rendering. If omitted, a non-empty nodes array also triggers V2 mode.
nodesUINode[]The root-level component tree.
scriptsRecord<string, string>Named script dictionary. Keys are names referenced by scriptRef events; values are JavaScript function bodies.
scriptModulesRecord<string, ScriptModule>Reusable script modules with declared inputs, returns, and optional debounce.
bridgeBridgeConfigPermissions and configuration for the kodexa.* Bridge API. The permissions array controls which bridge methods scripts may call.

UINode Anatomy

A UINode is the fundamental building block. Every element in a V2 form — panels, editors, labels, grids — is a UINode.
interface UINode {
  component: string;                              // Registry key, e.g. "card:cardPanel"
  props?: Record<string, any>;                    // Static property values
  bindings?: Record<string, string>;              // JS expressions evaluated against the data context
  computed?: Record<string, string>;              // Cached computed expressions (reserved)
  events?: Record<string, EventConfig | EventConfig[]>; // Event handler configurations
  children?: UINode[];                            // Nested child nodes (default slot)
  slots?: Record<string, UINode[]>;               // Named slot content
  if?: string;                                    // JS expression -- removes node from DOM when falsy
  show?: string;                                  // JS expression -- hides via CSS when falsy
  for?: ForConfig;                                // Iteration directive
  class?: string | Record<string, string>;        // CSS class(es)
  style?: Record<string, any> | string;           // Inline styles
  key?: string;                                   // Unique identifier for list rendering
  ref?: string;                                   // Template ref for Bridge API access
  meta?: NodeMeta;                                // Design-time metadata (ignored at runtime)
}

How the renderer works

SchemaRoot initializes the QuickJS script runtime, the Bridge host, and a reactive data context containing the current dataObjects and tagMetadataMap. It then walks the nodes array and renders a SchemaNode for each entry. Each SchemaNode resolves its component from the registry, evaluates the if conditional, merges static props with dynamically evaluated bindings, wires up events as handler functions, and renders the resolved Vue component. Children and slots are rendered recursively as nested SchemaNode instances.

Conditional Rendering

V2 forms support two conditional mechanisms that control whether a node appears in the DOM.

if and show (JavaScript expressions)

if removes the node from the DOM entirely when its expression evaluates to a falsy value. show keeps the node mounted but hides it with display: none.
{
  "component": "card:cardPanel",
  "if": "ctx.dataObjects?.some(o => o.path === 'invoice')",
  "props": { "title": "Invoice Details" }
}
{
  "component": "card:label",
  "show": "ctx.$item?.status !== 'draft'",
  "props": { "label": "Published" }
}
Use if when the node is expensive to keep mounted or when you want clean DOM output. Use show when you need the node to retain its internal state while toggled.

ifFormula and showFormula (reactive KEXL formulas)

These behave the same as if and show but accept reactive KEXL formulas evaluated via the WASM document engine rather than plain JavaScript expressions. They re-evaluate automatically when referenced data attributes change, without requiring explicit watchers.
{
  "component": "card:cardPanel",
  "ifFormula": "hasValue('invoice/totalAmount')",
  "props": { "title": "Total Summary" }
}
{
  "component": "card:label",
  "showFormula": "getAttribute('invoice/status') == 'approved'",
  "props": { "label": "Approved" }
}
ifFormula and showFormula are planned extensions to the UINode interface. Use if and show with JavaScript expressions for current implementations.

Iteration

The for directive renders a node once for each item in a source collection. Loop variables are injected into the data context for child expressions.
interface ForConfig {
  source: string;   // JS expression resolving to an array
  itemAs: string;   // Variable name for the current item
  indexAs?: string;  // Variable name for the current index
  key: string;       // Expression for a unique key per item
}

Example: rendering a panel per data object

{
  "component": "card:cardPanel",
  "for": {
    "source": "ctx.dataObjects",
    "itemAs": "$item",
    "indexAs": "$index",
    "key": "$item.uuid"
  },
  "bindings": {
    "title": "$item.path + ' (#' + ($index + 1) + ')'"
  },
  "children": [
    {
      "component": "card:dataAttributeEditor",
      "bindings": {
        "dataObjectUuid": "$item.uuid"
      },
      "props": { "taxon": "lineItem/description" }
    }
  ]
}
The source expression is evaluated against the current data context. Each iteration injects $item and $index (or whatever names you specify in itemAs and indexAs) into the context for use in bindings and child expressions. The key expression must produce a unique value per item to enable efficient DOM updates.

Styling

V2 nodes accept class and style for visual customization. class can be a static string or a record mapping class names to binding expressions for conditional application:
{
  "class": "mt-4 rounded-lg border"
}
{
  "class": {
    "bg-red-50": "ctx.$item?.hasException",
    "bg-white": "!ctx.$item?.hasException"
  }
}
style accepts either an inline CSS string or an object of style properties:
{
  "style": { "maxHeight": "400px", "overflow": "auto" }
}
The platform uses Tailwind CSS, so Tailwind utility classes in the class field are the preferred approach for most styling needs.

Next Steps

Data Binding

Expressions, context variables, and reactive bindings

Layout Components

Panels, tabs, rows, columns, and alerts

Data Components

Attribute editors, tables, grids, and more

Bridge API

The kodexa.* scripting API reference