Skip to content
Forms, switch product

States

States are named boolean expressions over the live form data. Declare them once in formConfig.states, then reference the names from include / exclude rules, per-state property overrides, and selectors.

const formConfig = {
states: {
hasDiscount: '$form.hasDiscountCode === true',
isAdult: '$form.driverAge >= 18',
},
};

Expressions are JavaScript-like. The engine exposes:

  • $form — the current form data tree.
  • Standard operators: &&, ||, !, ===, !==, <, >, <=, >=, +, -, *, /, ?:.
  • Optional chaining: $form.user?.name.
  • $fn — host functions you supply through the functions config, called as $fn.name(...).

Expressions can reference any path in the form data.

Expressions only read the data. Before an expression runs for the first time, the form checks it: in named states, when rules, {{ }} slots and i18n params alike.

  • Calls. An expression can call host functions under $fn, and these methods on any value: map, filter, reduce, find, some, every, includes, indexOf, join, slice, at, toFixed, toUpperCase, toLowerCase and trim. Arrow functions work as their callbacks: $form.items.some(i => i.price > 100).
  • No writes. Assignments (=, += and the rest), ++, -- and delete are rejected, and so are the methods that change a value in place: those of arrays (push, pop, splice, sort, reverse and the like), of Maps and Sets (set, add, delete, clear) and of Dates (setDate, setFullYear and the other setters). The check sees only the method’s name, so an immutable library’s method with the same name, such as dayjs’s add, is rejected too: call it in a host function.
  • No code. new, tagged templates, the names constructor, prototype and __proto__, and calls on anything other than a method or $fn are rejected.
  • Size. An expression has at most 400 characters, 80 nodes and 30 levels of nesting.

A rejected expression fails: the form reports { status: 'errored', code: 50 } through formHealth, with the reason in its message, and the data stays unchanged. Writes and code always fail. A method outside the list, or an expression over the size limits, runs by default and prints a warning in development: set expressionPolicy to 'strict' to make those fail too. The MCP server runs the same check when it validates a form, so these errors show before the form runs.

A state expression must evaluate to exactly true to activate — a truthy non-boolean does not count. Have host functions used in states return a real boolean: hasItems: (items) => (items?.length ?? 0) > 0, not (items) => items?.length.

  • In include / exclude rules: gate widgets.
  • In per-state property overrides on shortcuts: change props when active.
  • In gui.selectors.state(...) chains: scope a selector to one state.

Two properties on every shortcut control whether a widget renders:

PropertyShape
include{ in: ['stateName', ...] } — render when any named state is active. Or { when: '<expr>' } — render only when the expression is truthy.
exclude{ from: ['stateName', ...] } — render unless any of the named states are active. Or { when: '<expr>' } — render unless the expression is truthy.

Note the field names — include uses in, exclude uses from. Both also accept when for inline expressions that don’t need a name.

When to use each

  • include with in — the condition has a name and is used in multiple places (gating, prop overrides, selectors).
  • include with when — a one-off condition, expressed inline, not worth declaring a state for.
  • exclude — when “hide-when X” reads more naturally than “show-unless X”.

Putting it together — a support-ticket form

Section titled “Putting it together — a support-ticket form”

The demo below shows the three gating mechanisms working side by side:

  • include: { in: ['isBug'] } on the Steps to reproduce textarea — render only when the named state is active.
  • include: { in: ['isBug', 'isBilling'] } on the Priority radiogroup — render when any of the listed states is active (OR semantics).
  • exclude: { from: ['isBilling'] } on the Marketing opt-in checkbox — render unless the named state is active.
  • include: { when: '$form.subscribeNewsletter === true' } on the Newsletter frequency radiogroup — an inline reactive expression for a one-off condition where naming a state would be overkill.
import { gui } from '@golemui/gui-shared';
const formDef = [
gui.inputs.radiogroup('issueType', {
label: 'What can we help you with?',
options: [
{ value: 'bug', label: 'Bug report' },
{ value: 'billing', label: 'Billing question' },
{ value: 'feature', label: 'Feature request' },
{ value: 'other', label: 'Other' },
],
direction: 'row',
}),
gui.inputs.textarea('description', {
label: 'Description',
placeholder: 'Tell us what happened…',
}),
// include — only render when isBug is active.
gui.inputs.textarea('reproSteps', {
label: 'Steps to reproduce',
placeholder: '1. Go to …\n2. Click …\n3. Notice …',
include: { in: ['isBug'] },
}),
// include with multiple states — render when ANY is active.
gui.inputs.radiogroup('priority', {
label: 'Priority',
options: [
{ value: 'low', label: 'Low' },
{ value: 'medium', label: 'Medium' },
{ value: 'high', label: 'High' },
],
direction: 'row',
include: { in: ['isBug', 'isBilling'] },
}),
// exclude — render unless isBilling is active.
gui.inputs.checkbox('marketingOk', {
label: 'You may share my email with our partners.',
exclude: { from: ['isBilling'] },
}),
// inline `when` — no named state needed for a one-off condition.
gui.inputs.booleanInput('subscribeNewsletter', {
label: 'Subscribe to the product newsletter',
}),
gui.inputs.radiogroup('newsletterFrequency', {
label: 'How often?',
options: [
{ value: 'weekly', label: 'Weekly' },
{ value: 'monthly', label: 'Monthly' },
],
direction: 'row',
include: { when: '$form.subscribeNewsletter === true' },
}),
gui.actions.button({ label: 'Submit ticket', actionType: 'submit' }),
];
const formConfig = {
states: {
isBug: '$form.issueType === "bug"',
isBilling: '$form.issueType === "billing"',
},
};

Try it: switch the issue type and watch the Steps to reproduce appear/disappear (named-state include), the Priority show for both Bug and Billing (multi-state include), and the marketing opt-in disappear when you pick Billing (named-state exclude). Then toggle the newsletter switch — the frequency radio uses an inline when expression with no named state at all.

  • Features / States — gating, per-state property overrides, composing states.
  • Runtime Functions — when a one-off prop should react to data per-render rather than via a named state.