Client-side validation
validate: true mirrors your ActiveModel validators into shipped Stimulus controllers — the no-phlex-reactive fallback.
Turning it on#
validate: true introspects the model's validators and emits data-validations--* bindings per field, plus a form-level submit coordinator. The form gets novalidate — the shipped Stimulus controllers own error display, so you never see the inconsistent native browser bubbles.
Form(model: @partner, validate: true) do |f|
f.field :title
f.field :note
f.submit :primary
endRegister the controllers once (see Installation):
import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
lazyLoadControllersFrom("phlex_forms/controllers", application)Per-field overrides#
Form(model: @partner, validate: true) do |f|
f.field :title # every validator on :title
f.field :slug, validate: false # opt this field out
f.field :note, validate: { length: { maximum: 30 } } # explicit inline rules
endWhat's supported#
Presence, length (with a live character counter), format, numericality, inclusion, exclusion, confirmation, and acceptance.
Validators with :if, :unless, or :on are skipped — they need server context — and uniqueness can't be checked client-side. The server stays authoritative either way; this layer is UX, not enforcement.
Messages & i18n#
A validator's own message: reaches the browser, so the client shows the text the server would add:
- a String is used as is;
%{value}is filled in client-side, and%{count}for rules that have one (length, the numericality bounds). - a Symbol is resolved on the server through ActiveModel's lookup chain (
activemodel.errors.models.<model>.attributes.<attr>.<key>, thenerrors.messages.<key>) in the current locale. - a Proc is skipped (it needs a record); the generic message shows.
Length's too_long:, too_short: and wrong_length: win over message:, as on the server. An inline validate: { ... } hash takes the same options; a Symbol there has no model behind it, so it resolves through errors.messages.<key> only, with %{attribute} reading "Value".
validates :code, format: { with: /\A[a-z]*\z/, message: "may only contain lowercase letters" }
validates :bio, length: { maximum: 160, too_long: "keep it under %{count} characters" }Without a message:, the bundled messages apply. They ship for en, fr, and af; override any string page-wide via window.PhlexForms.messages. The engine prepends the gem's locale files to I18n.load_path, so your app's files win.