Skip to content
Forms, switch product

Authoring pipeline

@golemui/dx is the form authoring pipeline with the widget set factored out: the item walker, selector resolver, widget merger, event wiring, and state expansion that power gui.*, packaged so any widget set can build its own namespace on top. It ships as a single public entry point:

Terminal window
npm i @golemui/dx @golemui/core

The pipeline is four steps: create a registry, register your shortcut types, build a selector chain, and hand all three to createImplementation.

import { createItemTypeRegistry } from '@golemui/dx';
const registry = createItemTypeRegistry();

Each widget set owns its own registry instance, so shortcut names are free to overlap across sets. Three rules to know:

  • Registering the same item type twice in one registry throws.
  • Every type declares a kind (input, action, display, or layout). Umbrella selectors like selectors.inputs(...) match by that kind.
  • A type registered without a kind never matches an umbrella selector.

A shortcut type maps one authoring call (like myset.inputs.textInput(...)) to an engine widget. createShortcutType builds the definition without registering it, so registration stays an explicit act of the module that owns the registry instead of an import side effect that bundlers may drop:

import { createShortcutType, registerShortcutType } from '@golemui/dx';
import type { NonFunctionWidget } from '@golemui/core';
const textFieldType = createShortcutType<TextFieldEntry, TextFieldDecorator>({
itemType: 'MYSET_TEXT_FIELD',
kind: 'input',
entryShape: 'keyed',
mapToWidget: (def) =>
({
uid: def.uid ?? '',
kind: 'input',
type: 'mysetText',
path: def.path ?? '',
...(def.label != null ? { label: def.label } : {}),
props: {},
}) as NonFunctionWidget,
});
registerShortcutType(registry, textFieldType);

entryShape tells the walker what the authoring call produces:

ShapeUsed forEntry looks like
keyedinputs{ key, def }: the data path plus a decorator
bareactions, displaysa decorator or render callback on its own
compoundlayouts{ def, children }: a decorator plus child entries

Optional hooks cover the harder cases: sensibleDefaults (auto labels and placeholders), afterMerge (post-process the merged widget), buildCustomWidget (full control over the emitted widget), and getChildren (extract children from a compound entry).

defineShortcutType(registry, config) does both steps in one call and returns just the selector factories. Prefer the split form when your types live in separate modules from the registry.

Each shortcut type definition also carries two selector factories, gsl and gslByUid, which become the per-type methods of your selector chain in the next step.

import { createSelectors } from '@golemui/dx';
const selectors = createSelectors({
textFields: textFieldType.gsl,
textFieldByUid: textFieldType.gslByUid,
});

createSelectors returns the chain root that becomes myset.selectors. On top of your map it auto-generates the eight umbrella methods (inputs, inputByUid, actions, actionByUid, displays, displayByUid, layouts, layoutByUid, matching by registered kind) and the four scope methods (tag, state, tagsAnd, tagsOr). Rules:

  • A map entry with the same name as an umbrella method replaces the generated one.
  • The scope method names are reserved: using tag, state, tagsAnd, or tagsOr as a map key throws.
  • Scope methods are immutable: each call returns a new chain level, so a scoped chain can be stored and reused.

createImplementation assembles the pieces into the public surface of the widget set:

import { createImplementation, type DxAdapter } from '@golemui/dx';
const adapter: DxAdapter = {
bareItemToWidget: (renderFn) => mysetDisplay(renderFn),
rootEntry: (children) => mysetVerticalStack(children),
};
const facade = {
inputs: { textInput: mysetTextInput /* ... */ },
actions: { button: mysetButton /* ... */ },
displays: { display: mysetDisplay /* ... */ },
layouts: { flex: mysetFlex /* ... */ },
};
export const implementation = createImplementation<
typeof facade,
typeof selectors
>({
name: 'myset',
registry,
facade,
selectors,
adapter,
});
export const myset = implementation.namespace;

The adapter answers two questions the pipeline cannot answer generically: how a bare render function becomes a display widget, and how top-level definitions get wrapped in a root layout (the root must resolve to the reserved flex type).

The result gives you everything downstream code needs:

PropertyWhat it is
namespaceyour facade plus selectors: the object you export as myset
formDefsthe DxService that processes definitions (what gui’s formDefs is)
resolveFormInputthe function the framework form components consume
name, registryechoed back, name is used in error messages

Two sharp edges: the facade must not contain a selectors key (that slot is filled by the chain, and createImplementation throws if it would be shadowed), and when you pass type arguments you must pass all three together, so name the facade and chain types first as shown above.

If your widget set threads a typed dependency object through forms (like an icon pack), declare its shape once and it flows to formConfig.dependencies and the form components:

type MySetDependencies = { icons?: IconPack };
export const implementation = createImplementation<
typeof facade,
typeof selectors,
MySetDependencies
>({ name: 'myset', registry, facade, selectors, adapter });