Guide

Type inference

field :x interrogates the bound model — as:/choices: are overrides, not requirements.

Precedence#

The control is resolved through a chain — first hit wins:

  1. Explicit as:
  2. A positional type modifier — field :price, :number
  3. Explicit choices::select
  4. Model structure — rich text, attachment, enum, belongs_to
  5. Non-string column type — boolean, text, dates, numerics
  6. The attribute-name map — emailtype=email, passwordtype=password, phonetype=tel, …
  7. type=text

The column type beats the name map because it is ground truth for the value's shape; the name map only disambiguates strings. The accepted degenerate case: a text column named email renders a textarea.

Model structure#

DeclarationRenders
has_rich_text :bodyfield :body → rich textarea
has_one_attached :avatarfield :avatar → file input
has_many_attached :photosfield :photos → file input, multiple, name="…[photos][]"
enum :role, {…}field :role → select over humanized enum keys, round-trips the current value
belongs_to :countryfield :country or :country_id → select over the association

The association select rewrites the field name to the foreign key (user[country_id]), takes its label and required flag from the association, and builds choices lazily from klass.all with the option text resolved through a nametitlelabelto_s chain. Errors Rails attaches to :country still display.

Association choices query per render and are unscoped. Pass choices: to scope, order, or cache them — the explicit argument always wins.

Column types#

Column typeControl
booleantoggle (checkbox + hidden unchecked pair)
texttextarea
date / datetime / timematching input type (datetime → datetime-local)
integernumber, step: 1
decimal (scale n)number, step: 10⁻ⁿ
decimal / float (no scale)number, step: "any"
stringfalls through to the name map

Validator-derived attributes#

Unconditional validators contribute HTML attributes, merged under caller options:

  • length: { maximum: 30 }maxlength="30" on text-like controls,
  • numericality: { greater_than_or_equal_to: 18, less_than_or_equal_to: 130 }min/max on number inputs (exclusive bounds map ±1 for only_integer, otherwise they're skipped),
  • a presence validator → the required flag and label marker.

Validators with :if, :unless, or :on are skipped — they need server context the renderer doesn't have.

Degradation & the kill switch#

Every model touch sits behind respond_to? guards. Plain objects, Structs, and untyped ActiveModel::Attributes fall through to the name map — exactly the pre-inference behavior. There is no ActiveRecord dependency.

To restore name-map-only inference everywhere:

config/initializers/phlex_forms.rb
PhlexForms.configure do |c|
  c.infer_from_model = false
end