FormEngine reference
FormEngine holds values, errors, visibility, and step state without depending on a UI framework.
Constructor
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.
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);
}| Function | Returns | Description |
|---|---|---|
checkDefinition(input, { validators? }) | DefinitionIssue[] | Never throws. Errors come first. With validators, also reports validator names missing from it |
parseDefinition(input, { validators? }) | FormDefinition | Returns the input typed as FormDefinition, or throws FormDefinitionError with the errors |
validateDefinition | string[] | Deprecated. The messages of checkDefinition(). Removed in 1.0 |
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().
| Property | Type | Description |
|---|---|---|
values | Readonly<Partial<TValues>> | Current draft values, including retained inactive branch answers in routed forms |
errors | Readonly<Record<string, string>> | Errors attached to visible fields |
topLevelErrors | readonly string[] | Errors for missing or hidden fields |
fieldLoading | Readonly<Record<string, boolean>> | Per-field loading state |
stepValidating | boolean | Whether async navigation, including validation and before/after hooks, is running |
submitting | boolean | Whether submission validation, hooks or the submit handler are running |
currentStepIndex | number | Zero-based index in the visible step list (active path when routes are enabled) |
visibleFields | FormField[] | Visible fields on the current step, or all visible fields in a single-step form |
visibleSteps | FormStep[] | Visible steps on the active path; without routes, all steps whose conditions pass |
currentStep | FormStep | null | Current visible step |
isFirstStep / isLastStep | boolean | Current navigation position |
canGoNext | boolean | Whether current-step validation passes |
progress | { current: number; total: number } | One-based progress through visible steps on the active path |
isMultiStep | boolean | Whether the definition uses steps |
Values and validation
| Method | Returns | Description |
|---|---|---|
setValue(key, value) | void | Sets a value, clears its error, and reconciles dependent visibility |
setErrors(errors) | void | Replaces existing errors and navigates to the first visible field with an error |
setFieldLoading(key, loading) | void | Updates loading state for one field |
validate() | Record<string, string> | Validates visible fields on the active path; skipped branch values are excluded |
validateField(key) | string | null | Validates one visible field |
getSubmitValues() | Partial<TValues> | Returns visible field values on the active path, excluding inactive branch answers |
reset(values?) | void | Resets 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:
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.
Navigation
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.
| Method | Returns | Description |
|---|---|---|
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) | boolean | Whether the step is skipped and left out of validation and submitted values |
goToStepWithField(key) | void | Moves 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:
const unsubscribe = engine.subscribe(() => {
render(engine.values, engine.errors);
});
const version = engine.getSnapshot();For renderers, the granular subscriptions avoid updating unrelated fields:
| Method | Description |
|---|---|
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.
Navigation and submission lifecycle
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:
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.
| API | Result |
|---|---|
cancelPendingActions() | Invalidates pending guards without clearing answers. Cannot undo a dispatched submission. |
submitting | True 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.