Composing states
States are simple boolean expressions, so you can compose them inline with && and ||. The convention in GolemUI is to define one canonical expression per state — don’t reference state names inside other state expressions; recombine raw $form.* reads instead.
const formConfig = { // ...other formConfig options states: { isAdult: '$form.age >= 18', hasLicense: '$form.licenseNumber != null', canDrive: '$form.age >= 18 && $form.licenseNumber != null', },};{ "states": { "isAdult": "$form.age >= 18", "hasLicense": "$form.licenseNumber != null", "canDrive": "$form.age >= 18 && $form.licenseNumber != null" }, "form": [ /* ... */ ]}Combining states in include
Section titled “Combining states in include”The in form of include is an array; multiple names are combined with AND.
gui.inputs.textInput('drivingTest', { include: { in: ['isAdult', 'hasLicense'] }, // both must be active});{ "kind": "input", "type": "textinput", "path": "drivingTest", "include": { "in": ["isAdult", "hasLicense"] }}For OR semantics, write a composite state with || and reference it by name.
Combining states with prop overrides
Section titled “Combining states with prop overrides”Per-state property overrides apply additively. If two states are active and both override the same prop, the last one wins (declaration order in the states map).
gui.actions.button('save', { label: 'Save', states: { busy: { disabled: true }, loading: { disabled: true, label: 'Loading…' }, },});{ "kind": "action", "type": "button", "uid": "save", "label": "Save", "disabled.busy": true, "disabled.loading": true, "label.loading": "Loading…"}If both busy and loading are active, the widget shows Loading… and is disabled.
Hierarchical state chains with :
Section titled “Hierarchical state chains with :”For richer flows you can chain state names with a colon, e.g. register:adult:canSubmit. The chain isn’t magic — each name is still its own expression evaluated against $form — but the colon convention gives you three things at once:
- A naming hierarchy that reads like a path (
<parent>:<child>:<grandchild>), so related states cluster visually in thestatesmap. - Cascading activeness when used with the
<prop>.<state>suffix syntax: a longer chain wins over a shorter one.label.register:adult:canSubmitoverrideslabel.registerwhenever both apply. - A natural place to encode “stage X is satisfied” flags —
register:adult:canSubmitreads as “the adult branch of the register flow has met its submit prerequisites.”
Here’s a complete sign-in / sign-up flow that uses the chain to gate everything from labels and placeholders to disabled state and click handlers:
const formConfig = { // ...other formConfig options states: { register: '$form.registerMode === true', 'register:tall': '$form.user.height > 180', 'register:minor': '$form.user.age < 18', 'register:minor:canSubmit': '$form.terms === true && $form.parentalApproval === true', 'register:adult': '$form.user.age >= 18', 'register:adult:canSubmit': '$form.terms === true', },};{ "states": { "register": "$form.registerMode === true", "register:tall": "$form.user.height > 180", "register:minor": "$form.user.age < 18", "register:minor:canSubmit": "$form.terms === true && $form.parentalApproval === true", "register:adult": "$form.user.age >= 18", "register:adult:canSubmit": "$form.terms === true" }, "form": [ /* ... */ ]}Read top-to-bottom: there’s a register mode (the user toggled “Register” instead of “Login”). Inside that mode there are two branches — register:adult and register:minor — driven by age. Each branch has its own canSubmit precondition (terms-only for adults, terms + parental approval for minors). And independently of all that, register:tall is a leaf state used to reveal a “Play Basketball” checkbox if the user is taller than 180cm.
Activating chained states
Section titled “Activating chained states”Each entry is just a string key. There’s no implicit dependency between register and register:adult from the engine’s perspective — both have to evaluate true for register:adult to be active, only because both expressions read $form.registerMode (transitively, via $form.user.age). The chain in the key name is for you and the suffix system, not for the evaluator.
That means you can write a deeply-nested key without a parent and it’ll still work — the engine just evaluates the expression. Use the convention to keep your state map readable.
Using chains with prop overrides
Section titled “Using chains with prop overrides”The real payoff is in widget definitions. The more specific suffix wins when multiple match.
gui.actions.button('login', { label: 'Login', disabled: false, states: { register: { label: 'Register', disabled: true }, 'register:minor:canSubmit': { disabled: false }, 'register:adult:canSubmit': { disabled: false }, }, on: { click: 'handleLogin', },});In Programmatic, swapping the click handler per state is best done in your formEvent handler — branch on event.name and on the active states (read them from event.data or directly).
{ "kind": "action", "type": "button", "uid": "login", "label": "Login", "label.register": "Register", "disabled": false, "disabled.register": true, "disabled.register:minor:canSubmit": false, "disabled.register:adult:canSubmit": false, "on": { "click": "handleLogin", "click.register": "handleRegister" }}A single button now drives three flows: a Login click in non-register mode, a disabled Register button while the form isn’t yet valid, and an enabled Register button once the appropriate canSubmit precondition is met.
Including widgets gated by chained states
Section titled “Including widgets gated by chained states”Conditional rendering reads the same way:
gui.inputs.booleanInput('parentalApproval', { label: 'Parental Approval!', include: { in: ['register:minor'] },});{ "kind": "input", "type": "checkbox", "path": "parentalApproval", "label": "Parental Approval!", "include": { "in": ["register:minor"] }}parentalApproval only renders when both registerMode === true and user.age < 18 — the colon name simply documents the relationship.
Display widgets that react to a chain
Section titled “Display widgets that react to a chain”Display widgets pick up the same overrides:
gui.displays.alert('submit-hint', { text: 'Some fields need your attention', level: 'warning', states: { 'register:adult:canSubmit': { text: 'You can Register now', level: 'success', }, 'register:minor:canSubmit': { text: 'You can Register now', level: 'success', }, },});{ "kind": "display", "type": "alert", "props": { "text": "Some fields need your attention", "level": "warning", "text.register:adult:canSubmit": "You can Register now", "level.register:adult:canSubmit": "success", "text.register:minor:canSubmit": "You can Register now", "level.register:minor:canSubmit": "success" }}The alert is amber + warning copy by default; once either the adult or minor canSubmit chain matches, it swaps to a green success message — without you writing a single conditional in your application code.
See also
Section titled “See also”- Properties per state — the suffix syntax for per-state prop overrides.
- Include & Exclude — gating widgets with active-state lists.
- Form Definition API / States — declaring states with
gui.*.