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 — `email` → `type=email`, `password` → `type=password`, `phone` → `type=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

| Declaration | Renders |
| --- | --- |
| `has_rich_text :body` | field :body → rich textarea |
| `has_one_attached :avatar` | field :avatar → file input |
| `has_many_attached :photos` | field :photos → file input, multiple, name="…[photos][]" |
| `enum :role, {…}` | field :role → select over humanized enum keys, round-trips the current value |
| `belongs_to :country` | field :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 `name` → `title` → `label` → `to_s` chain. Errors Rails attaches to `:country` still display.

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

## Column types

| Column type | Control |
| --- | --- |
| `boolean` | toggle (checkbox + hidden unchecked pair) |
| `text` | textarea |
| `date / datetime / time` | matching input type (datetime → datetime-local) |
| `integer` | number, step: 1 |
| `decimal (scale n)` | number, step: 10⁻ⁿ |
| `decimal / float (no scale)` | number, step: "any" |
| `string` | falls 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:

```ruby
PhlexForms.configure do |c|
  c.infer_from_model = false
end
```