Validation

# Live validation

live model: User runs your real ActiveModel validators on blur — uniqueness included — and morphs errors back with focus preserved.

## One source of truth

Client-side validation frameworks mirror your validators in JavaScript — and every mirror has a ceiling: uniqueness needs the database, `:if`/`:unless` need server context, custom validators don't translate, and every message exists twice.

`live` takes the other path. The whole form becomes one [phlex-reactive](https://phlex-reactive.zoolutions.llc) component: leaving a field POSTs **all** form fields to a signed `:validate` action, the server assigns them to the model, runs the **real** validators, and replies with a focus-preserving morph.

```ruby
class UserForm < Forms::Base
  live model: User, debounce: 300

  def fields
    field :email                  # uniqueness validates against the real DB
    field :password
    field :password_confirmation  # cross-field confirmation just works
    submit :primary
  end
end
```

i18n is plain Rails i18n. Custom validators, `:on` contexts, `errors.add` from anywhere — it all works, because it *is* your model. **Nothing is ever persisted** by `:validate`; native submit and your controller stay authoritative.

## How it works

- The `<form>` element is the reactive root: it carries the signed identity token and a **debounced** whole-form `input` trigger.
- Each input carries a `blur` trigger with a `_touch` param naming the field.
- Identity is **state-backed**: the token signs the model's GlobalID when persisted (`nil` for new records) plus the touched-field list — tamper-proof, with zero client-side bookkeeping.
- The endpoint rebuilds the form from its class, locates the record (or news up `live_model_class`), assigns a whitelisted slice, calls `model.validate`, and replies `morph` — Idiomorph preserves the focused input and its caret.

## No premature errors

A field's error first appears when you **leave** it (blur adds it to the signed `touched` set) — typing in a fresh field never flashes an error under a half-typed value. Meanwhile the debounced input trigger live-updates the errors of fields you *already* touched, so fixing the password clears the confirmation error as you type.

A form re-rendered after a failed submit (the classic 422) arrives with errors already on the model — those fields are auto-touched, so the standard flow shows everything.

## Assignment is double-whitelisted

The action's param schema admits only the form's scope and `_touch` — everything else is dropped at the endpoint. In the action, the slice is narrowed again to the model's column/attribute names plus every validated attribute (and `_confirmation` twins), assigned through public writers on an in-memory model. Tune it per class:

```ruby
class UserForm < Forms::Base
  live model: User
  live_deny :role                       # never assign these
  # live_permit :email, :password       # or: assign ONLY these
end
```

## Constraints

- `live` requires a `Forms::Base` subclass. The endpoint rebuilds the form from its **class** — an inline `Form(model:) { … }` block cannot be serialized, so `Form(live: true)` raises and says so.
- `phlex-reactive` is a soft dependency: without it the macro raises a `FeatureUnavailable` with install guidance, and the [Stimulus fallback](https://phlex-forms.zoolutions.llc/docs/client-validation) still works.
- Collection controls (`collection_check_boxes`, multi-selects) are excluded from live assignment in v1.
- Every blur/debounced input runs the full validator set — including uniqueness queries. The default 300ms debounce keeps that reasonable; raise it for hot forms.