Skip to content

Runtime Functions

Most form definitions are static — every shortcut returns a typed object the engine renders once. Runtime functions flip a single widget into reactive mode: pass a callback in place of a value and the engine re-runs it whenever the form data changes, swapping in a new label, a new validator, a new event handler, anything you need.

gui.inputs.textInput('user.name', {
label: ({ $form }) =>
$form.registerMode ? 'Choose a username' : 'Sign-in name',
});

That single line covers a surprisingly large number of dynamic-form scenarios without you having to declare a state, write a selector, or split the widget into two definitions gated by include/exclude.

Every runtime function gets the same argument shape:

type DxRuntimeParams<FormData = any> = {
$form: FormData; // Live, immutable form data.
errors: ValidationStatus | undefined;
touched: boolean | undefined;
translate: I18nTranslator['translate'] | undefined;
};

$form is the canonical name — read it instead of the path-bound widget’s local value. errors and touched reflect the field’s validation state; translate is the active i18n translator if one is wired up.

Any prop in a gui.* shortcut can be a function. Below are the shapes you’ll reach for most.

gui.inputs.checkbox('registerMode', {
label: ({ $form }) =>
$form.registerMode ? 'Switch to login' : 'Switch to register',
});

The label text follows the toggle’s own value — flipping the checkbox swaps the wording immediately.

gui.inputs.textInput('user.name', {
validator: ({ $form }) =>
$form.registerMode
? { type: 'string', required: true, minLength: 3 }
: { type: 'custom', allowedNames: ['John', 'Jane'] },
});

When the user is registering, the field requires any 3+ character name. When the user is logging in, the field must match one of the allowed names. Same widget, two validation policies, decided per render.

onChange, onLoad, onFilter, and onBlur accept runtime-function handlers that fire on the event. The handler receives a wrapped DxFormEvent — same shape as the application-level formEvent callback, but with an update({ path, prop: value }) helper that translates into an OVERRIDE_WIDGET_PROP dispatch under the hood. So a single inline callback can read live form data and push changes to sibling widgets without leaving the form definition. Pass path to target an input widget by its data-binding key, or uid to target any widget — including display widgets (gui.displays.*) that have no data path.

const formDef = [
gui.inputs.booleanInput('upperMode', { label: 'Convert to UPPERCASE' }),
gui.inputs.textInput('source', {
label: 'Source',
placeholder: 'Type something…',
onChange: ({ data, update }) => {
const text = String(data.source ?? '');
const transformed = data.upperMode ? text.toUpperCase() : text;
update({
path: 'mirror',
hint: transformed || 'Type in the source field to see it mirrored.',
});
},
}),
gui.inputs.textInput('mirror', {
label: 'Mirror',
readonly: true,
hint: 'Type in the source field to see it mirrored.',
}),
];

The source field’s onChange reads data.upperMode and data.source, transforms the value, and pushes it into the mirror field’s hint via update({ path: 'mirror', hint: ... }). Toggle the uppercase switch to change the transformation on the next keystroke:

onClick is special: when it’s a function, the function is the click handler (it receives event.data directly, not the wrapped event) — so you can’t use it as a runtime selector for a different event name. For click-driven dispatches into the form, prefer wiring onClick to a named string event and handling it in your application’s formEvent callback.

Beyond label / validator / event handlers, any prop the widget exposes can be a runtime function — including the widget-specific ones (checkboxPosition, direction, placeholder, hint, etc.). The DX layer flattens those props onto the decorator, so you set them directly without nesting under props:

gui.inputs.checkbox('agreed', {
label: 'I agree',
checkboxPosition: ({ $form }) => ($form.rtlMode ? 'right' : 'left'),
});

Display widgets that produce a string (alert text, headings, hints) can read from $form the same way:

gui.displays.alert({
text: ({ $form }) =>
$form.user?.name
? `Hello ${$form.user.name}!`
: 'Welcome — type your name above.',
level: 'info',
});

The renderer widget takes this to its logical conclusion — its render prop is always a callback, and returning framework-specific markup (html\…“, JSX, an Angular template) is the standard usage.

  • Pure functions only. The engine calls runtime functions on every relevant data change. Side effects (network requests, timers, mutating external objects) will fire repeatedly — keep callbacks pure and read from $form.
  • Don’t mutate $form. It’s an immutable view; assigning to it is a no-op but signals a code smell.
  • Prefer states for shared conditions. If three widgets all branch on $form.registerMode, define a register state and reference its name from the override map and from include. You’ll get a single source of truth that the engine evaluates once.
  • States — named boolean expressions and gating widgets with include / exclude / when.
  • Selectors — decorate widget groups by type, tag, or uid.
  • Renderer widget — the canonical example of a function-based prop.