Skip to content

FormStore

The core class that manages all form state -- values, errors, touched, dirty, and validation. Can be used with or without React.

Import

ts
import { FormStore, createFormStore } from '@ereo/forms'

Factory

ts
function createFormStore<T extends Record<string, any>>(
  config: FormConfig<T>
): FormStore<T>

This is equivalent to new FormStore(config).

Constructor

ts
new FormStore<T>(config: FormConfig<T>)

Creates a new form store. On construction:

  1. Default values are deep-cloned into the baseline
  2. Per-field signals are created for all leaf paths
  3. Status signals (isValid, isDirty, etc.) are initialized
  4. A ValidationEngine is created and config validators are registered
  5. If validateOnMount is true, validation runs asynchronously after construction

Values Proxy

ts
readonly values: T

An ES Proxy that provides natural property access to form values:

ts
const form = createFormStore({
  defaultValues: { user: { name: 'Alice', email: 'alice@example.com' } },
})

// Read
console.log(form.values.user.name) // 'Alice'

// Write
form.values.user.name = 'Bob'
console.log(form.values.user.name) // 'Bob'

The proxy reads from and writes to the underlying signals, so changes are reactive.

Value Access

MethodSignatureDescription
getValue(path: string) => unknownGet value at dot-path
setValue(path: string, value: unknown) => voidSet value, update dirty tracking, sync child/parent signals, trigger validation
setValues(partial: DeepPartial<T>) => voidMerge partial values (sets all leaf paths)
getValues() => TReconstruct full values object from signals
getSignal(path: string) => Signal<unknown>Get the underlying signal for a path (lazy-created)

Error Management

MethodSignatureDescription
getErrors(path: string) => Signal<string[]>Get error signal for a field
setErrors(path: string, errors: string[]) => voidSet field errors, updates isValid
clearErrors(path?: string) => voidClear errors for a field, or all if no path
getFormErrors() => Signal<string[]>Get form-level error signal
setFormErrors(errors: string[]) => voidSet form-level errors
setErrorsWithSource(path: string, errors: string[], source: ErrorSource) => voidSet errors with source tracking. ErrorSource is 'sync' | 'async' | 'schema' | 'server' | 'manual'
clearErrorsBySource(path: string, source: ErrorSource) => voidClear only errors from a specific source (e.g. clear server errors while keeping client-side errors)
getErrorMap(path: string) => Signal<Record<ErrorSource, string[]>>Get errors grouped by source for a field. Returns a signal with { sync, async, schema, server, manual } arrays

Touched / Dirty

MethodSignatureDescription
getTouched(path: string) => booleanWhether field has been blurred
setTouched(path: string, touched?: boolean) => voidMark field as touched (default true)
getDirty(path: string) => booleanWhether field differs from baseline
triggerBlurValidation(path: string) => voidManually trigger blur validation
getFieldValidating(path: string) => Signal<boolean>Whether async validation is running for field

Status Signals

These are Signal instances from @ereo/state. Use useSignal() to subscribe in React, or call .get() / .subscribe() outside React.

SignalTypeDescription
isValidSignal<boolean>true when no errors exist anywhere
isDirtySignal<boolean>true when any field is dirty
isSubmittingSignal<boolean>true during submit
submitStateSignal<FormSubmitState>'idle' | 'submitting' | 'success' | 'error'
submitCountSignal<number>Count of successful submits

Field Registration

MethodSignatureDescription
register(path: string, options?: FieldOptions) => FieldRegistrationRegister a field with options and validators
unregister(path: string) => voidUnregister a field and its validators

Submit

MethodSignatureDescription
handleSubmit(e?: Event) => Promise<void>Validate and call config.onSubmit
submitWith(handler: SubmitHandler<T>, submitId?: string) => Promise<void>Validate and call a custom handler
validate() => Promise<boolean>Run all validation without submitting
trigger(path?: string) => Promise<boolean>Manually trigger validation. Pass a path to validate a single field, or omit to validate all fields. Does not submit the form.

handleSubmit and submitWith:

  • Abort any in-flight submit
  • Touch all registered fields (so errors become visible)
  • Run schema + per-field validation
  • If valid, call the handler with { values, formData, signal }
  • Set submitState to 'success' or 'error'
  • Increment submitCount on success
  • Reset if resetOnSubmit is configured

Reset

MethodSignatureDescription
reset() => voidReset to original defaultValues
resetTo(values: T) => voidReset to arbitrary values, clears all tracking state
resetField(path: string) => voidReset a single field to its default value, clear its errors, and unmark touched/dirty
setBaseline(values: T) => voidUpdate baseline without changing current values (recalculates dirty)
getChanges() => DeepPartial<T>Get only the dirty field paths and their values

Watch

MethodSignatureDescription
watch(path: string, callback: WatchCallback) => () => voidWatch a single path; returns unsubscribe
watchFields(paths: string[], callback: WatchCallback) => () => voidWatch multiple paths
subscribe(callback: () => void) => () => voidSubscribe to any form state change

Serialization

MethodSignatureDescription
toJSON() => TGet current values (same as getValues)
toFormData() => FormDataConvert to FormData (leaf values + Files). Throws if FormData is unavailable (SSR).

Field Refs

MethodSignatureDescription
getFieldRef(path: string) => HTMLElement | nullGet the DOM element for a field
setFieldRef(path: string, el: HTMLElement | null) => voidSet the DOM element reference
getFieldOptions(path: string) => FieldOptions | undefinedGet registered options
getBaseline() => TGet deep-cloned baseline values

Cleanup

ts
dispose(): void

Disposes the validation engine, clears all subscribers, watchers, field refs, and aborts any in-flight submit. Called automatically by useForm on unmount.

Usage Outside React

ts
import { createFormStore, required, email } from '@ereo/forms'

const form = createFormStore({
  defaultValues: { email: '', password: '' },
  validators: {
    email: [required(), email()],
    password: [required()],
  },
})

// Set values
form.setValue('email', 'user@example.com')
form.setValue('password', 'secret123')

// Validate
const valid = await form.validate()

// Read values
console.log(form.getValues()) // { email: 'user@example.com', password: 'secret123' }

// Dirty tracking with baseline
form.setBaseline(form.getValues())
console.log(form.isDirty.get()) // false

// Watch changes
const unsub = form.watch('email', (value, path) => {
  console.log(`${path} changed to ${value}`)
})

// Clean up
unsub()
form.dispose()

Released under the MIT License.