Skip to content

FormEngine reference ​

FormEngine holds values, errors, visibility, and step state without depending on a UI framework.

Constructor ​

ts
import { FormEngine } from '@formhaus/core';

const engine = new FormEngine(definition, initialValues, {
  validators,
  onStepValidate,
  onBeforeStepChange,
  onAfterStepChange,
  onBeforeSubmit,
  onAfterSubmit,
  onWarning,
});

Both initialValues and the options object are optional. Pass a values type to get typed values and getSubmitValues(): new FormEngine<{ email: string }>(definition). It defaults to Record<string, unknown>. Values belonging to hidden fields or hidden steps are removed during construction. In routed forms, skipped branch answers are retained as draft values; see active-path values.

Definition checks ​

The constructor runs checkDefinition(). It throws a FormDefinitionError when the definition has an issue with severity: 'error', and passes each warning to onWarning. Without onWarning, warnings go to console.warn unless process.env.NODE_ENV is production.

ts
import { checkDefinition, FormDefinitionError, parseDefinition } from '@formhaus/core';

const issues = checkDefinition(json);
const definition = parseDefinition(json);

try {
  new FormEngine(json);
} catch (error) {
  if (error instanceof FormDefinitionError) console.log(error.issues);
}
FunctionReturnsDescription
checkDefinition(input, { validators? })DefinitionIssue[]Never throws. Errors come first. With validators, also reports validator names missing from it
parseDefinition(input, { validators? })FormDefinitionReturns the input typed as FormDefinition, or throws FormDefinitionError with the errors
validateDefinitionstring[]Deprecated. The messages of checkDefinition(). Removed in 1.0
ts
interface DefinitionIssue {
  code: DefinitionIssueCode
  severity: 'error' | 'warning'
  path: string
  message: string
  hint?: string
}

path is a JSON pointer to the node, such as /steps/0/routes/1/show/0/field. message has the form <path>: <what is wrong>. <how to fix>. and hint holds the fix part. The codes are listed in the specification.

State ​

All state properties are read-only. Change values with setValue() and errors with setErrors().

PropertyTypeDescription
valuesReadonly<Partial<TValues>>Current draft values, including retained inactive branch answers in routed forms
errorsReadonly<Record<string, string>>Errors attached to visible fields
topLevelErrorsreadonly string[]Errors for missing or hidden fields
fieldLoadingReadonly<Record<string, boolean>>Per-field loading state
stepValidatingbooleanWhether async navigation, including validation and before/after hooks, is running
submittingbooleanWhether submission validation, hooks or the submit handler are running
currentStepIndexnumberZero-based index in the visible step list (active path when routes are enabled)
visibleFieldsFormField[]Visible fields on the current step, or all visible fields in a single-step form
visibleStepsFormStep[]Visible steps on the active path; without routes, all steps whose conditions pass
currentStepFormStep | nullCurrent visible step
isFirstStep / isLastStepbooleanCurrent navigation position
canGoNextbooleanWhether current-step validation passes
progress{ current: number; total: number }One-based progress through visible steps on the active path
isMultiStepbooleanWhether the definition uses steps

Values and validation ​

MethodReturnsDescription
setValue(key, value)voidSets a value, clears its error, and reconciles dependent visibility
setErrors(errors)voidReplaces existing errors and navigates to the first visible field with an error
setFieldLoading(key, loading)voidUpdates loading state for one field
validate()Record<string, string>Validates visible fields on the active path; skipped branch values are excluded
validateField(key)string | nullValidates one visible field
getSubmitValues()Partial<TValues>Returns visible field values on the active path, excluding inactive branch answers
reset(values?)voidResets values, errors, loading state, validation state, and navigation

WARNING

validate() is the browser check. It runs the same type checks as the server for built-in types, but trusts the values your app put in the engine. On the server, use validateSubmission().

reset() also removes values hidden by the new reset values, while preserving answers in skipped route branches. Independently hidden fields still follow visibility clearing rules. Pending navigation and pre-submit results are discarded. A submission already dispatched cannot be undone.

A custom renderer that receives external errors as a prop can skip redundant setErrors() calls:

ts
import { shouldApplyExternalErrors } from '@formhaus/core/internal';

if (shouldApplyExternalErrors(engine, previousErrors, nextErrors)) engine.setErrors(nextErrors);

It returns false only when nextErrors equals previousErrors and the engine still shows every one of them. Pass undefined as previousErrors for a new engine.

WARNING

@formhaus/core/internal holds the plumbing the React and Vue renderers share: shouldApplyExternalErrors, autoAdvance, fieldDomId, and the Cloud form controller (createCloudController, initialCloudState, focusProblem). It has no semver guarantee and can change in any release. Pin the exact @formhaus/core version if you import it.

next(), back(), skip() and submit() are async, run the lifecycle hooks and resolve to true when the action completed. While one of them is pending, the others resolve to false; call cancelPendingActions() to drop the pending one.

MethodReturnsDescription
next()Promise<boolean>Validates the current step, awaits onStepValidate and the step hooks, then advances
back()Promise<boolean>Runs the step hooks and moves to the previous step of the path without validation
skip(submit?)Promise<boolean>Runs the step hooks with reason: 'skip' and the pre-skip values, then resets the step and advances. When no step follows, it submits through submit without the step
submit(handler)Promise<boolean>Validates every step of the path, runs onBeforeSubmit, awaits handler(values, skippedSteps) and then onAfterSubmit
isStepSkipped(stepId)booleanWhether the step is skipped and left out of validation and submitted values
goToStepWithField(key)voidMoves to the visible step containing a field without lifecycle hooks

See Async step validation for an onStepValidate example.

Subscriptions ​

Use the general subscription when a consumer needs the entire engine state:

ts
const unsubscribe = engine.subscribe(() => {
  render(engine.values, engine.errors);
});

const version = engine.getSnapshot();

For renderers, the granular subscriptions avoid updating unrelated fields:

MethodDescription
subscribeField(key, listener)Runs the listener when that field's value, error, or loading state changes
getFieldSnapshot(key)Returns that field's revision number
subscribeStructure(listener)Runs the listener when visible fields, visible steps, or navigation changes
getStructureSnapshot()Returns the structure revision number

Each subscribe method returns an unsubscribe function. Snapshot methods return stable numbers until the corresponding state changes, so they can be passed to external-store APIs such as React's useSyncExternalStore.

FormEngineOptions additionally accepts onBeforeStepChange, onAfterStepChange, onBeforeSubmit and onAfterSubmit. Before-hooks may return false to cancel, synchronously or asynchronously. Step hooks receive StepChangeContext; submit hooks receive submission values.

These types are exported by @formhaus/core:

ts
interface StepChangeContext {
  fromStepId: string
  toStepId: string
  reason: 'next' | 'back' | 'autoAdvance' | 'skip'
  values: Record<string, unknown>
}

type BeforeStepChangeFn = (context: StepChangeContext) => boolean | void | Promise<boolean | void>
type AfterStepChangeFn = (context: StepChangeContext) => void | Promise<void>
type BeforeSubmitFn = (values: Record<string, unknown>) => boolean | void | Promise<boolean | void>
type SubmitFn = (values: Record<string, unknown>, skippedSteps: string[]) => void | Promise<void>

onAfterSubmit and submit(handler) use SubmitFn. reason is autoAdvance when a renderer advances after an autoAdvance field commits. Callback values are shallow snapshots. For a complete Vue component and React usage, see lifecycle hooks.

APIResult
cancelPendingActions()Invalidates pending guards without clearing answers. Cannot undo a dispatched submission.
submittingTrue while submission and its hooks are pending.

A cancelled, invalid or concurrent action resolves to false. Exceptions reject. FormLifecycleError has committed: true, phase and cause for an after-hook failure: the action already completed. stepValidating remains true throughout navigation hooks. setErrors(), goToStepWithField() and reset() do not run the hooks. See lifecycle ordering, recovery and adapter examples.

Formhaus is MIT licensed. Legal