Skip to main content

Form Editor Reference

Every export documented here comes from @vef-framework-react/form-editor's package entry point (import { ... } from "@vef-framework-react/form-editor") — no internal module paths. For narrative explanations, see Overview, Embedding, Schema, and Linkage.

FormEditor​

ExportShape
FormEditorPre-composed component. Object.assign'd with .Provider, .Shell, .Workspace, .Stage, .Toolbar, .Palette, .Properties — see Embedding.
FormEditorProvider (= FormEditor.Provider)Context root: store + registries + preview runtime. Props: FormEditorProviderProps.
FormEditorShell (= FormEditor.Shell)Chrome frame (layout measurement, device context, drag-drop, shortcuts). Props: FormEditorShellProps (children only).
FormEditorWorkspace (= FormEditor.Workspace)Palette / stage / properties band. Props: { children: ReactNode }.
FormEditorStage (= FormEditor.Stage)Canvas column. No props.
FormEditorToolbar (= FormEditor.Toolbar)Default toolbar. Props: ToolbarProps.

FormEditorProps​

Extends FormEditorProviderProps (below) plus:

PropTypeNotes
brandToolbarBrand ({ name?, tag?, icon? })Toolbar brand mark; defaults to the framework's own name/glyph.
onPublish(schema: FormSchema) => voidRuns the editor's validation plus the host's validate first (errors block, warnings ask for confirmation); the publish button is not rendered when omitted.
publishTextstringDefaults to "发布". Ignored when onPublish is omitted.
publishLoadingbooleanLoading state on the publish button. Ignored when onPublish is omitted.

FormEditorProviderProps​

PropTypeNotes
initialSchemaFormSchemaRead once, on mount. Not validated — see Schema → Validating an imported schema.
registriesPartial<DeviceRegistries>Per-device registry override; an omitted device falls back to registry, then the built-in defaults. Read once, on mount.
registryFormFieldRegistryShorthand for one registry shared by both devices (container chrome included). Read once, on mount.
evaluatorsLinkageEvaluatorsForwarded to the live preview and JSON view — see Linkage. Without a dispatchEffect, the preview shows alert / navigate / api_call as toasts.
dataSourceResolverDataSourceResolverForwarded to the live preview and JSON view — see Embedding.
dataSourceApisDataSourceApi[]The host's option-API catalog; remote option sources lead with a picker over it. Design-time only — see Embedding → Offering an API catalog.
codeSetsCodeSetDescriptor[]The host's data-dictionary catalog; the 数据字典 option source picks from it instead of a typed key. Design-time only — see Embedding → Data dictionaries.
codeSetQueryFnCodeSetQueryFnForwarded to the canvas, the live preview and the JSON view. Default AppContext.codeSetQueryFn — see Embedding → Data dictionaries.
evaluationContextEvaluationContextForwarded to the live preview and JSON view, with the 变量 tab's preview values laid over it — see Linkage.
contextSourcesLinkageContextSource[]The form's system variables: listed in the 变量 tab with preview values and offered by the condition builder — see Linkage. Design-time only.
validateSchemaValidatorThe host's own checks, called only for a schema free of structural errors; its issues join the status-bar diagnostics and the publish gate. Pass a stable function — see Embedding → Host diagnostics.
onSchemaChange(schema: FormSchema) => voidFires whenever the document changes — edits, undo / redo, import, clear, setSchema — but not for the initial schema.
apiRefRef<FormEditorApi>See below.
childrenReactNode—

FormEditorApi​

MemberSignature
getSchema() => FormSchema
setSchema(schema: FormSchema) => void — import semantics: resets the undo timeline and the selection; does not validate
undo / redo() => void
canUndo / canRedo() => boolean
selectNode(nodeId: string | null) => void
setDevice(device: EditorDeviceMode) => void
setViewMode(mode: EditorViewMode) => void

ToolbarBrand / ToolbarProps​

TypeShape
ToolbarBrand{ name?: string; tag?: string; icon?: ReactNode }
ToolbarProps{ brand?: ToolbarBrand; onPublish?: (schema: FormSchema) => void; publishText?: string; publishLoading?: boolean }

FormRenderer​

ExportShape
FormRendererRuntime component. Props: FormRendererProps.

FormRendererProps​

PropTypeNotes
schemaFormSchemaRequired. Not validated.
devicePresentationDevice ("pc" | "mobile")Default "pc". An undesigned device renders an empty state.
defaultValuesRecord<string, unknown>Seeds the root-scope keyed fields — see Embedding → Default values.
disabledbooleanRenders the whole form read-only; a disabled form skips submit validation.
fieldPermissionsRecord<string, FieldPermission>Server-resolved clamp — see Linkage.
evaluatorsLinkageEvaluatorsSee Linkage.
dataSourceResolverDataSourceResolverSee Embedding. Default noopDataSourceResolver.
codeSetQueryFnCodeSetQueryFnLoads every data dictionary the form's codeSet sources read, in one call. Default AppContext.codeSetQueryFn — see Embedding → Data dictionaries.
evaluationContextEvaluationContextSee Linkage. Read slot by slot; variables re-seeds $vars only on a content change.
onSubmit(values: Record<string, unknown>) => void | Promise<void>Receives the filtered payload after validation and beforeSubmit — see Embedding → Submission.
apiRefRef<FormRendererApi>See below.
containOverlaysbooleanDefault false. Mobile-only; pins overlay pickers to the renderer's own box.

FormRendererApi​

MemberSignature
submit() => Promise<void> — same validation pipeline as a schema submit button
reset() => void
getValues() => Record<string, unknown> — the RAW live form state, including hidden fields' seeded defaults and read-only (non-writable-clamped) values
getSubmitValues() => Record<string, unknown> — exactly what the submit pipeline would deliver to onSubmit right now: the live values run through the same payload filtering (effectively-hidden fields dropped, non-writable-clamped keys excluded, subforms recursed per row), without running validation or the submit lifecycle. Added in v2.10.0 alongside the field-permission clamp

Field registries​

ExportSignature
createDefaultRegistry() => FormFieldRegistry — PC registry: the built-in field/container definitions + the antd container chrome
createDefaultMobileRegistry() => FormFieldRegistry — antd-mobile registry: mobileFieldDefinitions + the antd-mobile container chrome
registerDefaults(registry: FormFieldRegistry) => void — applies the built-in field set + PC chrome to an existing registry
DeviceRegistries (type){ pc: FormFieldRegistry; mobile: FormFieldRegistry }

FormFieldRegistry​

MemberSignatureNotes
register(definition: FieldDefinition) => voidKeyed by definition.config.type.
unregister(type: string) => boolean—
get(type: string) => FieldDefinition | undefined—
has(type: string) => boolean—
list() => FieldDefinition[]—
registerPropertyEntry(type: EntryType, component: FC<EntryComponentProps>) => voidDesign-time property-panel renderer.
getPropertyEntry(type: EntryType) => FC<EntryComponentProps> | undefinedFalls back to the panel's statically-imported built-ins when undefined.
setContainerChrome(chrome: ContainerChromeSet) => void—
getContainerChrome() => ContainerChromeSetThrows if none installed.
subscribe(listener: () => void) => () => voidNotified on every mutation.
getRevision() => numberMonotonic; for useSyncExternalStore snapshots.

Property entries​

Built-in property-entry renderer components (each FC<EntryComponentProps>), resolvable by EntryType:

EntryTypeComponent
"text"TextEntry
"key"KeyEntry
"number"NumberEntry
"checkbox"CheckboxEntry
"select"SelectEntry
"icon"IconEntry
"options-editor"OptionsSourceEntry
"linkage-rules"LinkageRulesEntry

EntryComponentProps: { entry: PropertyEntry; field: FormField; schema: FormSchema; onChange: (value: unknown) => void }. PropertyEntryTypeMap is the open interface EntryType is keyed from — augment it (declare module) to register a new entry kind alongside registerPropertyEntry.

Definition builders​

ExportSignature
defineFieldDefinition<TField extends FormField, TValue = unknown>(definition: { config: FieldDefinitionConfig; Component: FC<FieldComponentProps<TField, TValue>>; properties: PropertiesDescriptor }) => FieldDefinition
defineContainerDefinition(definition: { config: FieldDefinitionConfig }) => FieldDefinition — no Component slot (containers render structurally)
definePropertyEntry<TField extends FormField, TValue>(entry: PropertyEntry<TField, TValue>) => PropertyEntry — pins the entry's field/value types at its declaration site

FieldDefinitionConfig​

FieldTypeNotes
typeBlock["type"]Discriminator.
namestringPalette display name.
groupFieldGroup ("basic-input" | "selection" | "date-file" | "container" | "action" | "presentation")Palette category.
keyedbooleanWhether the field needs a unique data-binding key.
iconDynamicIconNameOptional; palette card / properties header icon.
iconUrlstringOptional.
create() => FieldCreateResultFactory returning type-specific defaults; id / key are engine-minted.

FieldComponentProps<TField, TValue>​

FieldType
fieldTField
valueTValue
onChange(value: TValue) => void
errorsstring[] | undefined
domIdstring
disabledboolean | undefined
requiredboolean | undefined
labelPositionLabelPosition | undefined

FieldCreateResult = DistributedOmit<Block, "id" | "key">. FieldDefinition = { config: FieldDefinitionConfig; Component?: FC<FieldComponentProps>; properties: PropertiesDescriptor }.

PropertyEntry / PropertiesDescriptor​

TypeShape
PropertyEntry<TField, TValue>{ id, label, type: EntryType, options?: PropertyEntryOption[], visible?: (field: TField) => boolean, readOnly?, description?, placeholder?, read: (field: TField) => TValue, write: (field: TField, value: TValue) => TField }
PropertyEntryOption{ value: string; label: string }
PropertyGroup{ id: string; label: string; tab?: PropertyTabId; entries: PropertyEntry[] }
PropertiesDescriptorPropertyGroup[]
PropertyTabId"props" | "validation" | "linkage" | "layout"

Editor store​

ExportSignature
FormEditorStoreProviderStore context provider (installed by FormEditorProvider)
useFormEditorStore<T>(selector: (state: FormEditorStoreState) => T) => T
useFormEditorStoreApi() => FormEditorStoreApi (UnboundStore<FormEditorStoreState>)
useCurrentLayer() => PresentationLayer — the active device's presentation
isPaletteVisible(state: Pick<FormEditorStoreState, "viewMode">) => boolean

FormEditorStoreState is the store's full state + actions — most consumers read it through useFormEditorStore(selector) rather than the whole shape. Notable pieces:

MemberTypeNotes
schemaFormSchema—
selectedIdstring | null—
viewModeEditorViewMode ("edit" | "preview" | "json")—
deviceEditorDeviceMode (= PresentationDevice)—
formConfigOpen / formConfigTabboolean / FormConfigTabIdFormConfigTabId = "outline" | "form" | "variables" | "dataSources" | "linkage" — the drawer's 大纲 / 表单 / 变量 / 数据源 / 联动 tabs
past / futureHistoryEntry[]HistoryEntry = { schema: FormSchema; device: EditorDeviceMode }; at most 100 entries, global across devices
setSchema(schema: FormSchema) => voidReplace the document; resets the history and the selection.
clearSchema() => voidReset to an empty schema as an ordinary undoable edit.
selectNode, setViewMode, setDeviceactionsView state — never checkpointed.
setFormConfigOpen, setFormConfigTabactionsOpen / collapse the form-config drawer, or switch it to a tab (opening it); no-ops outside edit mode.
initMobile(layer: PresentationLayer) => voidSeed the mobile presentation (blank or converted) and switch to it; one undo removes it again.
insertField, moveNode, removeNode, removeTab, duplicateNodeactionsStructural edits, each one checkpointed undo step. removeNode / removeTab prune the linkage rules referencing the keys that die with the removed subtree in the same step.
setSpan, setFlex, setColumnWidth, setStackSlotactionsLayout edits, coalesced per node.
editField(args, options?: EditCoalesceOptions), updateBlock(args, options?)actionsEditCoalesceOptions = { coalesceKey?: string } — consecutive edits sharing a key fold into one undo step. Folding is opt-in: omit coalesceKey and the edit takes its own undo entry and ends the current run, so the next edit takes one too. Name what is being edited (the built-in property panel passes field:<id>:<entryId>) to fold a run of keystrokes in one input; leave it off for discrete actions — a select, a switch, an add/remove — that each deserve their own step.
setFieldKeyactionSanitizes, dedupes, and renames every reference to the old key. Deduplication spans both device presentations at the field's own value scope, and the twin bound to the same key at the same scope on the other device is renamed in the same undo step — see Schema → Per-device presentations.
patchSchema(patch: FormSchemaPatch) => voidFormSchemaPatch = Partial<Pick<FormSchema, "dataSources" | "id" | "linkage" | "variables">> & { gap?: GapScale }
renameVariable, removeVariable, removeDataSourceactionsRename/remove + reference cleanup.
undo, redo, canUndo, canRedoactions—

Device and registry hooks​

ExportSignatureNotes
DeviceProvider(props: { device: PresentationDevice; children }) => ReactElementSets the active device for registry resolution.
RegistryProvider(props: { registries: DeviceRegistries; children }) => ReactElement—
useDeviceRegistries() => DeviceRegistriesThrows without an ancestor RegistryProvider.
useFieldRegistry() => FormFieldRegistryThe active device's registry; re-renders on registration changes.
useMobileScopeContainer() => () => HTMLElementgetContainer callback for antd-mobile overlays — the containing phone-frame element when one is present (design-time preview), else document.body (real mobile runtime).

Rendering parts​

ExportProps
FieldShellFieldShellProps: { children, domId, errors?, helperText?, label, labelPosition?, required?, labelledBy? } — shared label/control/helper/error layout. labelledBy ("control" default | "group") picks how the label associates with what it names: "control" emits <label for={domId}>, which needs domId on a labelable element; "group" is for a field that renders a set rather than one control (radio, checkbox-group, code-editor, upload) — for is inert against their wrapper, so the label carries an id and the body becomes a role="group" pointing back at it with aria-required / aria-invalid. Without it such a field has no accessible name at all: a screen reader announces the first option and nothing about the question.
LabelLabelProps: { children, htmlFor?, id?, position?: LabelPosition, required?, title?, "aria-hidden"? } — id lets a role="group" body point back at the label (used instead of htmlFor); title is the native tooltip a truncated side label shows; aria-hidden hides a label whose control already carries the same aria-label

Data sources​

ExportSignature
DataSourceProvider(props: { children, dataSources?: FormDataSource[], resolver?: DataSourceResolver, versions?: Record<string, number> }) => ReactElement — installed by FormEditor and FormRenderer; owns the option cache for its lifetime.
useFieldOptions(source: FieldOptionSource | undefined) => { options: FieldOption[]; loading: boolean; error: boolean } — error is set after a rejected resolve, which keeps the previous options
DataSourceResolver (type){ resolve: (request: ResolvedDataSourceRequest, mapping?: RemoteOptionMapping) => Promise<FieldOption[]> } — a resolver only ever sees evaluated parameters and a normalized mapping
noopDataSourceResolverDataSourceResolver whose resolve always returns [] — the default.
RemoteDataSourceRequest (type){ resource: string; action: string; version?: string; params?: Record<string, DynamicValue> } — the schema shape, carrying bindings
ResolvedDataSourceRequest (type){ resource: string; action: string; version?: string; params?: Record<string, unknown> } — the runtime shape, carrying data; see Embedding → Bound parameters
DynamicValue (type){ kind: "literal"; value: unknown } | { kind: "expression"; source: string } — one declaration shared with LinkageActionValue
isDynamicValue(value: unknown) => value is DynamicValue — narrowing guard for values that crossed a serialization boundary
RemoteOptionMapping (type){ labelKey?: string; valueKey?: string; disabledKey?: string; descriptionKey?: string } — labelKey / valueKey default to "label" / "value"
normalizeOptionMapping(mapping: RemoteOptionMapping | undefined) => RemoteOptionMapping | undefined — drops blank and whitespace-only keys, trims the rest, returns undefined when nothing is left; see Schema → Options and data sources
DataSourceApi (type){ label: string; description?: string; group?: string; resource: string; action: string; version?: string; mapping?: RemoteOptionMapping; params?: DataSourceApiParam[] } — one entry of the designer's API catalog; see Embedding → Offering an API catalog
CodeSetQueryFn (type)QueryFunction<Record<string, DataOption[]>, string[]> — the AppContext.codeSetQueryFn contract: given code set keys, answers each key's entries
CodeSetDescriptor (type){ codeSet: string; name: string; group?: string } — one entry of the designer's data-dictionary catalog
DataSourceApiParam (type){ name: string; label?: string; description?: string; required?: boolean; defaultValue?: DynamicValue } — a parameter an API declares

Mobile​

ExportShape
mobileFieldDefinitionsFieldDefinition[] — antd-mobile leaf renderers (every built-in leaf type except code-editor) plus the structural containers, reusing each PC field's config / properties.
useMobileScopeContainerSee Device and registry hooks.

Schema types​

See Schema for the narrative model. Top-level and supporting types:

ExportShape
FormSchema{ id, version: 2, variables?, dataSources?, linkage?, presentations: { pc: PresentationLayer; mobile?: PresentationLayer } }
PresentationLayer{ gap?: GapScale; children: Block[] }
RuntimeSchemaPresentationLayer & { id: string; variables?, dataSources?, linkage? } — produced by toRuntimeSchema
BlockFormField | ContainerNode
FormFieldFormFieldTypeMap[keyof FormFieldTypeMap] — see Leaf field interfaces
FormFieldTypeMapOpen interface (module-augmentable) mapping type → field interface
KeyedFormFieldExtract<FormField, KeyedNode>
ContainerNodeSectionNode | TabsNode | SubformNode | FlexNode | GridNode (closed set)
CONTAINER_TYPESArray<ContainerNode["type"]>
KeyedNode{ key: string; columnType?: ColumnDataType }
KeyedNodeUnionExtract<Block, KeyedNode>
Validatable{ validate?: { required?, minLength?, maxLength?, min?, max?, pattern?, message? } }
PresentationDevice"pc" | "mobile"
LabelPosition"top" | "left" | "right"
GapScale"small" | "medium" | "large"
GAP_SCALESreadonly GapScale[]
DEFAULT_GAP_SCALE"medium"
ROW_COLS24
FlexSlot{ grow?: number; shrink?: number; basis?: string }
CssLength{ value: number; unit: "px" | "%" }
StackSlot{ width?: CssLength; minWidth?: CssLength; maxWidth?: CssLength; align?: "start" | "center" | "end" }
ColumnDataType"string" | "text" | "integer" | "decimal" | "boolean" | "date" | "datetime" | "json"
FieldOption{ label: string; value: string | number }
FieldOptionSourceStaticOptionSource | RefOptionSource | RemoteOptionSource | CodeSetOptionSource (kind: "static" | "ref" | "remote" | "codeSet")
CodeSetOptionSource{ kind: "codeSet"; codeSet: string } — a host data dictionary by key; see Embedding → Data dictionaries
FormDataSourceStaticDataSource | RemoteDataSource (each { id, name, kind, ... })
FormVariable{ id, name, type: "string" | "number" | "boolean" | "json", defaultValue? } — name is identifier-shaped (^[A-Za-z_]\w*$) and unique; see Schema → Form variables

Leaf field interfaces​

Every leaf field extends the shared base: { id: string; label?: string; labelPosition?: LabelPosition; span?: number; flex?: FlexSlot; columnWidth?: number; stack?: StackSlot; linkage?: FieldLinkage }. Keyed fields additionally carry { key: string; columnType?: ColumnDataType }; Validatable fields additionally carry { validate?: {...} } (see Schema types). Only each interface's own fields are listed below.

InterfacetypeExtendsOwn fields
TextfieldField"textfield"Keyed, Validatableplaceholder?, helperText?, prefixIcon?: DynamicIconName, size?: "small"|"middle"|"large", allowClear?: boolean, maxLength?: number, inputType?: "text"|"password"
CodeEditorField"code-editor"Keyed, Validatableplaceholder?, helperText?, language?: CodeEditorLanguage, minHeight?: number, maxHeight?: number, showLineNumbers?: boolean, showFoldGutter?: boolean, tabSize?: number
NumberField"number"Keyed, Validatableplaceholder?, helperText?, min?: number, max?: number, step?: number, size?, prefix?: string, suffix?: string, precision?: number, controls?: boolean (default shown)
SwitchField"switch"Keyed (not Validatable)helperText?, onText?: string, offText?: string, size?: "default"|"small"
SelectField"select"Keyed, Validatableplaceholder?, helperText?, allowClear?: boolean, size?, showSearch?: boolean, dataSource?: FieldOptionSource
RadioField"radio"Keyed, ValidatablehelperText?, dataSource?: FieldOptionSource, optionType?: "default"|"button", buttonStyle?: "outline"|"solid", direction?: "horizontal"|"vertical" (default "vertical", applied explicitly on both devices, whose native defaults differ)
CheckboxGroupField"checkbox-group"Keyed, ValidatablehelperText?, dataSource?: FieldOptionSource, direction?: "horizontal"|"vertical" (default "vertical")
TextareaField"textarea"Keyed, Validatableplaceholder?, helperText?, rows?: number, autoSize?: boolean, maxLength?: number, showCount?: boolean, size?, allowClear?: boolean
DateField"date"Keyed, Validatableplaceholder?, helperText?, allowClear?: boolean (default true)
DatetimeField"datetime"Keyed, Validatableplaceholder?, helperText?, allowClear?: boolean (default true)
DateRangeField"daterange"Keyed, ValidatablehelperText?, allowClear?: boolean (default true)
UploadField"upload"Keyed, ValidatablehelperText?, maxCount?: number (default 1), public?: boolean, accept?: string, dragger?: boolean — maxCount: 1 makes the value a single storage key, > 1 an array of them; public lands objects under pub/ instead of priv/ and needs the backend's vef.storage.allow_public_uploads
ButtonField"button"(non-keyed)action?: "submit"|"reset"|"button" (default "submit"), buttonType?: "primary"|"default"|"dashed"|"text"|"link" (default "primary"), danger?: boolean, size?, block?: boolean, ghost?: boolean, shape?: "default"|"round", icon?: DynamicIconName
DividerField"divider"(non-keyed)title?: string, titlePlacement?: "left"|"center"|"right" (default "center"), dashed?: boolean
AlertBlockField"alert-block"(non-keyed)message?: string, description?: string, alertType?: "info"|"success"|"warning"|"error", showIcon?: boolean (default true), closable?: boolean, banner?: boolean
ParagraphField"paragraph"(non-keyed)text?: string, textType?: "secondary"|"success"|"warning"|"danger", strong?: boolean, italic?: boolean

Some props have no antd-mobile counterpart, so the mobile renderers ignore them: size on textfield / number / select / textarea / switch, showSearch on select, optionType / buttonStyle on radio, controls on number, allowClear on textarea, ghost on button, dashed on divider, and banner on alert-block. A mobile button is full-width unless block is stored as false.

Container node interfaces​

Every container extends the shared block base (id, span?, flex?, columnWidth?, stack?, linkage?).

InterfacetypeOwn fields
CardSection / CollapseSection (SectionNode)"section"variant: "card" | "collapse", title?: string, gap?: GapScale, children: Block[] — CollapseSection adds defaultCollapsed?: boolean
TabsNode"tabs"gap?: GapScale, tabs: TabItem[] — TabItem = { id: string; label: string; children: Block[] }
StackSubform / TableSubform (SubformNode)"subform"Keyed. variant: "stack" | "table", label?: string, template: Block[], minRows?: number, maxRows?: number, addLabel?: string — StackSubform adds gap?: GapScale; TableSubform adds size?: "small" | "middle" | "large"
FlexNode"flex"children: Block[], direction?: "row" | "column" (default "row"), justify?: FlexJustify, align?: FlexAlign, wrap?: boolean (default false), gap?: number
GridNode"grid"children: Block[], columns?: number (default 2), gap?: number, rowGap?: number (default = gap)

FlexJustify = "start" | "center" | "end" | "between" | "around". FlexAlign = "start" | "center" | "end" | "stretch".

Field and container definition exports​

One FieldDefinition constant per built-in type, registered by createDefaultRegistry/createDefaultMobileRegistry — see Schema → Leaf fields and Schema → Containers for the type / group mapping.

textfieldDefinition, codeEditorDefinition, numberFieldDefinition, switchFieldDefinition, selectFieldDefinition, radioFieldDefinition, checkboxGroupFieldDefinition, textareaFieldDefinition, dateFieldDefinition, datetimeFieldDefinition, dateRangeFieldDefinition, uploadFieldDefinition, buttonDefinition, sectionDefinition, tabsDefinition, subformDefinition, flexDefinition, gridDefinition, dividerDefinition, alertBlockDefinition, paragraphDefinition.

Schema engine functions​

ExportSignaturePurpose
createEmptySchema() => FormSchemaFresh document, empty pc presentation.
createId(prefix: IdPrefix) => stringMint a prefixed cuid2 node id.
generateUniqueKey(layer, baseKey, scope?) => stringScope-unique key allocation.
nextUniqueKey(used: Set<string>, baseKey: string) => stringSame, against a pre-collected set.
sanitizeKey(key: string) => stringStrip non-word characters.
isKeyedNode(node: Block) => node is KeyedNodeUnion—
isKeyedField(field: FormField) => field is KeyedFormField—
isContainerNode(node: Block) => node is ContainerNode—
isLeafField(node: Block) => node is FormField—
walkNodes(layer, visit: (node: Block, scope: readonly string[]) => void) => voidDepth-first, every node. scope is the chain of enclosing subform keys ([] at the root).
walkFields(layer, visit: (field: FormField, scope: readonly string[]) => void) => voidDepth-first, leaf fields only.
findNode / findField(layer, id: string) => Block | undefined / FormField | undefined—
findParentContainer(layer, blockId: string) => ContainerNode | undefined—
insertBlock(layer, block: Block, target: DropTarget) => PresentationLayer—
moveBlock(layer, nodeId: string, target: DropTarget) => PresentationLayer—
cloneBlock(block: Block, allocateKey: ((base: string) => string) | null) => Block—
setSpan(layer, blockId: string, span: number | undefined) => PresentationLayer—
setFlex(layer, blockId: string, flex: FlexSlot | undefined) => PresentationLayer—
editField(layer, fieldId: string, updater: (field: FormField) => FormField) => PresentationLayer—
removeBlock(layer, id: string) => PresentationLayer—
updateNode(layer, id: string, updater: (node: Block) => Block) => PresentationLayer—
currentLayer(schema: FormSchema, device: PresentationDevice) => PresentationLayer—
emptyLayer() => PresentationLayer—
resolvePresentation(schema, device) => PresentationLayer | undefined—
withPresentation(schema, device, layer) => FormSchema—
toRuntimeSchema(schema, device) => RuntimeSchema | undefined—
validateSchema(candidate: unknown, registries: DeviceRegistries) => ValidateSchemaResultValidateSchemaResult = { valid: boolean; schema?: FormSchema; issues: ValidationIssue[] }
inferColumnType(field: FormField) => ColumnDataType—
toColumnDefinitions(schema: FormSchema) => ColumnDefinition[]ColumnDefinition = { key, columnType, maxLength?, precision? }
toFormFieldDefinitions(schema: FormSchema) => FormFieldDefinition[]FormFieldDefinition = { key, kind: FieldKind, label, options? }; FieldKind = "input" | "textarea" | "select" | "number" | "date" | "upload"
convertPresentation(pc: PresentationLayer, rules: ConversionRegistry, target: FormFieldRegistry) => { layer: PresentationLayer; report: ConversionReport }ConversionReport = { convertedCount: number; dropped: Array<{ sourceType, reason, label? }> }
createDefaultConversionRules() => ConversionRegistry—
ConversionRegistry (class).register({ type, convert }): this / .convert(block, ctx): BlockConversionBlockConversion = { status: "converted"; block } | { status: "unwrapped"; blocks } | { status: "dropped"; sourceType; reason }
BlockConversionRule{ type: Block["type"]; convert: (source: Block, ctx: ConversionContext) => BlockConversion }The shape .register() takes — one rule per source block type.
ConversionContext{ target: FormFieldRegistry; convertBlocks: (blocks: Block[]) => Block[] }Passed to convert; convertBlocks recurses a container's body through the same rule set.

DropTarget = { kind: "beside"; anchorId: string; side: "before" \| "after" } | { kind: "container"; containerId: string; tabIndex?: number } | { kind: "append" }.

Linkage​

See Linkage for the full model. Exported surface:

ExportSignature / Shape
validateLinkageSchema(layer: PresentationLayer, formLinkage?: FieldLinkage) => LinkageValidationResult ({ issues: ValidationIssue[] })
resolveLinkageEvaluators(overrides?: LinkageEvaluators) => Required<LinkageEvaluators>
defaultEvaluateExpression(source, values, context?) => boolean
defaultEvaluateAssignExpression(source, values, context?) => unknown
defaultEvaluateScriptAction(source, values, context?) => LinkageScriptResult | void
isStateAction / isEffectAction(action: FieldLinkageAction) => action is StateAction / EffectAction
isFieldEventTriggerKind(kind: LinkageTriggerKind) => boolean
getFieldPermission(permissions: Record<string, FieldPermission> | undefined, key: string) => FieldPermission | undefined — own-property-guarded lookup (a field keyed like an Object prototype member never reads a prototype slot). Added in v2.10.0
isWritableFieldPermission(permission: FieldPermission | undefined) => boolean — true for undefined (no clamp entry), "editable", and "required"; false for "visible" / "hidden". Added in v2.10.0
STATE_ACTION_TYPESreadonly StateActionType[] (show, hide, enable, disable, require, optional, assign, script)
EFFECT_ACTION_TYPESreadonly EffectActionType[] (set_field, set_variable, refresh_data_source, alert, api_call, navigate, submit, reset)
LINKAGE_ACTION_TYPESreadonly LinkageActionType[] — the union of the two above
KEYED_ONLY_ACTIONSReadonlySet<LinkageActionType> (require, optional, assign)
FIELD_TRIGGER_KINDSreadonly LinkageTriggerKind[] (condition, change, focus, blur, click) — legal on a field / container rule
FORM_TRIGGER_KINDSreadonly LinkageTriggerKind[] (condition, load, beforeSubmit, afterSubmit) — legal on a form-scope rule
FIELD_EVENT_TRIGGER_KINDSreadonly LinkageTriggerKind[] (change, focus, blur, click)
LINKAGE_OPERATORSreadonly LinkageOperator[] (eq, ne, gt, lt, gte, lte, contains, empty, notEmpty) — semantics in Linkage → Operators
ALERT_LEVELSreadonly LinkageAlertLevel[] (info, success, warning, error)

Context and dispatch shapes:

TypeShape
EvaluationContext{ variables?, user?, node?, applicant?, globals? }, each Record<string, unknown> — bound as $vars / $user / $node / $applicant / $globals; see Linkage → Host context and system variables
LinkageContextSource{ key: string; label: string } — a system variable: key is the full scope path ("$applicant.departmentId"), stored verbatim as a leaf's sourceKey
EffectDispatchContext{ values: Record<string, unknown>; resolveValue: (value: LinkageActionValue) => unknown; resolveRequest: (request: RemoteDataSourceRequest) => ResolvedDataSourceRequest } — scoped to the firing rule's value scope
LinkageScriptResult{ hidden?: boolean; disabled?: boolean; required?: boolean; value?: unknown } — a script action's partial state patch

Other types: FieldLinkage, FieldLinkageRule, FieldLinkageAction, FieldLinkageDefaults, LinkageTrigger, LinkageTriggerKind, LinkageCondition (LinkageConditionLeaf | LinkageConditionGroup | LinkageConditionExpression), LinkageOperator, StateAction, StateActionType, EffectAction, EffectActionType, LinkageActionType, LinkageActionValue, LinkageAlertLevel, ConditionRetrigger ("edge" | "always"), LinkageEvaluators, FieldPermission ("hidden" | "visible" | "editable" | "required").

Validation​

ExportSignature
formatIssueMessage(code: ValidationIssueCode, params?) => string — every issue message flows through this
SchemaIssue (type){ path: string; code: string; severity: ValidationSeverity; message: string } — any diagnostic the editor shows, its own or a host's
ValidationIssue (type)SchemaIssue & { code: ValidationIssueCode; ruleId?: string } — the editor's own; ruleId names the rule a rule-scoped linkage issue belongs to
SchemaValidator (type)(schema: FormSchema) => readonly SchemaIssue[] — the validate prop; see Embedding → Host diagnostics
ValidationIssueCode (type)Closed union of the codes below
ValidationSeverity (type)"error" | "warning"

message is pre-rendered Chinese product copy; program against code and path, which are stable across locales. An issue's path is either a presentation path (presentations.pc.children[0].key), a node path (node[Field_1].linkage.rules[0]), a form-level path (variables[1].name, dataSources[0].request, schema.linkage.rules[0]), or linkage for a dependency cycle.

Validation issue codes​

Error means the schema is structurally broken and fails validation; warning marks legitimate mid-authoring state (incomplete or dangling configuration) that round-trips through export → import.

CodeSeverityRaised when
schema_not_objecterrorThe candidate is not a plain object.
id_requirederrorThe schema, a node, a tab, or a rule lacks a non-empty string id.
duplicate_iderrorA node or tab id repeats within one presentation.
version_unsupportederrorversion is not 2.
presentation_not_objecterrorpresentations, or one device's layer, is not an object.
variables_invaliderrorvariables is not an array of declarations with unique non-empty ids, unique identifier-shaped names, and a known type.
data_sources_invaliderrordataSources is not an array of entries with unique non-empty ids, a string name, and a valid static / remote payload and mapping.
block_not_objecterrorA block is not a plain object.
children_not_arrayerrorA body — children, tabs, template, or a condition group's children — is not an array.
tabs_emptyerrorA tabs container has no tab.
tab_malformederrorA tab is not an object, or its label is not a string.
type_requirederrorA block has no non-empty type.
unknown_field_typeerrorThe type is not registered on that device's registry.
key_requirederrorA keyed field type, or a subform, has no key.
stray_keyerrorA non-keyed field type carries a key.
stray_key_on_containererrorA layout container (section / tabs / flex / grid) carries a key.
duplicate_keyerrorA key repeats within one value scope.
key_invalid_charseterrorA key holds anything other than letters, digits, and underscores.
span_invaliderrorspan is not an integer in 1..24.
flex_invaliderrorA malformed flex slot, or a flex container's direction / justify / align / wrap out of range.
column_width_invaliderrorcolumnWidth is not a positive number.
stack_invaliderrorA malformed stack slot.
gap_invaliderrorA stack container's gap is not a GapScale, or a flex / grid gap / rowGap is not a non-negative integer.
columns_invaliderrorA grid's columns is not an integer in 1..24.
variant_invaliderrorA section's variant is not "card" / "collapse".
subform_variant_invaliderrorA subform's variant is not "stack" / "table".
title_invaliderrorA section's title is not a string.
display_text_invaliderrorlabel / placeholder / helperText / title / text / message is not a string.
rows_bound_invaliderrorminRows / maxRows is not a non-negative integer, or minRows exceeds maxRows.
subform_table_columnwarningA table subform's template holds a container or a non-keyed block; the renderer skips it.
validate_malformederrorvalidate is not an object.
validate_range_invaliderrorminLength exceeds maxLength, or min exceeds max.
pattern_invaliderrorpattern is not a compilable regular expression.
data_source_invaliderrorA field's dataSource is malformed — an unknown kind, a malformed option list, or a malformed mapping.
data_source_ref_unknownwarningA ref names no declared data source.
request_malformederrorA request is not an object, or its resource / action is not a string.
request_incompletewarningA request's resource and/or action is empty. Reported once per request, naming what is missing — at the request when both are, at the empty one otherwise.
linkage_malformederrorlinkage, or its defaults, is not an object.
rules_not_arrayerrorrules is not an array.
rule_malformederrorA rule is not an object.
trigger_malformederrorA trigger is not an object with a string kind.
trigger_kind_invaliderrorThe trigger kind is not legal at the rule's scope.
trigger_requires_keyed_leaferrorA change trigger on anything but a keyed leaf field.
condition_malformederrorA condition is not an object, or a leaf's sourceKey / an expression's source is not a string.
condition_kind_invaliderrorA condition's kind is not leaf / group / expression.
logic_invaliderrorA group's logic is not "all" / "any".
condition_group_emptywarningA group has no children: under all the rule always applies, under any it never does.
operator_invaliderrorA leaf's operator is not one of LINKAGE_OPERATORS.
source_key_emptywarningA leaf has no source selected.
source_key_unresolvedwarningA leaf's form key (plain or $form.) names no keyed node in the rule's value scope.
source_scope_unknownwarningA leaf's $ root is none of $form / $vars / $user / $node / $applicant / $globals.
source_emptywarningAn expression condition, an expression value, or a script is empty.
actions_not_arrayerroractions is not an array.
actions_emptywarningA rule has no actions.
action_malformederrorAn action is not an object, or its targetKey / variable / dataSourceId / source is not a string.
action_unknown_typeerrorAn action type is not one of LINKAGE_ACTION_TYPES.
state_action_on_edge_triggererrorA state action under an edge trigger.
state_action_on_form_scopeerrorA state action in a form-scope rule.
action_requires_keyed_leaferrorrequire / optional / assign on anything but a keyed leaf field.
defaults_requires_keyed_leaferrordefaults.required on anything but a keyed leaf field.
defaults_on_form_scopeerrorA defaults block on the form-scope linkage.
default_hidden_unreachablewarningA block is hidden by default and none of its own condition rules shows it.
target_key_emptywarningA set_field has no target selected.
target_key_unresolvedwarningA set_field target names no keyed node in the rule's value scope.
variable_emptywarningA set_variable has no variable name.
data_source_id_emptywarningA refresh_data_source action, or a ref option source, has no data source selected.
code_set_emptywarningA codeSet option source has no dictionary selected.
value_malformederrorAn action value is not a literal / expression shape.
alert_level_invaliderrorAn alert's level is not one of ALERT_LEVELS.
retrigger_invaliderrorretrigger is not "edge" / "always".
retrigger_ignoredwarningretrigger on an effect under an edge trigger, where it has no effect.
self_referenceerrorA value-writing rule's condition reads the very field the rule writes.
cycle_detectederrorValue-writing rules form a dependency loop; the message names it.

Chrome​

ContainerChromeSet is the per-device set of presentational shells for structural containers, installed via FormFieldRegistry.setContainerChrome:

MemberProps type
SectionSectionChromeProps = { title?: ReactNode; variant?: "card" | "collapse"; defaultCollapsed?: boolean; children: ReactNode }
TabsTabsChromeProps = { items: ChromeTabItem[]; activeKey?: string; onChange?: (key: string) => void } — ChromeTabItem = { key: string; label: ReactNode; children: ReactNode }
SubformSubformChromeProps = { title?: ReactNode; errors?: string[]; children: ReactNode } — a custom chrome should render errors: a subform mounts one field for the whole array, so its own errors ("at least one row" on a required subform, a table variant's rolled-up row error) have no other slot and would otherwise block submission with nothing on screen. Both built-in chromes render it under the body through the same footer a leaf field uses.
SubformRowSubformRowChromeProps = { removeControl?: ReactNode; children: ReactNode }
AddButtonAddControlChromeProps = { label: ReactNode; onClick: () => void }
RemoveButtonRemoveControlChromeProps = { onClick: () => void }

flex and grid are pure CSS layout and use no chrome — identical on both devices.