Getting started

Quick start

From a model to a live-validating form class in four steps.

Start from the model#

Everything phlex-forms infers comes from here — column types, the enum, the association, the validators:

app/models/user.rb
class User < ApplicationRecord
  belongs_to :country
  enum :role, { member: 0, admin: 1 }

  validates :email, presence: true, uniqueness: true
  validates :bio, length: { maximum: 400 }
end

Render an inline form#

Form(model:) derives the scope, URL, and method from the record (POST for a new record, PATCH + hidden _method for a persisted one), emits the CSRF token, and yields itself as the builder:

app/views/users/new.rb
Form(model: @user) do |f|
  f.field :email                 # type=email, required — both inferred
  f.field :role                  # enum → select: Member / Admin
  f.field :country               # belongs_to → select, name="user[country_id]"
  f.field :bio, hint: "Optional" # text column → textarea, maxlength=400
  f.submit :primary              # "Create User" / "Update User"
end

Each field call renders a labeled control with the error (when the model has one) or the hint beneath it. Server round-trip errors just work: re-render the form with the invalid record and every failed field shows its message.

Promote it to a form class#

The same form as a reusable, testable object — self is the form, so the f. prefix disappears:

app/forms/user_form.rb
class UserForm < Forms::Base
  form_options :spaced, field_variants: [:primary]

  def fields
    field :email
    field :role
    field :country
    field :bio, hint: "Optional"
    submit :primary
  end
end
app/views/users/new.rb
render UserForm.new(model: @user)

form_options defaults are inherited and merged down the subclass chain; instance arguments win. See Form classes.

Go live#

With phlex-reactive in the bundle, one macro turns the form into a server-truth live validator — blur a field and the real validators run (including that uniqueness check), the errors morph in, and the caret never moves:

app/forms/user_form.rb
class UserForm < Forms::Base
  live model: User, debounce: 300

  def fields
    field :email
    field :role
    field :country
    field :bio, hint: "Optional"
    submit :primary
  end
end

Nothing is persisted by live validation — native submit and your controller stay authoritative. The full story (touched tracking, whitelists, constraints) is on Live validation.