# FormValidatorGroup

**When to use:** Manage a whole form — multiple fields, a single submit handler, validate-on-submit, dirty/touched tracking across the form, and mapping server-side errors back onto the right fields. Build it from a map of named `FormValidator` instances. A form you submit is a group even with one field; a lone `FormValidator` suits a field that is never submitted, such as a filter.

**Keywords:** form submission, whole form validation

**Import:** `import { useFormValidatorGroup, FormValidatorGroup, FormValidator, Validators } from '@ahrowe/ui'`
**Types:** `import type { FormValidatorGroupOptions, InferGroupValues, MergedValues } from '@ahrowe/ui'`

## Recommended: the `useFormValidatorGroup` hook

In function components, create the group with the hook. It instantiates the group once on mount, keeps `onSubmit`/`onValidate` fresh via refs, and re-renders the component whenever any field, the submit state, or validation state changes.

Once on mount means the field map is read on the first render only: the `new FormValidator(...)` calls of later renders are discarded. So `form.Form` keeps its identity (a field inside it keeps focus across re-renders), and a validator that depends on changing state has to read it through a ref.

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

function ProfileForm({ user, onSaved }) {
  const form = useFormValidatorGroup(
    {
      // Each FormValidator's first arg is the DEFAULT (empty) value, not the user's value
      name:  new FormValidator('', [Validators.required(), Validators.minLength(2)]),
      email: new FormValidator('', [Validators.required(), Validators.email()]),
    },
    {
      // initialValues OVERRIDES those defaults with the existing record you're editing
      initialValues: { name: user.name, email: user.email },
      onSubmit: async (values) => {
        // `values` is fully typed: { name: string; email: string }
        await api.updateProfile(values);
        onSaved();
      },
    },
  );

  // `form.Form` wires up onSubmit + noValidate for you
  return (
    <form.Form>
      <Input label="Name"  formValidator={form.group.name} />
      <Input label="Email" type={InputType.Email} formValidator={form.group.email} />
      <ActionButtons
        labels={{ submit: 'Save' }}
        onSubmit={form.submit}
        isSubmitButtonLoading={form.isSubmitting}
        isSubmitButtonDisabled={!form.isDirty}
      />
    </form.Form>
  );
}
```

> The exported `FormValidatorGroup` is the form-aware subclass: every instance has a `Form` component (`form.Form`) that renders `<form onSubmit={form.submit} noValidate>`. You can also call `form.submit(event)` directly from a button.

> **Defaults vs. `initialValues` — the editing pattern.** Each `FormValidator`'s first argument is the field's _empty default_ (`''`, `null`, `false`, …). When you load an existing entity to **edit** it (e.g. an existing user), put those current values in `initialValues` — not in the validator constructors. The group seeds each field from `initialValues` on creation, and that same set becomes the baseline `reset()` restores to. Keep the constructors as empty defaults so a "create new" form (no `initialValues`) and an "edit" form share the exact same field definitions.

```tsx
// Create form — no initialValues, fields stay at their empty defaults
const createForm = useFormValidatorGroup(fields, { onSubmit: create });

// Edit form — same `fields`, current record passed as initialValues
const editForm = useFormValidatorGroup(fields, { initialValues: user, onSubmit: update });
```

## Submit flow

`submit(event?)` does the following:

1. `preventDefault` / `stopPropagation` on the event if present.
2. Sets `isSubmitting` true.
3. On the **first** submit, runs `validate()` (validates every field + marks them touched); on later submits, runs `onValidate` only. "First" means `submitCount` is 0, which a field change and `reset()` both restore.
4. If any field error or a custom validation error exists → marks every field that has an error touched, so each one shows, calls `onSubmitError` with it, increments `submitCount`, and stops (does **not** call `onSubmit`).
5. Otherwise calls `onSubmit(values, group)`. If it throws, the error is passed to `parseError(err, true)` to surface field-level or generic errors.
6. After a successful `onSubmit`, clears dirty/touched on every field.
7. Sets `isSubmitting` false.

By default `onSubmitError` shows the first error as an error **toast** (see `Toast`). Override it via `options.onSubmitError`.

## Clearing the form after a successful submit

Call `reset()` from `onSubmit`, after the work that can fail. A submit straight after it, before anything is typed, still marks the empty required fields (step 4):

```tsx
onSubmit: async (values, form) => {
  await api.invite(values.email);
  form.reset();
},
```

## Field errors and form errors

Where an error is raised decides where the user sees it:

| Raised by | Shown | Use it for |
|-----------|-------|------------|
| A validator in the field's list | On the field, as `showErrors` says | Rules about the field's own value |
| `onValidate` returning `{ id, message }` | Only through `onSubmitError` (a toast by default); no field is marked | Rules across fields, such as matching passwords |
| `onSubmit` throwing an error `errorParser` understands | On the field, marked touched, and through `onSubmitError` | What only the submit can find out: the server, or data outside the form such as a list the value must not already be in |

The default `errorParser` reads an `errors` array of `{ <field name>: message }` off whatever is thrown, so an `Error` works as well as the API's own error object, and lint rules against throwing plain objects stay happy. A synchronous `onSubmit` is caught the same way:

```tsx
onSubmit: (values) => {
  if (members.some((m) => m.email === values.email)) {
    throw Object.assign(new Error('Already on the team'), { errors: [{ email: 'Already on the team' }] });
  }
},
```

Such an error shows on the field and, by default, as a toast too. To show it on the field only, pass `onSubmitError: () => {}`; a custom `errorParser` puts an error on a field by returning its name as the `id`.

`addError` from an `onSubmit` that then returns normally does not show: step 6 clears `touched` on every field after a successful `onSubmit`.

## Options

```ts
interface FormValidatorGroupOptions {
  initialValues?: Record<string, unknown>;     // OVERRIDES the validators' empty defaults with an
                                                // existing record's values (use when editing); also
                                                // becomes the baseline reset() restores to. Not checked
                                                // against the field names: a misspelt key compiles
                                                // and seeds nothing
  onSubmit?:   (values, group) => void | Promise<void>;
  onValidate?: (group) => ValidationError | null | void;  // extra cross-field validation
  errorParser?: (err: unknown) => ValidationError[];      // turn an API error into field errors
  onSubmitError?: (errors: ValidationError[]) => void;    // default: error toast of the first message
  showErrors?: 'touched' | 'dirty' | 'always';             // default for every field that does not set
                                                          // its own; see FormValidator.md
}
```

`onValidate` runs as part of `submit` — return a `{ id, message }` to block submission with a form-level error (e.g. "passwords must match"):

```tsx
useFormValidatorGroup(
  { password: new FormValidator(''), confirm: new FormValidator('') },
  {
    onValidate: (group) =>
      group.group.password.value === group.group.confirm.value
        ? null
        : { id: 'mismatch', message: 'Passwords do not match' },
    onSubmit: async (values) => api.setPassword(values),
  },
);
```

## Instance API

| Member | Signature | Description |
|--------|-----------|-------------|
| `group` | `Record<name, FormValidator>` | The field validators. Access a field via `form.group.<name>`. |
| `Form` | `React.FC<FormHTMLAttributes>` | `<form>` pre-wired with `onSubmit={submit}` + `noValidate`. |
| `submit` | `(event?) => Promise<void>` | Validate then run `onSubmit`. Safe to pass straight to a `<form onSubmit>` or button. |
| `validate` | `() => ValidationError \| null \| void` | Validate every field, mark all touched, run `onValidate`. |
| `getValues` | `() => MergedValues` | Typed snapshot: `initialValues` merged with every field's current value. |
| `set` | `(name, value) => this` | Set one field's value (validates it). Chainable. |
| `setValidator` | `(name, validator) => this` | Swap a field's `FormValidator` instance. |
| `reset` | `(overrides?) => void` | Restore fields to their post-creation values (or `overrides`), clear dirty/touched, fire `onReset`. |
| `resetDirtyAndTouched` | `() => void` | Clear dirty/touched on all fields without changing values. |
| `parseError` | `(err, isInternal?) => void` | Run `errorParser`, push field errors onto matching validators and mark those touched; otherwise a generic error. |
| `getFormError` | `() => ValidationError \| null` | First error across all fields, or `null`. |
| `isSubmitting` | `boolean` (get) | True while `submit` is running. |
| `isDirty` | `boolean` (get) | Any field `dirty`: changed by the user, even if changed back (see FormValidator.md). |
| `wasTouched` | `boolean` (get) | Any field entered and left. |
| `hasErrors` | `boolean` (get) | Any field currently has an error. |
| `initialValues` | `object` (get) | Copy of the initial values. |
| `submitCount` | `number` | How many times `submit` has run (reset to 0 on any field change). |
| `onDebounce` | `(values) => void` | Assignable callback fired 500ms after the last field change. |
| `onReset` | `(group, valuesAfterCreation, initialValues) => void` | Assignable, fired by `reset()`. |

## Mapping API errors back to fields

If your API returns errors shaped like `{ errors: [{ <fieldName>: '<message>' }] }`, the default `errorParser` routes each message to the matching field. A thrown `onSubmit` error is parsed automatically; you can also call it manually:

```tsx
try {
  await api.save(form.getValues());
} catch (err) {
  form.parseError(err); // pushes e.g. an "email already taken" error onto form.group.email
}
```

Pass a custom `errorParser` in the options when your API uses a different shape.

## Class components

For legacy class components, instantiate directly and pass the component instance so the group can call `forceUpdate()`:

```tsx
this.form = new FormValidatorGroup(
  { name: new FormValidator('', [Validators.required()]) },
  this,                              // component instance — enables re-render via forceUpdate
  { onSubmit: (values) => this.save(values) },
);
```

`ValidatableComponent` (also exported) is the base class the built-in form components extend — subclass it when building your own validator-aware input.
</content>
