Skip to content

How it works

Hand-maintained JSON form definitions are easy to start with and painful to scale: there’s no autocomplete, no type-checking, no refactor support, and every cross-cutting concern (placeholders, validation policy, per-state overrides, tagged behaviors) ends up duplicated across every widget. A growing form inevitably becomes a growing copy-paste exercise.

The Form Definition API solves that by giving you a small, typed namespace — gui — to compose forms with. Each shortcut is a single function call; cross-cutting concerns live in Selectors; metadata you want selectors to target lives in Tags.

Every form definition is just an array of gui.* shortcut calls:

import { gui } from '@golemui/gui-shared';
const formDef = [
gui.inputs.textInput('name', { label: 'Name' }),
gui.actions.button({ label: 'Submit', actionType: 'submit' }),
];

Each shortcut returns a typed object the engine can render. The signatures follow predictable rules per group:

GroupSignatureNotes
gui.inputs.*(path, props?, tags?)path is where the value lives in the form data.
gui.actions.*(props, tags?)No path — actions don’t store data.
gui.displays.*(props, tags?)Read-only, no path, no events.
gui.layouts.*(children, props?, tags?)children is an array of more gui.* calls.
gui.selectorschainable — gui.selectors.<scope>?.<group>(decorator)Behavior layer; matches by type, tag, or uid.

Every shortcut and every individual prop can be a function instead of a static value. The engine re-evaluates these callbacks whenever the form data changes, so a single widget can react to live state — labels that flip with a toggle, validators that swap rules per mode, event handlers that pick a different name based on what the user has typed.

gui.inputs.textInput('user.name', {
label: ({ $form }) =>
$form.registerMode ? 'Choose a username' : 'Sign-in name',
validator: ({ $form }) =>
$form.registerMode
? { type: 'string', required: true, minLength: 3 }
: { type: 'string', required: true },
});

Runtime functions sit between States (named boolean expressions, evaluated by the engine) and Selectors (cross-cutting decorators) — and pick up where both leave off when the dynamic shape of one widget can’t be expressed declaratively.

→ Read the full guide at Runtime Functions.

Read and write form data. Every input takes a string path plus a typed props object that mirrors the widget’s typed WidgetProps (everything in the Widgets Reference) plus DX-only fields (label, validator, defaultValue, tags, size, states, include, exclude, onChange, onLoad, onFilter, onBlur).

gui.inputs.textInput('user.email', {
label: 'Email',
validator: { format: 'email', required: true },
});

→ See the full list at gui.inputs reference.

Buttons and triggers. No path, no value to store — they fire named events through the formEvent callback when clicked.

gui.actions.button({ label: 'Save', actionType: 'submit' });
gui.actions.button({ label: 'Cancel', onClick: () => 'cancelForm' });

→ See the full list at gui.actions reference.

Read-only presentational widgets — alerts, markdown, and a renderer escape hatch for one-off bits of markup.

gui.displays.alert({ text: 'Tip: pick a memorable username.', level: 'info' });
gui.displays.display(({ data }) => html`<p>Hello ${data.name}!</p>`);

→ See the full list at gui.displays reference.

Containers that arrange children. flex for rows/columns sized by size, grid with subgrid alignment, plus tabs and accordion for grouped content.

gui.layouts.flex([
gui.inputs.textInput('first', { label: 'First name' }),
gui.inputs.textInput('last', { label: 'Last name' }),
]);

→ See the full list at gui.layouts reference.

A free-form metadata list on every shortcut. Tags don’t change rendering — they’re hooks for selectors. Group widgets that share a behavioral concern ('identity', 'address', 'compact') and the selector layer can target the whole subset in one place.

gui.inputs.textInput('email', { label: 'Email' }, ['identity']);
gui.inputs.password('password', { label: 'Password' }, ['identity']);

→ Read more at Tags.

The behavior layer. Decorate widgets by type, tag, or uid without touching the form structure. Selectors are how you keep cross-cutting concerns — autocomplete, placeholders, validation timing, per-state overrides — out of the per-widget shortcut calls.

const formSelectors = [
// Disable browser autocomplete on every input tagged 'identity'.
gui.selectors.tag('identity').inputs({ autocomplete: 'off' }),
// Default every text input to a sensible placeholder.
gui.selectors.textInputs({ placeholder: 'Type here…' }),
];

formSelectors lives alongside formDef on the form component. The engine merges decorator overrides with each shortcut’s own props at render time.

→ Read more at Selectors and the gui.selectors reference.

Beyond the building blocks above, the Form Definition API also covers:

  • Custom Widgets — plug your own widgets in via gui.<group>.custom(...).
  • States — named conditions plus include / exclude / when for gating widgets.
  • Events — every event hook (onClick, onChange, onLoad, onFilter, onBlur).

Each page is self-contained — read what you need, skip the rest.