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', },};Expression syntax
Section titled “Expression syntax”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 thefunctionsconfig, called as$fn.name(...).
Expressions can reference any path in the form data.
What expressions can do
Section titled “What expressions can do”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,toLowerCaseandtrim. Arrow functions work as their callbacks:$form.items.some(i => i.price > 100). - No writes. Assignments (
=,+=and the rest),++,--anddeleteare rejected, and so are the methods that change a value in place: those of arrays (push,pop,splice,sort,reverseand the like), of Maps and Sets (set,add,delete,clear) and of Dates (setDate,setFullYearand the other setters). The check sees only the method’s name, so an immutable library’s method with the same name, such as dayjs’sadd, is rejected too: call it in a host function. - No code.
new, tagged templates, the namesconstructor,prototypeand__proto__, and calls on anything other than a method or$fnare 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.
Where to use states
Section titled “Where to use states”- In
include/excluderules: gate widgets. - In per-state property overrides on shortcuts: change props when active.
- In
gui.selectors.state(...)chains: scope a selector to one state.
Gating with include / exclude / when
Section titled “Gating with include / exclude / when”Two properties on every shortcut control whether a widget renders:
| Property | Shape |
|---|---|
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
includewithin— the condition has a name and is used in multiple places (gating, prop overrides, selectors).includewithwhen— 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.
See also
Section titled “See also”- 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.