phlex-forms
A model-bound, DaisyUI-styled form builder for Phlex.
Form(model:) { |f| f.field :email } renders a label, an input, and an
error/hint in one call — inferring the input type from the attribute name and
the required flag from the model's validations. It's built on the
daisyui gem, so every field is a thin
binding layer over a real DaisyUI component: positional variants stack, blocks
pass through, and the markup stays DaisyUI v5-correct.
Form(:spaced, model: @user) do |f|
f.field :email, hint: t("users.email_hint") # type=email, required inferred
f.field :role, as: :select, choices: roles
f.field :bio, as: :textarea, rows: 6
f.field :notify, as: :toggle
f.submit :primary
end
Installation
# Gemfile
gem "phlex-forms"
phlex-forms exposes its components under the Forms:: namespace as a
Phlex::Kit. Include it wherever you render
components (typically your ApplicationComponent / base view):
class ApplicationComponent < Phlex::HTML
include Forms
end
Now Form(...), Submit(...), and every other Forms::* component are available
as bare kit helpers.
The field API
f.field(name, *modifiers, **options) is the primary verb. It renders a
form-control wrapping a label, the input, and an error (or hint):
| Option | Effect |
|---|---|
label: |
Label text. Defaults to the model's humanized attribute name. label: false omits it. |
hint: |
Help text shown when there is no error. |
as: |
Override the control: :select, :textarea, :toggle, :checkbox, :file, :radio, :hidden, or any text-like type. |
required: |
Force the required flag. Otherwise inferred from the model's presence validators. |
choices: |
Choices for as: :select. |
| positional modifiers | daisyui variants — :primary, :lg, :ghost, … — stacked onto the input. |
Type is inferred from the attribute name (email → type=email, password →
type=password, phone → type=tel, …).
Escape hatches
When you need full control (custom Stimulus wiring, bespoke layout), the lower-level component methods are always available with stable signatures:
f.Control(:email, label: "Email") do
f.Input(:email, :primary, data: { controller: "autocomplete" })
end
f.Input(:name, :primary, :lg) # bare input, variants stacked
f.Select(:role, choices: roles) # native <select>
f.Textarea(:bio, :ghost)
f.Checkbox(:terms) ; f.Toggle(:notify) ; f.FileInput(:avatar)
f.Label(:email) ; f.Hidden(:token) ; f.submit("Save", :primary)
Icons inside a field (DaisyUI v5)
WrappedInput renders the <label class="input">{icon}{input} pattern:
f.field(:search).wrapped_input(:primary) do
LucideIcon("search", class: "opacity-50") # leading content
end
Nested attributes & collections
f.fields_for(:line_items) do |item| # single assoc or has_many
item.field :description
end
f.collection_check_boxes(:role_ids, Role.all, :id, :name) do |b|
render b.check_box
render b.label
end
f.collection_select(:country_id, Country.all, :id, :name, prompt: "Select…")
Client-side validation
phlex-forms ships a Stimulus validation framework that mirrors your ActiveModel validators — no ugly, browser-inconsistent native validation bubbles.
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 rules
end
When validate: true, the form gets novalidate and a submit coordinator; each
field emits data-forms--validations--* bindings introspected from the model.
Supported: presence, length (with a live counter), format, numericality,
inclusion, exclusion, confirmation, acceptance. Validators with :if / :unless
/ :on are skipped (they need server context); the server stays authoritative.
Register the controllers in your Stimulus setup:
// app/javascript/controllers/index.js
import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
lazyLoadControllersFrom("phlex_forms/controllers", application)
Validation messages ship for en / fr / af; override any string via
window.PhlexForms.messages.
Icons
Icons default to a bundled inline SVG so the gem is self-contained. To use
glyphs (rails-icons under the hood):
PhlexForms.configure do |c|
c.icon_renderer = PhlexForms::Configuration.glyphs_renderer
end
RuboCop cops
Two cops nudge call sites toward the phlex-forms API. Enable them in your
.rubocop.yml:
require:
- phlex_forms/rubocop
inherit_gem:
phlex-forms: config/rubocop.yml
PhlexForms/RawForm— useForm()overform_with/ rawform()(autocorrects).PhlexForms/LegacyFormMethod— useform.field(...)/ the PascalCase methods over Rails-styletext_field/select/ etc.
JavaScript peer dependencies
@hotwired/stimulus— required.choices.js— only if you use searchable/multi selects (searchable: true).
Companion
Form-level error summaries (FormErrors) live in your app's UI kit, not here —
pair them with Form() as you like.
License
MIT © Mikael Henriksson