# form

Headless form/field orchestrator for an existing `<form>` (progressive
enhancement). It discovers fields, validates them, wires ARIA
(`aria-invalid`, `aria-describedby`), reflects `data-state="valid|invalid"` on
each field wrapper and emits typed events — it applies no visual styles.

> It does not own the inputs: it reads native `input`/`select`/`textarea` values
> (and works alongside other `@42/core` controls). On a `<form>` root it sets
> `noValidate` and `preventDefault`s submit so you handle it via `form:submit`.

## Markup

```html
<form data-c42-form>
  <div data-c42-field>
    <label for="email">Email</label>
    <input id="email" name="email" type="email" data-c42-validate="required email" />
    <span data-c42-field-error hidden></span>
  </div>

  <div data-c42-field>
    <label for="pwd">Password</label>
    <input id="pwd" name="pwd" type="password"
           data-c42-validate="required" data-c42-minlength="8" />
    <span data-c42-field-error hidden></span>
  </div>

  <div data-c42-field>
    <label><input type="checkbox" name="tos" value="yes" data-c42-validate="required" /> Accept terms</label>
    <span data-c42-field-error hidden></span>
  </div>

  <button type="submit">Sign up</button>
</form>
```

### Markup parts

| Selector | Role |
|----------|------|
| `[data-c42-form]` | Root (usually a `<form>`) |
| `[data-c42-field]` | Field wrapper; gets `data-state="valid\|invalid"` |
| `[data-c42-field-control]` | Optional explicit control; otherwise the first native `input`/`select`/`textarea` is used |
| `[data-c42-field-error]` | Error message slot; linked via `aria-describedby`, toggled `hidden` |

The field **name** is the control's `name` attribute (fallback: the wrapper's
`data-c42-field` value, useful to label radio groups).

## Validation rules

Declared on the control via `data-c42-validate` (space-separated):

| Rule | Fails when… |
|------|-------------|
| `required` | value is empty / no radio checked / checkbox unchecked |
| `email` | value is not a valid email |
| `url` | value is not an `http(s)` URL |
| `number` | value is not numeric |
| `integer` | value is not a whole number |

Constraints (read from `data-c42-*` first, then the native attribute):

| Constraint | Attribute(s) |
|------------|--------------|
| min length | `data-c42-minlength` / `minlength` |
| max length | `data-c42-maxlength` / `maxlength` |
| min value | `data-c42-min` / `min` |
| max value | `data-c42-max` / `max` |
| pattern | `data-c42-pattern` / `pattern` (anchored to the full value) |

Empty optional fields skip all rules except `required`.

### Messages

Defaults are English. Override globally with `options.messages`, or per field
with `data-c42-error-<rule>`:

```html
<input name="pwd" data-c42-validate="required" data-c42-minlength="8"
       data-c42-error-minlength="Use at least 8 characters." />
```

## Options

```ts
import { Form } from '@42/core/form';

new Form(root, {
  // When to validate live, in addition to submit. Default 'submit'.
  // After the first submit attempt, fields re-validate on 'input' to clear errors.
  mode: 'blur', // 'submit' | 'blur' | 'change' | 'input'

  // Custom per-field validators, run after the built-in rules pass.
  // Return an error string, or null/undefined when valid.
  validators: {
    pwd: (value, values) => (value === values.confirm ? null : 'Passwords differ'),
  },

  // Override default built-in messages globally.
  messages: { required: 'This field is mandatory.' },
});
```

## Methods

| Method | Description |
|--------|-------------|
| `validate()` | Validate every field; returns `true` if all pass |
| `validateField(name)` | Validate one field; returns its error or `null` |
| `submit()` | Validate all, emit `form:submit` (valid) or `form:invalid`, focus first invalid; returns validity |
| `getValues()` | `{ [name]: value }` for every field |
| `setValues(map)` | Set values (handles inputs, checkboxes, radios) |
| `setError(name, msg)` | Set a server-side error on a field |
| `clearErrors()` | Clear all error state (keeps values) |
| `reset()` | Reset the form, clear errors/touched, emit `form:reset` |
| `getState()` | `{ values, errors, valid }` |
| `on(event, handler)` | Subscribe; returns an unsubscribe fn |
| `destroy()` | Remove all listeners |

## Events

| Event | Detail |
|-------|--------|
| `form:submit` | `{ values }` — only when valid; native submit is prevented |
| `form:invalid` | `{ errors }` — map of `name` → first error message |
| `form:change` | `{ name, value, values }` — on field input/change |
| `form:reset` | `{}` |

## Recipes

### Server-side errors

```ts
const form = new Form(root);
form.on('form:submit', async (e) => {
  const res = await api.signup(e.detail.values);
  if (res.emailTaken) form.setError('email', 'That email is already registered.');
});
```

### Cross-field validation (confirm password)

```ts
new Form(root, {
  validators: {
    confirm: (value, values) => (value === values.pwd ? null : 'Passwords must match.'),
  },
});
```

## Notes

- Radio groups: put `data-c42-validate="required"` on (at least) the first radio;
  the group is treated as one field whose value is the checked radio's `value`.
- Checkboxes: an unchecked box reads as empty, so `required` enforces opt-in
  (e.g. accept-terms). A checked box reads as its `value` (or `'on'`).
- The theme (`@42/styles/form.css`) is self-sufficient and styles labels, inputs,
  focus rings and the error/invalid states. Functional CSS only toggles error
  visibility.
