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 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.
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
endi18n 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-forminputtrigger. - Each input carries a
blurtrigger with a_touchparam naming the field. - Identity is state-backed: the token signs the model's GlobalID when persisted (
nilfor 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, callsmodel.validate, and repliesmorph— 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:
class UserForm < Forms::Base
live model: User
live_deny :role # never assign these
# live_permit :email, :password # or: assign ONLY these
endConstraints#
liverequires aForms::Basesubclass. The endpoint rebuilds the form from its class — an inlineForm(model:) { … }block cannot be serialized, soForm(live: true)raises and says so.phlex-reactiveis a soft dependency: without it the macro raises aFeatureUnavailablewith install guidance, and the Stimulus fallback 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.