Configuration
This page is the canonical reference for every input the form component accepts. The same surface ships in React, Angular, Lit, Vue, and Vanilla JS — only the binding syntax differs (prop={...} for React, [prop]="..." for Angular, .prop=${...} for Lit, :prop="..." for Vue, element.prop = ... for Vanilla JS). For the framework-typed signatures, see React, Angular, Lit, Vue, Vanilla JS.
You’re looking at the Programmatic view. The Programmatic API leans on formSelectors and formConfig to wire the engine — you’ll see those everywhere below. Switch to the JSON view from the language selector to see the same props described from the JSON form’s perspective.
You’re looking at the JSON view. JSON forms don’t use formSelectors or formConfig — every engine-wiring input below is passed as a standalone component prop. Switch to the Programmatic view to see the gui.* / formConfig equivalents.
Property reference
Section titled “Property reference”Required
Section titled “Required”formDef
Section titled “formDef”A Programmatic array of gui.* shortcuts. Composed with gui.inputs.*, gui.layouts.*, gui.actions.*, gui.displays.* and resolved through the DX pipeline at render time.
import { gui } from '@golemui/gui-shared';
const formDef = [ gui.inputs.textInput('name', { label: 'Name' }), gui.actions.button({ label: 'Submit', actionType: 'submit' }),];Wire it to the form component:
const config = { formDef };// <GuiForm config={config} />// template:// <gui-form [config]="config"></gui-form>protected config = { formDef };// class field:config = { formDef };// render(): html`<gui-form .config=${this.config}></gui-form>`const config = { formDef };// <GuiForm :config="config" />const form = document.getElementById('app-form');form.config = { formDef };See Form Definition API.
A parsed JSON form definition. Loaded from a file, fetched from an API, or authored as a literal in your code.
{ "form": [ { "kind": "input", "type": "textinput", "path": "name", "label": "Name" }, { "kind": "action", "type": "button", "label": "Submit", "on": { "click": "submit" } } ]}Wire it to the form component:
import formDef from './my-form.json';
const config = { formDef };// <GuiForm config={config} />import formDef from './my-form.json';
// template:// <gui-form [config]="config"></gui-form>protected config = { formDef };import formDef from './my-form.json';
// class field:config = { formDef };// render(): html`<gui-form .config=${this.config}></gui-form>`import formDef from './my-form.json';
const config = { formDef };// <GuiForm :config="config" />import formDef from './my-form.json';
const form = document.getElementById('app-form');form.config = { formDef };Top-level keys like "states" and per-widget suffixes ("label.someState") replace the Programmatic formConfig and selector-decorator paths.
Selectors and engine config
Section titled “Selectors and engine config”The Programmatic API has two dedicated wiring objects — formSelectors for behavior decorators, and formConfig for engine knobs. Both are typed and composable.
formSelectors
Section titled “formSelectors”GSL selector decorators that apply by widget type, tag, or uid. Built with gui.selectors.* and resolved against your formDef array at render time. See Selectors.
import { gui } from '@golemui/gui-shared';
const formSelectors = [ gui.selectors.inputs({ suppressAutomaticPlaceholders: true }), gui.selectors.tag('identity').inputs({ autocomplete: 'off' }),];Wire it alongside formDef:
const config = { formDef, formSelectors };// <GuiForm config={config} />// template:// <gui-form [config]="config"></gui-form>protected config = { formDef, formSelectors };// class field:config = { formDef, formSelectors };// render(): html`<gui-form .config=${this.config}></gui-form>`const config = { formDef, formSelectors };// <GuiForm :config="config" />const form = document.getElementById('app-form');form.config = { formDef, formSelectors };formConfig
Section titled “formConfig”DX engine wiring container. Holds widgetLoaders, itemRenderers, dependencies, functions, validateOn, customValidators, states, suppressAutomaticStack. The Programmatic API treats formConfig as the single home for all of these knobs.
const formConfig = { widgetLoaders: { productCard: async () => (await import('./ProductCard')).ProductCard, }, itemRenderers: { countryRenderer }, dependencies: { markdown: { parse: snarkdown } }, functions: { grandTotal: (items = []) => items.reduce((sum, i) => sum + i.price, 0), }, validateOn: 'eager', states: { hasDiscount: '$form.hasDiscountCode === true', },};Wire it alongside formDef:
const config = { formDef, formConfig };// <GuiForm config={config} />// template:// <gui-form [config]="config"></gui-form>protected config = { formDef, formConfig };// class field:config = { formDef, formConfig };// render(): html`<gui-form .config=${this.config}></gui-form>`const config = { formDef, formConfig };// <GuiForm :config="config" />const form = document.getElementById('app-form');form.config = { formDef, formConfig };Putting it all together
Section titled “Putting it all together”A complete component skeleton wiring formDef, formSelectors, and formConfig together — copy this as the starting shape for your own form:
import { gui } from '@golemui/gui-shared';import { GuiForm } from '@golemui/gui-react';
const formDef = [ gui.inputs.textInput('name', { label: 'Name' }), gui.actions.button({ label: 'Submit', actionType: 'submit' }),];const formSelectors = [ gui.selectors.inputs({ suppressAutomaticPlaceholders: true }),];const formConfig = { validateOn: 'eager' as const };
const config = { formDef, formSelectors, formConfig };
export function MyForm() { return <GuiForm config={config} />;}import { Component } from '@angular/core';import { CommonModule } from '@angular/common';import { gui } from '@golemui/gui-shared';import { FormComponent } from '@golemui/gui-angular';
@Component({ imports: [CommonModule, FormComponent], selector: 'app-my-form', template: `<gui-form [config]="config"></gui-form>`,})export class MyForm { protected config = { formDef: [ gui.inputs.textInput('name', { label: 'Name' }), gui.actions.button({ label: 'Submit', actionType: 'submit' }), ], formSelectors: [ gui.selectors.inputs({ suppressAutomaticPlaceholders: true }), ], formConfig: { validateOn: 'eager' as const }, };}import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import { gui } from '@golemui/gui-shared';import '@golemui/gui-lit';
@customElement('my-form')export class MyForm extends LitElement { config = { formDef: [ gui.inputs.textInput('name', { label: 'Name' }), gui.actions.button({ label: 'Submit', actionType: 'submit' }), ], formSelectors: [ gui.selectors.inputs({ suppressAutomaticPlaceholders: true }), ], formConfig: { validateOn: 'eager' as const }, };
override createRenderRoot() { return this; }
override render() { return html`<gui-form .config=${this.config}></gui-form>`; }}<script setup lang="ts">import { gui } from '@golemui/gui-shared';import { GuiForm } from '@golemui/gui-vue';
const config = { formDef: [ gui.inputs.textInput('name', { label: 'Name' }), gui.actions.button({ label: 'Submit', actionType: 'submit' }), ], formSelectors: [ gui.selectors.inputs({ suppressAutomaticPlaceholders: true }), ], formConfig: { validateOn: 'eager' as const },};</script>
<template> <GuiForm :config="config" /></template>import '@golemui/gui-components/index.css';import '@golemui/gui-lit';import { gui } from '@golemui/gui-shared';
const formDef = [ gui.inputs.textInput('name', { label: 'Name' }), gui.actions.button({ label: 'Submit', actionType: 'submit' }),];const formSelectors = [ gui.selectors.inputs({ suppressAutomaticPlaceholders: true }),];const formConfig = { validateOn: 'eager' };
const form = document.getElementById('app-form');form.config = { formDef, formSelectors, formConfig };The JSON path expresses the same behaviors inline on the form itself and as standalone component props — there’s no formSelectors or formConfig wrapper to compose. Each path has its own ergonomics:
- Inline on the JSON —
"states"at the form root,"label.someState"suffixes on widget props for per-state overrides, thevalidatorblock on each input. The form ships with its behavior baked in, which is convenient when the form definition lives in a CMS or backend. configobject props —customWidgetLoaders,itemRenderers,customValidators,dependencies,validateOn. These stay in your application code, which keeps secrets, framework imports, and platform-specific renderers out of the form payload.
A typical JSON form with state-driven behavior:
{ "states": { "hasDiscount": "$form.hasDiscountCode === true" }, "form": [ { "kind": "input", "type": "textinput", "path": "discountCode", "label": "Discount code", "label.hasDiscount": "Your discount code", "include": { "in": ["hasDiscount"] } } ]}Putting it all together
Section titled “Putting it all together”A complete component skeleton wiring a JSON formDef plus the standalone engine-wiring props — copy this as the starting shape for your own JSON-driven form. The widget loader, renderer, and validator below are the same shapes used in the Programmatic example; only the wiring differs.
import { GuiForm } from '@golemui/gui-react';import formDef from './my-form.json';import { CountryRenderer } from './renderers/CountryRenderer';import { allowedNames } from './validators/allowed-names';
const config = { formDef, customWidgetLoaders: { productCard: async () => (await import('./widgets/ProductCard')).ProductCard, }, itemRenderers: { countryRenderer: CountryRenderer }, customValidators: { allowedNames }, validateOn: 'eager' as const,};
export function MyForm() { return <GuiForm config={config} />;}import { Component } from '@angular/core';import { CommonModule } from '@angular/common';import { FormComponent } from '@golemui/gui-angular';import formDef from './my-form.json';import { CountryRenderer } from './renderers/country-renderer.component';import { allowedNames } from './validators/allowed-names';
@Component({ imports: [CommonModule, FormComponent], selector: 'app-my-form', template: `<gui-form [config]="config"></gui-form>`,})export class MyForm { protected config = { formDef, customWidgetLoaders: { productCard: async () => (await import('./widgets/product-card.component')).ProductCardComponent, }, itemRenderers: { countryRenderer: CountryRenderer }, customValidators: { allowedNames }, validateOn: 'eager' as const, };}import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-lit';import formDef from './my-form.json';import { countryRenderer } from './renderers/country-renderer';import { allowedNames } from './validators/allowed-names';
@customElement('my-form')export class MyForm extends LitElement { config = { formDef, customWidgetLoaders: { productCard: async () => (await import('./widgets/product-card.element')).ProductCardElement, }, itemRenderers: { countryRenderer }, customValidators: { allowedNames }, validateOn: 'eager' as const, };
override createRenderRoot() { return this; }
override render() { return html`<gui-form .config=${this.config}></gui-form>`; }}<script setup lang="ts">import { GuiForm } from '@golemui/gui-vue';import formDef from './my-form.json';import CountryRenderer from './renderers/CountryRenderer.vue';import { allowedNames } from './validators/allowed-names';
const config = { formDef, customWidgetLoaders: { productCard: async () => (await import('./widgets/ProductCard.vue')).default, }, itemRenderers: { countryRenderer: CountryRenderer }, customValidators: { allowedNames }, validateOn: 'eager' as const,};</script>
<template> <GuiForm :config="config" /></template>import '@golemui/gui-components/index.css';import '@golemui/gui-lit';import formDef from './my-form.json';import { countryRenderer } from './renderers/country-renderer';import { allowedNames } from './validators/allowed-names';
const form = document.getElementById('app-form');form.config = { formDef, customWidgetLoaders: { productCard: async () => (await import('./widgets/product-card.element')).ProductCardElement, }, itemRenderers: { countryRenderer }, customValidators: { allowedNames }, validateOn: 'eager',};Data inputs
Section titled “Data inputs”The initial form data object. Reads/writes flow through the engine’s reactive store; the value here only seeds the first render.
const formData = { username: 'golemui', email: 'hello@example.com' };const config = { formDef, data: formData };// <GuiForm config={config} />// template:// <gui-form [config]="config"></gui-form>protected config = { formDef, data: { username: 'golemui', email: 'hello@example.com' } };// class field:config = { formDef: this.formDef, data: { username: 'golemui', email: 'hello@example.com' },};// render(): html`<gui-form .config=${this.config}></gui-form>`const formData = { username: 'golemui', email: 'hello@example.com' };const config = { formDef, data: formData };// <GuiForm :config="config" />const formData = { username: 'golemui', email: 'hello@example.com' };const form = document.getElementById('app-form');form.config = { formDef, data: formData };Optional out-of-band metadata you want to thread through events and state. The engine doesn’t interpret it; you read it back from event.data / via custom widgets.
const formMeta = { tenantId: 'acme-co', sessionId: 'abc-123' };const config = { formDef, data: formData, meta: formMeta };// <GuiForm config={config} />// template:// <gui-form [config]="config"></gui-form>protected config = { formDef, meta: { tenantId: 'acme-co', sessionId: 'abc-123' } };// class field:config = { formDef: this.formDef, meta: { tenantId: 'acme-co', sessionId: 'abc-123' },};// render(): html`<gui-form .config=${this.config}></gui-form>`const formMeta = { tenantId: 'acme-co', sessionId: 'abc-123' };const config = { formDef, data: formData, meta: formMeta };// <GuiForm :config="config" />const formMeta = { tenantId: 'acme-co', sessionId: 'abc-123' };const form = document.getElementById('app-form');form.config = { formDef, data: formData, meta: formMeta };Engine wiring
Section titled “Engine wiring”The next group of properties wires the engine — widget loaders, item renderers, custom validators, validation timing, dependencies, and states. They mean exactly the same thing in both paths; what differs is where you put them.
With the Programmatic API, the typical home for these knobs is formConfig — a typed object you nest inside the config prop. The top-level config properties (customWidgetLoaders, itemRenderers, etc.) also accept these knobs directly and merge with whatever you put in formConfig, which is handy when you want to keep environment-specific bits out of the resolved form.
With the JSON path, each knob is a property on the config object — customWidgetLoaders, itemRenderers, customValidators, validateOn, dependencies. They live in your application code rather than in the form payload, which is exactly what you want when the JSON itself is loaded from a CMS, an API, or a backend.
Widget loaders
Section titled “Widget loaders”Map of widget type → async loader. Merged with the framework’s default widget set.
Place it inside formConfig.widgetLoaders:
const formConfig = { widgetLoaders: { productCard: async () => (await import('./ProductCard')).ProductCard, },};// <GuiForm config={config} ... />protected formConfig = { widgetLoaders: { productCard: async () => (await import('./product-card.component')).ProductCardComponent, },};// <gui-form [config]="config"></gui-form>formConfig = { widgetLoaders: { productCard: async () => (await import('./product-card.element')).ProductCardElement, },};// html`<gui-form .config=${this.config}></gui-form>`const formConfig = { widgetLoaders: { productCard: async () => (await import('./ProductCard.vue')).default, },};// <GuiForm :config="config" />const formConfig = { widgetLoaders: { productCard: async () => (await import('./product-card.element')).ProductCardElement, },};// const form = document.getElementById('app-form');// form.config = { formDef, formConfig };Pass it as customWidgetLoaders in the config object:
const customWidgetLoaders = { productCard: async () => (await import('./ProductCard')).ProductCard,};// <GuiForm config={config} ... />protected customWidgetLoaders = { productCard: async () => (await import('./product-card.component')).ProductCardComponent,};// <gui-form [config]="config"></gui-form>customWidgetLoaders = { productCard: async () => (await import('./product-card.element')).ProductCardElement,};// html`<gui-form .config=${this.config}></gui-form>`const customWidgetLoaders = { productCard: async () => (await import('./ProductCard.vue')).default,};// <GuiForm :config="config" />const customWidgetLoaders = { productCard: async () => (await import('./product-card.element')).ProductCardElement,};// const form = document.getElementById('app-form');// form.config = { formDef, customWidgetLoaders };See Features / Widget Loaders.
Item renderers
Section titled “Item renderers”Map of renderer name → framework-specific component. Referenced from a widget’s itemRenderer prop (dropdown, list, radiogroup).
Place it inside formConfig.itemRenderers:
import { CountryRenderer } from './CountryRenderer';
const formConfig = { itemRenderers: { countryRenderer: CountryRenderer },};// <GuiForm config={config} ... />import { CountryRenderer } from './country-renderer.component';
protected formConfig = { itemRenderers: { countryRenderer: CountryRenderer },};// <gui-form [config]="config"></gui-form>import { countryRenderer } from './country-renderer';
formConfig = { itemRenderers: { countryRenderer },};// html`<gui-form .config=${this.config}></gui-form>`import CountryRenderer from './CountryRenderer.vue';
const formConfig = { itemRenderers: { countryRenderer: CountryRenderer },};// <GuiForm :config="config" />import { countryRenderer } from './country-renderer';
const formConfig = { itemRenderers: { countryRenderer },};// const form = document.getElementById('app-form');// form.config = { formDef, formConfig };Pass it as itemRenderers in the config object:
import { CountryRenderer } from './CountryRenderer';
const itemRenderers = { countryRenderer: CountryRenderer };// <GuiForm config={config} ... />import { CountryRenderer } from './country-renderer.component';
protected itemRenderers = { countryRenderer: CountryRenderer };// <gui-form [config]="config"></gui-form>import { countryRenderer } from './country-renderer';
itemRenderers = { countryRenderer };// html`<gui-form .config=${this.config}></gui-form>`import CountryRenderer from './CountryRenderer.vue';
const itemRenderers = { countryRenderer: CountryRenderer };// <GuiForm :config="config" />import { countryRenderer } from './country-renderer';
const itemRenderers = { countryRenderer };// const form = document.getElementById('app-form');// form.config = { formDef, itemRenderers };See Features / Item Renderers.
Custom validators
Section titled “Custom validators”Map of validator name → schema factory. Referenced from a widget’s validator: { type: 'custom', <name>: <config> }. The factory follows the CustomValidatorSchemaFn interface and returns a schema (typically a Zod check):
import type { CustomValidatorSchemaFn } from '@golemui/core';import { z } from 'zod';
export const allowedNames: CustomValidatorSchemaFn = (names: string[]) => z.string().check( z.superRefine((val, ctx) => { if (val && names.includes(val) === false) { ctx.addIssue({ code: 'custom', message: `Name "${val}" not in ${names.map((n) => `"${n}"`).join(', ')}`, input: val, }); } }), );Place it inside formConfig.customValidators:
import { allowedNames } from './validators/allowed-names';
const formConfig = { customValidators: { allowedNames },};// <GuiForm config={config} ... />import { allowedNames } from './validators/allowed-names';
protected formConfig = { customValidators: { allowedNames },};// <gui-form [config]="config"></gui-form>import { allowedNames } from './validators/allowed-names';
formConfig = { customValidators: { allowedNames },};// html`<gui-form .config=${this.config}></gui-form>`import { allowedNames } from './validators/allowed-names';
const formConfig = { customValidators: { allowedNames },};// <GuiForm :config="config" />import { allowedNames } from './validators/allowed-names';
const formConfig = { customValidators: { allowedNames },};// const form = document.getElementById('app-form');// form.config = { formDef, formConfig };Pass it as customValidators in the config object:
import { allowedNames } from './validators/allowed-names';
const customValidators = { allowedNames };// <GuiForm config={config} ... />import { allowedNames } from './validators/allowed-names';
protected customValidators = { allowedNames };// <gui-form [config]="config"></gui-form>import { allowedNames } from './validators/allowed-names';
customValidators = { allowedNames };// html`<gui-form .config=${this.config}></gui-form>`import { allowedNames } from './validators/allowed-names';
const customValidators = { allowedNames };// <GuiForm :config="config" />import { allowedNames } from './validators/allowed-names';
const customValidators = { allowedNames };// const form = document.getElementById('app-form');// form.config = { formDef, customValidators };See Extending GolemUI / Validators.
Validation timing
Section titled “Validation timing”When validation runs. Accepts a single mode or an array combining the latter three:
type ValidateOn = | 'eager' | 'change' | 'blur' | 'submit' | ('change' | 'blur' | 'submit')[];| Mode | Triggers when |
|---|---|
'change' | The user changes the field value |
'blur' | The user leaves the field |
'submit' | A submit event is emitted (all fields are touched first) |
'eager' | Any of the above happens (default) |
Place it inside formConfig.validateOn:
const formConfig = { validateOn: 'change' };// <GuiForm config={config} ... />import type { ValidateOn } from '@golemui/core';
protected formConfig = { validateOn: 'change' as ValidateOn,};// <gui-form [config]="config"></gui-form>import type { ValidateOn } from '@golemui/core';
formConfig = { validateOn: 'change' as ValidateOn,};// html`<gui-form .config=${this.config}></gui-form>`const formConfig = { validateOn: 'change' as const };// <GuiForm :config="config" />const formConfig = { validateOn: 'change' };// const form = document.getElementById('app-form');// form.config = { formDef, formConfig };Pass it as validateOn in the config object:
const config = { formDef, validateOn: 'change' as const };// <GuiForm config={config} />import type { ValidateOn } from '@golemui/core';
// template:// <gui-form [config]="config"></gui-form>protected config = { formDef, validateOn: 'change' as ValidateOn };import type { ValidateOn } from '@golemui/core';
// class field:config = { formDef: this.formDef, validateOn: 'change' as ValidateOn };// render(): html`<gui-form .config=${this.config}></gui-form>`const config = { formDef, validateOn: 'change' as const };// <GuiForm :config="config" />const form = document.getElementById('app-form');form.config = { formDef, validateOn: 'change' };Dependencies
Section titled “Dependencies”External helpers that widgets read at render time. Today, the only built-in widgets that need a dependency are Markdown and Markdown Text — both expect a markdown.parse function for live preview rendering.
Place it inside formConfig.dependencies:
import snarkdown from 'snarkdown';
const formConfig = { dependencies: { markdown: { parse: snarkdown } },};// <GuiForm config={config} ... />import snarkdown from 'snarkdown';
protected formConfig = { dependencies: { markdown: { parse: snarkdown } },};// <gui-form [config]="config"></gui-form>import snarkdown from 'snarkdown';
formConfig = { dependencies: { markdown: { parse: snarkdown } },};// html`<gui-form .config=${this.config}></gui-form>`import snarkdown from 'snarkdown';
const formConfig = { dependencies: { markdown: { parse: snarkdown } },};// <GuiForm :config="config" />import snarkdown from 'snarkdown';
const formConfig = { dependencies: { markdown: { parse: snarkdown } },};// const form = document.getElementById('app-form');// form.config = { formDef, formConfig };Pass it as dependencies in the config object:
import snarkdown from 'snarkdown';
const dependencies = { markdown: { parse: snarkdown } };// <GuiForm config={config} ... />import snarkdown from 'snarkdown';
protected dependencies = { markdown: { parse: snarkdown } };// <gui-form [config]="config"></gui-form>import snarkdown from 'snarkdown';
dependencies = { markdown: { parse: snarkdown } };// html`<gui-form .config=${this.config}></gui-form>`import snarkdown from 'snarkdown';
const dependencies = { markdown: { parse: snarkdown } };// <GuiForm :config="config" />import snarkdown from 'snarkdown';
const dependencies = { markdown: { parse: snarkdown } };// const form = document.getElementById('app-form');// form.config = { formDef, dependencies };Functions
Section titled “Functions”Pure functions callable from reactive expressions under the $fn namespace — they must be synchronous and derive their result only from their arguments.
Place the map inside formConfig.functions:
const formConfig = { functions: { grandTotal: (items = []) => items.reduce((sum, i) => sum + (i.price ?? 0), 0), },};// <GuiForm config={config} ... />protected formConfig = { functions: { grandTotal: (items = []) => items.reduce((sum, i) => sum + (i.price ?? 0), 0), },};// <gui-form [config]="config"></gui-form>formConfig = { functions: { grandTotal: (items = []) => items.reduce((sum, i) => sum + (i.price ?? 0), 0), },};// html`<gui-form .config=${this.config}></gui-form>`const formConfig = { functions: { grandTotal: (items = []) => items.reduce((sum, i) => sum + (i.price ?? 0), 0), },};// <GuiForm :config="config" />const formConfig = { functions: { grandTotal: (items = []) => items.reduce((sum, i) => sum + (i.price ?? 0), 0), },};// const form = document.getElementById('app-form');// form.config = { formDef, formConfig };A functions entry passed directly on the config object is merged over formConfig.functions and wins on a name collision — that lets a shared form bundle ship defaults the host can override.
Functions are not serializable, so they never live in the JSON form — pass them on the config object as a sibling of formDef:
const config = { formDef, functions: { grandTotal: (items = []) => items.reduce((sum, i) => sum + (i.price ?? 0), 0), },};The form definition then refers to them by name:
{ "form": [ { "uid": "total", "kind": "display", "type": "alert", "props": { "text": "Total: {{$fn.grandTotal($form.items)}}" } } ]}The map is captured when the form store is created, so it is not reactive: replacing it on a mounted form has no effect.
See Features / Host Functions.
States
Section titled “States”Named boolean expressions evaluated against $form.
Place the map inside formConfig.states:
const formConfig = { states: { hasDiscount: '$form.hasDiscountCode === true', },};// <GuiForm config={config} ... />protected formConfig = { states: { hasDiscount: '$form.hasDiscountCode === true', },};// <gui-form [config]="config"></gui-form>formConfig = { states: { hasDiscount: '$form.hasDiscountCode === true', },};// html`<gui-form .config=${this.config}></gui-form>`const formConfig = { states: { hasDiscount: '$form.hasDiscountCode === true', },};// <GuiForm :config="config" />const formConfig = { states: { hasDiscount: '$form.hasDiscountCode === true', },};// const form = document.getElementById('app-form');// form.config = { formDef, formConfig };At the JSON form root — the form payload itself carries the state map, so the wiring is the same regardless of framework:
{ "states": { "hasDiscount": "$form.hasDiscountCode === true" }, "form": [...]}See Features / States.
Other inputs
Section titled “Other inputs”The next four inputs apply equally to Programmatic and JSON forms. localization, middlewares, and formName sit inside the config object; autocomplete is a direct component attribute.
localization
Section titled “localization”Translator implementing the I18nTranslator interface from @golemui/core. Resolves Localizable shapes ({ key, default, params }) on labels, placeholders, hints, validator messages, and section labels. Enables RTL automatically when the active language is RTL. See Features / i18n for translator implementations and the Responsive & RTL guide for RTL details.
Include localization in the config object alongside formDef and formConfig:
const config = { formDef, data: formData, formConfig, localization };// <GuiForm config={config} /><gui-form [config]="config"></gui-form>html`<gui-form .config=${this.config}></gui-form>`;const config = { formDef, data: formData, formConfig, localization };// <GuiForm :config="config" />const form = document.getElementById('app-form');form.config = { formDef, data: formData, formConfig, localization };Include localization in the config object alongside the other engine-wiring props:
const config = { formDef, data: formData, customWidgetLoaders, localization };// <GuiForm config={config} /><gui-form [config]="config"></gui-form>html`<gui-form .config=${this.config}></gui-form>`;const config = { formDef, data: formData, customWidgetLoaders, localization };// <GuiForm :config="config" />const form = document.getElementById('app-form');form.config = { formDef, data: formData, customWidgetLoaders, localization };middlewares
Section titled “middlewares”Array of Middleware functions wrapping the form’s reactive store — used for logging, persistence, devtools. See Features / Middlewares.
import { devToolsMiddleware } from '@golemui/core';
const middlewares = [devToolsMiddleware()];const config = { formDef, data: formData, middlewares };// <GuiForm config={config} />import { devToolsMiddleware } from '@golemui/core';
// template:// <gui-form [config]="config"></gui-form>protected config = { formDef, middlewares: [devToolsMiddleware()] };import { devToolsMiddleware } from '@golemui/core';
// class field:config = { formDef: this.formDef, middlewares: [devToolsMiddleware()],};// render(): html`<gui-form .config=${this.config}></gui-form>`import { devToolsMiddleware } from '@golemui/core';
const middlewares = [devToolsMiddleware()];const config = { formDef, data: formData, middlewares };// <GuiForm :config="config" />import { devToolsMiddleware } from '@golemui/core';
const middlewares = [devToolsMiddleware()];const form = document.getElementById('app-form');form.config = { formDef, data: formData, middlewares };autocomplete
Section titled “autocomplete”Sets the underlying <form> element’s autocomplete attribute. Useful to disable browser autofill across the entire form. Individual input widgets also accept an autocomplete prop (via props) for fine-grained control using any W3C autofill token (e.g. 'given-name', 'street-address', 'off').
const config = { formDef };// <GuiForm config={config} autocomplete="off" /><gui-form [config]="config" [autocomplete]="'off'"></gui-form>html`<gui-form .config=${this.config} .autocomplete=${'off'}></gui-form>`;<GuiForm :config="config" autocomplete="off" />const form = document.getElementById('app-form');form.config = { formDef };form.setAttribute('autocomplete', 'off');formName
Section titled “formName”Optional form identifier. Useful when you have several <gui-form> instances on the same page and want to disambiguate them in event payloads or middleware logs.
const config = { formDef, formName: 'checkout' };// <GuiForm config={config} />// template:// <gui-form [config]="config"></gui-form>protected config = { formDef, formName: 'checkout' };// class field:config = { formDef: this.formDef, formName: 'checkout' };// render(): html`<gui-form .config=${this.config}></gui-form>`const config = { formDef, formName: 'checkout' };// <GuiForm :config="config" />const form = document.getElementById('app-form');form.config = { formDef, formName: 'checkout' };Form Callbacks
Section titled “Form Callbacks”formEvent
Section titled “formEvent”Fired when a widget fires a wired event (onClick, onChange, onLoad, onFilter, onBlur). Payload: { name, data, detail }. See Features / Form Events.
import type { FormEvent } from '@golemui/core';
function handleFormEvent(event: FormEvent) { if (event.name === 'submit') { console.log('Submit:', event.data); }}
const config = { formDef, data: formData };// <GuiForm config={config} formEvent={handleFormEvent} />import type { FormEvent } from '@golemui/core';
// template:// <gui-form [config]="config" (formEvent)="onFormEvent($event)"></gui-form>protected onFormEvent(event: FormEvent) { if (event.name === 'submit') { console.log('Submit:', event.data); }}import type { FormEvent } from '@golemui/core';
onFormEvent(event: FormEvent) { if (event.name === 'submit') { console.log('Submit:', event.data); }}
// render():html`<gui-form .config=${this.config} @formEvent=${(e: CustomEvent<FormEvent>) => this.onFormEvent(e.detail)}></gui-form>`;<script setup lang="ts">import type { FormEvent } from '@golemui/core';import { GuiForm } from '@golemui/gui-vue';
const config = { formDef, data: formData };
function onFormEvent(event: FormEvent) { if (event.name === 'submit') { console.log('Submit:', event.data); }}</script>
<template> <GuiForm :config="config" @form-event="onFormEvent" /></template>import '@golemui/gui-components/index.css';import '@golemui/gui-lit';
function onFormEvent(event) { if (event.name === 'submit') { console.log('Submit:', event.data); }}
const form = document.getElementById('app-form');form.config = { formDef, data: formData };form.addEventListener('formEvent', (e) => onFormEvent(e.detail));formHealth
Section titled “formHealth”Reports the form’s operational status whenever it changes. Payload: { status: 'ok' | 'errored', message?, code? }. An errored status covers both initialization problems (a malformed form definition, an undeclared state) and runtime problems (for example a string-interpolation syntax error); while errored, the form is considered non-operational. Wire this callback to disable a submit button, log the error, or trigger your own telemetry. To control what the user sees when the form is errored, use formHealthBoundary.
import type { FormHealth } from '@golemui/core';
function handleFormHealth(health: FormHealth) { console.log('Health:', health.status);}
const config = { formDef, data: formData };// <GuiForm config={config} formHealth={handleFormHealth} />import type { FormHealth } from '@golemui/core';
// template:// <gui-form [config]="config" (formHealth)="onFormHealth($event)"></gui-form>protected onFormHealth(health: FormHealth) { console.log('Health:', health.status);}import type { FormHealth } from '@golemui/core';
onFormHealth(health: FormHealth) { console.log('Health:', health.status);}
// render():html`<gui-form .config=${this.config} @formHealth=${(e: CustomEvent<FormHealth>) => this.onFormHealth(e.detail)}></gui-form>`;<script setup lang="ts">import type { FormHealth } from '@golemui/core';import { GuiForm } from '@golemui/gui-vue';
const config = { formDef, data: formData };
function onFormHealth(health: FormHealth) { console.log('Health:', health.status);}</script>
<template> <GuiForm :config="config" @form-health="onFormHealth" /></template>import '@golemui/gui-components/index.css';import '@golemui/gui-lit';
function onFormHealth(health) { console.log('Health:', health.status);}
const form = document.getElementById('app-form');form.config = { formDef, data: formData };form.addEventListener('formHealth', (e) => onFormHealth(e.detail));formHealthBoundary
Section titled “formHealthBoundary”When a form fails to initialize (a malformed definition, an undeclared state, a double-wrapped gui.* definition) the form would otherwise render as a silent blank. The formHealthBoundary controls what renders instead: it receives the current formHealth and, by default, prepends a red error banner above the form whenever the status is errored. When the status is ok it renders nothing extra, so a corrected definition restores the form in place.
Pass your own boundary to match your app’s design system or to log/escalate the error your own way. The form stays mounted inside the boundary, so recovering from an errored health clears your error UI automatically.
Each framework also exports its default boundary (DefaultFormHealthBoundary, defaultFormHealthBoundary, DefaultFormHealthBoundaryComponent) if you want to extend rather than replace it.
The contract differs slightly per framework:
React is a component receiving { health, children }; render children so the form stays mounted. Type: FormHealthBoundary from @golemui/react.
import type { FormHealthBoundary } from '@golemui/react';
const FormErrorBoundary: FormHealthBoundary = ({ health, children }) => ( <> {health.status === 'errored' && ( <div role="alert" className="form-error"> <strong>This form could not be loaded</strong> <div>{health.message}</div> </div> )} {children} </>);
const config = { formDef, data: formData };// <GuiForm config={config} formHealthBoundary={FormErrorBoundary} />Angular is a standalone component receiving the required health input. It renders only the error UI; the form itself is rendered by the component, so the boundary does not project the form.
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';import type { FormHealth } from '@golemui/core';
@Component({ selector: 'app-form-error-boundary', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, // The boundary renders only the error UI; the form is rendered by <gui-form>. template: ` @if (health().status === 'errored') { <div role="alert" class="form-error"> <strong>This form could not be loaded</strong> <div>{{ message() }}</div> </div> } `,})export class FormErrorBoundaryComponent { health = input.required<FormHealth>(); protected message = computed(() => { const current = this.health(); return current.status === 'errored' ? current.message : ''; });}
// In the host component:// protected readonly formErrorBoundary = FormErrorBoundaryComponent;// template:// <gui-form [config]="config" [formHealthBoundary]="formErrorBoundary"></gui-form>Lit is a function receiving { health, form } and returning a template that includes form. Type: FormHealthBoundary from @golemui/lit.
import type { FormHealthBoundary } from '@golemui/lit';import { html, nothing } from 'lit';
const formErrorBoundary: FormHealthBoundary = ({ health, form }) => html` ${health.status === 'errored' ? html`<div role="alert" class="form-error"> <strong>This form could not be loaded</strong> <div>${health.message}</div> </div>` : nothing} ${form}`;
// render():html`<gui-form .config=${this.config} .formHealthBoundary=${formErrorBoundary}></gui-form>`;Vue is a component receiving the health prop with the form in its default <slot />. Props type: FormHealthBoundaryProps from @golemui/vue.
<script setup lang="ts">import type { FormHealthBoundaryProps } from '@golemui/vue';
defineProps<FormHealthBoundaryProps>();</script>
<template> <div v-if="health.status === 'errored'" role="alert" class="form-error"> <strong>This form could not be loaded</strong> <div>{{ health.message }}</div> </div> <slot /></template><!-- Usage --><script setup lang="ts">import { GuiForm } from '@golemui/gui-vue';import FormErrorBoundary from './FormErrorBoundary.vue';
const config = { formDef, data: formData };</script>
<template> <GuiForm :config="config" :form-health-boundary="FormErrorBoundary" /></template>Vanilla JS is a function receiving { health, form } and returning the content to render, which must include form. Return form on its own when healthy, or an array of nodes (your banner plus form) when errored. Type: FormHealthBoundary from @golemui/lit.
import '@golemui/gui-components/index.css';import '@golemui/gui-lit';
const formErrorBoundary = ({ health, form }) => { if (health.status !== 'errored') return form;
const banner = document.createElement('div'); banner.setAttribute('role', 'alert'); banner.className = 'form-error';
const title = document.createElement('strong'); title.textContent = 'This form could not be loaded';
const message = document.createElement('div'); message.textContent = health.message;
banner.append(title, message); return [banner, form];};
const form = document.getElementById('app-form');form.config = { formDef, data: formData };// Set as a property, not an HTML attribute.form.formHealthBoundary = formErrorBoundary;