← All @molecule/* packages · App templates

@molecule/app-forms

Core interface · forms · App (browser) · v1.0.1 · Apache-2.0

Form handling interface for molecule.dev

npm install @molecule/app-forms

npm · Source on GitHub

How it works

@molecule/app-forms is the forms core interface on the app (browser) side: the API your app calls, with no vendor inside.

Bond a provider to choose the implementation.

import { createForm } from '@molecule/app-forms'
// (React apps: prefer the `useForm` hook from `@molecule/app-react` — same options.)

const form = createForm<{ email: string; password: string }>({
  defaultValues: { email: '', password: '' },
  mode: 'onBlur',
})

const email = form.register({
  name: 'email',
  required: t('forms.required', undefined, { defaultValue: 'This field is required' }),
  email: true,
})
// Wire email.value / email.onChange / email.onBlur to your input element.

const onSubmit = form.handleSubmit(async (values) => {
  await http.post('/signup', values) // relative path via the app HTTP client
})

Works with: @molecule/app-bond

Reference

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Form handling interface for molecule.dev.

Provides a unified form management API that works across different form libraries (native, React Hook Form, Formik, etc.).

Quick Start

import { createForm } from '@molecule/app-forms'
// (React apps: prefer the `useForm` hook from `@molecule/app-react` — same options.)

const form = createForm<{ email: string; password: string }>({
  defaultValues: { email: '', password: '' },
  mode: 'onBlur',
})

const email = form.register({
  name: 'email',
  required: t('forms.required', undefined, { defaultValue: 'This field is required' }),
  email: true,
})
// Wire email.value / email.onChange / email.onBlur to your input element.

const onSubmit = form.handleSubmit(async (values) => {
  await http.post('/signup', values) // relative path via the app HTTP client
})

Type

core

Installation

npm install @molecule/app-forms @molecule/app-bond

API

Interfaces

FieldRegistration

Field registration result (for native inputs).

interface FieldRegistration {
  /**
   * Field name.
   */
  name: string

  /**
   * Field value.
   */
  value: unknown

  /**
   * Change handler.
   */
  onChange: (event: { target: { value: unknown; name: string } } | unknown) => void

  /**
   * Blur handler.
   */
  onBlur: () => void

  /**
   * Reference setter (for DOM elements).
   */
  ref?: (element: HTMLElement | null) => void
}

FieldState

Reactive state for a single form field (value, validation errors, touched/dirty flags).

interface FieldState<T = unknown> {
  /**
   * Current field value.
   */
  value: T

  /**
   * Error message (if any).
   */
  error?: string

  /**
   * Whether the field has been touched.
   */
  touched: boolean

  /**
   * Whether the field is dirty (value changed from initial).
   */
  dirty: boolean

  /**
   * Whether the field is valid.
   */
  valid: boolean

  /**
   * Whether the field is currently being validated.
   */
  validating: boolean
}

FormController

Form controller interface.

All form providers must implement this interface.

interface FormController<T extends Record<string, unknown> = Record<string, unknown>> {
  /**
   * Gets the current form state.
   */
  getState(): FormState<T>

  /**
   * Gets the value of a specific field.
   */
  getValue(name: string): unknown
  getValue<K extends keyof T>(name: K): T[K]

  /**
   * Gets all form values.
   */
  getValues(): T

  /**
   * Sets the value of a specific field.
   */
  setValue(
    name: string,
    value: unknown,
    options?: { shouldValidate?: boolean; shouldDirty?: boolean; shouldTouch?: boolean },
  ): void

  /**
   * Sets multiple values at once.
   */
  setValues(values: Partial<T>, options?: { shouldValidate?: boolean }): void

  /**
   * Gets the error for a specific field.
   */
  getError(name: string): string | undefined

  /**
   * Sets the error for a specific field.
   */
  setError(name: string, error: string | undefined): void

  /**
   * Clears the error for a specific field.
   */
  clearError<K extends keyof T>(name: K): void

  /**
   * Clears all errors.
   */
  clearErrors(): void

  /**
   * Gets the field state for a specific field.
   */
  getFieldState<K extends keyof T>(name: K): FieldState<T[K]>

  /**
   * Registers a field for form management.
   */
  register(nameOrOptions: string | RegisterOptions, options?: RegisterOptions): FieldRegistration

  /**
   * Unregisters a field.
   */
  unregister(name: string): void

  /**
   * Validates a specific field.
   */
  validateField<K extends keyof T>(name: K): Promise<boolean>

  /**
   * Validates all fields.
   */
  validate(): Promise<boolean>

  /**
   * Resets the form to initial values.
   */
  reset(values?: Partial<T>): void

  /**
   * Handles form submission.
   */
  handleSubmit(
    onSubmit: (values: T) => void | Promise<void>,
    onError?: (errors: Partial<Record<keyof T, string>>) => void,
  ): (event?: { preventDefault?: () => void }) => Promise<void>

  /**
   * Sets focus to a field.
   */
  setFocus(name: keyof T): void

  /**
   * Subscribes to form state changes.
   */
  subscribe(callback: (state: FormState<T>) => void): () => void

  /**
   * Destroys the form controller.
   */
  destroy(): void
}

FormOptions

Form creation options.

interface FormOptions<T extends Record<string, unknown>> {
  /**
   * Default values.
   */
  defaultValues?: Partial<T>

  /**
   * Validation mode.
   */
  mode?: 'onSubmit' | 'onChange' | 'onBlur' | 'all'

  /**
   * Revalidation mode.
   */
  reValidateMode?: 'onChange' | 'onBlur' | 'onSubmit'

  /**
   * Whether to focus the first error field on submit.
   */
  shouldFocusError?: boolean

  /**
   * Form-level validation function.
   */
  validate?: (
    values: T,
  ) => Partial<Record<keyof T, string>> | Promise<Partial<Record<keyof T, string>>>
}

FormProvider

Form provider interface.

Implementations create form controllers.

interface FormProvider {
  /**
   * Creates a new form controller.
   */
  createForm<T extends Record<string, unknown>>(options: FormOptions<T>): FormController<T>
}

FormState

Aggregate state of an entire form (all field values, errors, and submission status).

interface FormState<T extends Record<string, unknown> = Record<string, unknown>> {
  /**
   * Form values.
   */
  values: T

  /**
   * Field errors.
   */
  errors: Partial<Record<keyof T, string>>

  /**
   * Touched fields.
   */
  touched: Partial<Record<keyof T, boolean>>

  /**
   * Whether the form is valid.
   */
  isValid: boolean

  /**
   * Whether the form is dirty.
   */
  isDirty: boolean

  /**
   * Whether the form is submitting.
   */
  isSubmitting: boolean

  /**
   * Number of times the form has been submitted.
   */
  submitCount: number
}

RegisterOptions

Field registration options.

interface RegisterOptions extends ValidationSchema {
  /**
   * Field name.
   */
  name: string

  /**
   * Default value.
   */
  defaultValue?: unknown

  /**
   * Value transformation on change.
   */
  transform?: (value: unknown) => unknown

  /**
   * Dependencies for validation.
   */
  deps?: string[]
}

ValidationRule

Field validation rule.

interface ValidationRule {
  /**
   * Rule type.
   */
  type:
    'required' | 'min' | 'max' | 'minLength' | 'maxLength' | 'pattern' | 'email' | 'url' | 'custom'

  /**
   * Rule value (for rules like min, max, pattern).
   */
  value?: unknown

  /**
   * Error message when validation fails.
   */
  message: string
}

ValidationSchema

Field validation schema.

interface ValidationSchema {
  /**
   * Whether the field is required.
   */
  required?: boolean | string

  /**
   * Minimum value (for numbers).
   */
  min?: number | { value: number; message: string }

  /**
   * Maximum value (for numbers).
   */
  max?: number | { value: number; message: string }

  /**
   * Minimum length (for strings).
   */
  minLength?: number | { value: number; message: string }

  /**
   * Maximum length (for strings).
   */
  maxLength?: number | { value: number; message: string }

  /**
   * Pattern to match (regex).
   */
  pattern?: RegExp | { value: RegExp; message: string }

  /**
   * Validate as email.
   */
  email?: boolean | string

  /**
   * Validate as URL.
   */
  url?: boolean | string

  /**
   * Custom validation function.
   */
  validate?: (value: unknown) => boolean | string | Promise<boolean | string>
}

Functions

createForm(options)

Creates a new form controller for the given options using the active form provider. The controller manages field values, validation, dirty tracking, and submission.

function createForm(options: FormOptions<T>): FormController<T>
  • options — Form configuration including initial values, validation rules, and submit handler.

Returns: A form controller instance for managing the form lifecycle.

createNativeFormProvider()

Creates a native form provider that manages form state, validation, and field registration without any external library. This is the built-in default used when no form library bond is configured.

function createNativeFormProvider(): FormProvider

Returns: A FormProvider backed by vanilla JavaScript state management.

getProvider()

Retrieves the bonded form provider. If none is bonded, automatically creates and bonds the built-in native form provider.

function getProvider(): FormProvider

Returns: The active form provider.

hasProvider()

Checks whether a form provider has been explicitly bonded.

function hasProvider(): boolean

Returns: true if a form provider is bonded.

setProvider(provider)

Registers a form provider as the active singleton.

function setProvider(provider: FormProvider): void
  • provider — The form provider implementation to bond.

validateValue(value, schema, t)

Validates a value against a validation schema.

When a translation function t is provided, default validation messages will be passed through it for i18n support.

function validateValue(
  value: unknown,
  schema: ValidationSchema,
  t?: TranslateFn,
): Promise<string | undefined>
  • value — The value to validate (string, number, array, or any type accepted by custom validators).
  • schema — The validation rules to check against (required, min/max, pattern, email, etc.).
  • t — Optional i18n translation function for localizing error messages.

Returns: The first validation error message, or undefined if the value passes all checks.

Constants

nativeProvider

Pre-instantiated native form provider, ready to use without calling createNativeFormProvider().

const nativeProvider: FormProvider

Injection Notes

Requirements

Peer dependencies:

  • @molecule/app-bond ^1.0.1

Runtime Dependencies

  • @molecule/app-bond

Build forms with {@link createForm} (or the framework hook), not a direct react-hook-form / formik import — that couples you to one library and breaks the swap.

  • Client validation is UX, NOT a security boundary. {@link validateValue} / client rules give instant feedback, but the SERVER must re-validate every field it receives — a request can skip the form entirely (curl, a tampered client). Never trust a value because the client "validated" it, and never enforce authorization in the form.
  • Keep secrets out of any form state you persist (see @molecule/app-storage), and submit through the HTTP client (@molecule/app-http) with a relative path — never a hardcoded URL.

E2E Tests

Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:

  • Typing into each field updates its displayed value — interact_preview into the field's data-mol-id, then read_preview_ui shows the new value in the input (no stuck/blank input, no lag behind what you typed).
  • Submitting with a required field empty, a malformed email, or an out-of-range number BLOCKS submit and shows that field's own error message beside it — the handler does not run (no navigation, success state, or POST).
  • Fixing the offending field clears its error, and once every field is valid the same submit succeeds — a valid submit passes the correct current values (confirm the request/next screen carries what you typed, not stale or blank data).
  • Errors appear at the configured time, not before: with mode onBlur or onSubmit a pristine, untouched field shows NO error on first render — the error only surfaces after you blur/touch it or attempt submit. No field screams before the user has interacted.
  • Cross-field / form-level rules fire (e.g. a confirm-password mismatch via the form-level validate) and block submit until satisfied, showing the message on the right field.
  • If the form does async validation (e.g. a username-taken check), submit waits for it to resolve before running the handler, any pending/validating indicator shows while it is in flight, and an async failure blocks submit with its message.
  • Resetting the form restores the initial default values in the inputs and clears every error and touched state — a previously-shown error is gone and the submit control returns to its initial enabled/disabled state.
  • Dirty/touched tracking is observable: an unchanged form reads as pristine (no "unsaved changes" affordance; save disabled if the app gates on dirty), editing a field flips it to dirty, and an invalid submit focuses the first error field.

Translations

Translation strings are provided by @molecule/app-locales-forms.