# FormValidator

**When to use:** Track and validate a single form field's value, dirty/touched state, and errors. Pass the instance to any form-connected component (`Input`, `Textarea`, `NumberInput`, `Dropdown`, `MultiSelect`, `InputDropdown`, `Checkbox`, `Switch`, `RadioGroup`, `DatePicker`, `TimeInput`, `OtpInput`, `IconPicker`, `TemplateEditor`) via its `formValidator` prop — the component then reads the value and surfaces validation errors automatically. A lone `FormValidator` has no `submit` or `reset()`: a form that is submitted (Enter, a submit button, cleared afterwards) is a `FormValidatorGroup`, even with one field.

**Keywords:** form field validation, field validator

**Import:** `import { FormValidator, Validators, useFormValidator } from '@ahrowe/ui'`
**Types:** `import type { ValidatorFunction, ValidationError, ShowErrorsMode, FormValidatorOptions } from '@ahrowe/ui'`

## Basic usage

```tsx
import { FormValidator, Validators, Input } from '@ahrowe/ui';

// Create with a default (empty) value and an optional list of validators
const nameValidator = new FormValidator('', [Validators.required(), Validators.minLength(2)]);

// Hand it to a form component — it tracks value, dirty, touched, and errors internally
<Input label="Name" formValidator={nameValidator} />

// Read state at any time
nameValidator.value                    // current value (typed as T)
nameValidator.hasError()               // true if any error is present
nameValidator.hasError('required')     // true if the error with id 'required' is present
nameValidator.getCurrentErrorMessage() // first error message, or null
nameValidator.touched                  // user has entered and left the field
nameValidator.dirty                    // value has changed since creation
```

> **The first constructor argument is the field's _default (empty)_ value** — what the field holds when nothing has been entered, **not** an existing record's value. Use the type's natural empty value:
>
> | Field | Default value |
> |-------|---------------|
> | string (text, email, …) | `''` |
> | day (`DatePicker`, `'YYYY-MM-DD'`) | `null` |
> | time (`TimeInput`, `'HH:MM'`) | `null` |
> | number | `undefined` (or `0`) |
> | checkbox / boolean | `false` |
> | single-select id | `null` |
>
> To pre-fill fields when **editing an existing entity**, don't seed values here — pass them as `initialValues` on a `FormValidatorGroup` (see [FormValidatorGroup.md](FormValidatorGroup.md)). That keeps the empty value as the validator's true default so `reset()` works correctly.

## When a component shows the error

A form component marks the field red, and shows the message on hover or focus, while
`shouldShowError` is true: the field has an error, and its `showErrors` mode allows it. The mode is
the constructor's third argument:

| `showErrors` | The error shows |
|--------------|-----------------|
| `'touched'` (default) | once the field is `touched`: left (blurred) by the user, a picker's panel closed, or marked by the group's `validate()` on submit |
| `'dirty'` | once the field is `dirty` (changed by the user) or `touched`. Suits password rules; an email field would complain after the first letter |
| `'always'` | from the start. For an edit form loaded with data that is already invalid |

```tsx
const password = new FormValidator('', [Validators.password()], { showErrors: 'dirty' });
```

Once shown, an error follows every edit and disappears as soon as the value is valid. A
`FormValidatorGroup` takes `showErrors` too, as the default for every field that does not set its
own (FormValidatorGroup.md).

## Re-rendering a function component when the value changes

A `FormValidator` lives outside React state, so changing it does **not** by itself re-render a function component. When you read `validator.value` / `validator.hasError()` directly in render (rather than only handing the instance to a form component), subscribe with `useFormValidator` so the component re-renders on every change:

```tsx
import { useMemo } from 'react';
import { FormValidator, Validators, useFormValidator, Input } from '@ahrowe/ui';

function NameField() {
  // Create once — never `new FormValidator(...)` inline in render without memoising
  const validator = useMemo(() => new FormValidator('', [Validators.required()]), []);
  useFormValidator(validator); // re-renders this component on value/error/touched/dirty changes

  return (
    <>
      <Input label="Name" formValidator={validator} />
      <button disabled={validator.hasError()}>Save</button>
    </>
  );
}
```

`useFormValidator(validator)` returns the same instance for convenience and accepts `null`.

## Typing the value

`FormValidator<T>` is generic. Pass the value type for non-string fields, and give each its natural empty value as the default:

```tsx
const ageValidator    = new FormValidator<number | undefined>(undefined, [Validators.required()]);
const dayValidator    = new FormValidator<string | null>(null); // DatePicker: '2026-09-15'
const agreedValidator = new FormValidator<boolean>(false, [Validators.required()]);
```

## Instance API

| Member | Signature | Description |
|--------|-----------|-------------|
| `value` | `T` (get/set) | Current value. The setter assigns **without** validating — prefer `set()` for user input. |
| `set` | `(newValue: T, wasUser = true) => this` | Assign value, mark dirty when `wasUser`, then re-validate. Chainable. |
| `validate` | `() => void` | Clear errors and run every validator against the current value. |
| `touched` | `boolean` (get/set) | User has entered and left the field. |
| `dirty` | `boolean` (get/set) | Set by the first change the user makes, and kept even if the value goes back to what it was. Cleared by the group's `reset()` and after a successful submit. |
| `showErrors` | `ShowErrorsMode \| undefined` (get/set) | When errors show: `'touched'`, `'dirty'` or `'always'`. `undefined` falls back to the group's, then `'touched'`. |
| `shouldShowError` | `boolean` (get) | Has an error that `showErrors` says to show now. What every form component reads. |
| `errors` | `ValidationError[]` (get) | All current errors (`{ id, message }`). |
| `hasError` | `(id?: string) => boolean` | Any error when called with no arg; a specific error by `id` otherwise. |
| `getCurrentErrorMessage` | `() => string \| null` | First error message, or `null`. |
| `addError` | `(error: ValidationError) => void` | Push an error manually (e.g. a server-side error). |
| `removeErrors` | `(ids: string[]) => void` | Remove errors by id. |
| `clearErrors` | `() => void` | Remove all errors. |
| `label` | `string` (get/set) | Field label; form components set this and validators receive it (used by `required()` messages). |
| `validators` | `ValidatorFunction[]` | The active validator list — mutable. |
| `registerOnUpdateListener` | `(cb: () => void) => void` | Subscribe to changes (what `useFormValidator` uses internally). |
| `removeOnUpdateListener` | `(cb: () => void) => void` | Unsubscribe. |

Every setter (`value`, `touched`, `dirty`, `showErrors`, `label`) and every change to the errors notifies the listeners, so a form component holding the validator re-renders on it: setting `touched = true` is enough to show a pending error.

> Validators run once in the constructor, so `hasError()` reflects the initial value immediately.

## Built-in validators

All live as static methods on `Validators`. Each returns a `ValidatorFunction` and accepts an optional custom `errorMsg` as the last argument to override the default message.

```tsx
Validators.required(errorMsg?)             // value must be present (0 counts as present)
Validators.minLength(min, errorMsg?)       // string length >= min (also fails on empty)
Validators.maxLength(max, errorMsg?)       // string length <= max
Validators.email(errorMsg?)                // valid email address
Validators.multiEmail(errorMsg?)           // comma/semicolon-separated list of emails
Validators.mustBeNumber(errorMsg?)         // parses as a number
Validators.oneOf(allowedValues, errorMsg?) // value is in the allowed array
Validators.typeOf(allowedTypes, errorMsg?) // typeof value matches (string or string[])
Validators.date(errorMsg?)                 // a real date: a 'YYYY-MM-DD' day (DatePicker), DD.MM.YYYY, a toISOString() timestamp, or a valid Date
Validators.dateObject(errorMsg?)           // an object with an `isValid` flag (e.g. a date wrapper)
Validators.website(errorMsg?)              // valid URL
Validators.bic(errorMsg?)                  // valid BIC/SWIFT code
Validators.minLetters(min, errorMsg?)      // at least `min` letters (a–z, incl. äöüß)
Validators.minDigits(min, errorMsg?)       // at least `min` digits
Validators.password(errorMsg?)             // min 8 chars, ≥1 letter, ≥1 digit
```

> **Empty values pass most validators.** Except `required()` and `minLength()`, every validator returns `null` for an empty value — so a field is "valid when empty" unless you also add `Validators.required()`. Combine them:
> `new FormValidator('', [Validators.required(), Validators.email()])`.

## Custom validators

A validator is any `(value, label?) => ValidationError | null` — return `null` when valid, or `{ id, message }` when not:

```tsx
import type { ValidatorFunction } from '@ahrowe/ui';

const mustBeEven: ValidatorFunction = (value) =>
  Number(value) % 2 === 0 ? null : { id: 'even', message: 'Must be an even number' };

const validator = new FormValidator<number>(0, [Validators.required(), mustBeEven]);
```

The `id` you choose is what `hasError('even')` and `removeErrors(['even'])` match against.

## Reading server-side errors back into a field

```tsx
validator.addError({ id: 'taken', message: 'This email is already registered' });
// later, once the user edits the field again:
validator.removeErrors(['taken']);
```

When validating a whole form at once, prefer `FormValidatorGroup.parseError()` — see `FormValidatorGroup`.
</content>
</invoke>
