API

new FormValidator(params)

The single entry point. Constructing it sets novalidate on the form, attaches event listeners, and (by default) starts managing setCustomValidity and aria-invalid on every form control.

import { FormValidator } from '@form-validator-js/core';
new FormValidator({ form, validatorDeclarations, onErrorMessageListChanged, /* ... */ });

Parameters

ParamTypeDefaultNotes
formHTMLFormElementThe form to attach to.
validatorDeclarationsRecord<string, ValidatorDeclaration>Map of validator name → declaration. The keys are the names referenced in data-validation. See Validator declaration.
onErrorMessageListChanged(target: Element, messages: string[], errors: ErrorDetail[]) => voidno-opCalled per affected element when its error list changes. messages and errors are parallel arrays. Render however you like — the engine doesn’t render.
trigger'input' | 'blur' | 'blur-then-input' | 'submit-only''blur-then-input'Default trigger mode. Per-field overrides via data-validation-trigger="…".
manageValiditybooleantrueIf true, calls target.setCustomValidity(messages.join('\n')) for each form control.
reportValidityOnSubmitbooleanfalseIf true, calls form.reportValidity() after preventDefault on an invalid submit.
onPendingChange(element: Element, isPending: boolean) => voidno-op1.1.0 — fires when an async validator starts / finishes for that element. Used to show “Checking…” UI.
onFormPendingChange(isPending: boolean) => voidno-op1.1.0 — fires when the form transitions between “any async in flight” and “all settled”. Used to disable submit buttons.

ErrorDetail is { validatorName: string; subtype: string; message: string; isContextError: boolean } — same length and order as messages. The reserved subtype 'error' signals an async failure (Promise rejected); map it via errorMessage: { error: 'Could not verify, try again' }.

For full details on timing and the platform integration, see the library’s CLAUDE.md.

Validator declaration

type ValidatorDeclaration = {
  init: (target, options) => FormValidatorInitResult;
  validate: (target, data, options) =>
    | FormValidatorValidationResult
    | Promise<FormValidatorValidationResult>;  // 1.1.0 — Promise allowed
  errorMessage?: string | { [subtype: string]: string };
  onError?: (err: unknown) => FormValidatorValidationResult;  // 1.1.0 — custom failure mapping
};

validate’s third argument is { signal: AbortSignal }. Sync validators ignore it; async ones honour it so the engine can cancel stale checks (e.g. when the user types another character before the previous server check resolves). The library detects async by instanceof Promise on the return value — no constructor option to “enable async”.

onError is invoked when the Promise rejects with anything other than an AbortError. Return a FormValidatorValidationResult to customise what the user sees; omit it and the engine emits the reserved 'error' subtype with a default message.

Methods

Static helpers

Trigger modes

Four modes. The default is 'blur-then-input' — validates on focus loss, then continuously on input once a field has been shown an error.

Same form, four trigger modes. Type "ab" in each — feel the difference.

input

Validates on every keystroke.

    blur

    Validates only on focus loss.

      blur-then-input (default)

      Like blur, then eagerly on input once an error has been shown.

        submit-only

        Validates only on submit.

          Async validation

          New in 1.1.0. Return a Promise<FormValidatorValidationResult> from your validator’s validate function and the engine handles the rest:

          See Async username for a runnable example with the AbortSignal wired in.

          The legacy injection pattern — dispatching FormValidator.createValidateEvent({ data: { validatorName: result } }) from outside the validator — still works and is the right tool when the result comes from somewhere the validator can’t reach (a global cache, a worker, a parent component).

          Lifecycle

          The validator stops doing anything after destroy(). The novalidate attribute and data-validation-context="*" set on the form during construction are intentionally left in place — removing them risks clobbering attributes a consumer set independently. Same for aria-invalid and aria-busy (1.1.0, set during in-flight async) on form controls.

          Custom validators

          import {
            FormValidatorInitResult,
            FormValidatorValidationResult,
            type ValidatorDeclaration,
          } from '@form-validator-js/core';
          
          const noWhitespace: ValidatorDeclaration = {
            init: (target) => new FormValidatorInitResult({ observableElementList: [target] }),
            validate: (target) => new FormValidatorValidationResult({
              isValid: !/\s/.test((target as HTMLInputElement).value),
            }),
            errorMessage: 'Cannot contain whitespace',
          };

          For multi-rule validators using validatorSubtypeList, see @form-validator-js/core README.