Validation

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
end

Register the controllers once (see Installation):

app/javascript/controllers/index.js
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
end

What'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.

If those gaps matter, live validation closes all of them by running the real validators server-side.

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>, then errors.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.