Reference

Nested & collections

Nested attributes, collection inputs, and the escape valves dynamic forms need.

Nested attributes: fields_for#

fields_for yields a builder per association (single) or per item (has_many), with the correctly-indexed _attributes scope Rails' accepts_nested_attributes_for expects:

f.fields_for(:line_items) do |item|
  item.field :description        # name="invoice[line_items_attributes][0][description]"
  item.field :quantity
end

The nested builder speaks the full field API — field, row, escape hatches, even further nesting — and inherits the parent form's theme and variants.

JSONB / hash columns#

For a hash-backed column that isn't a Rails nested-attributes association, nested_attributes: false nests under the raw name — no _attributes suffix:

f.fields_for(:settings, nested_attributes: false) do |s|
  s.field :locale                # name="user[settings][locale]"
end

Collection inputs#

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…")

collection_check_boxes emits the hidden empty field so an empty selection still submits, and yields a builder per item with check_box and label prewired. For a belongs_to, remember inference renders the association select from a bare field :countrycollection_select is for the explicit cases.

Unscoped mode & external widgets#

Dynamic rows — phlex-reactive per-row editors, <template>-cloned rows — need bare field names the model scope would break. scope: false turns scoping off (while scope: nil still derives from the model):

Form(model: @item, scope: false, url: item_path(@item)) do |f|
  f.field :quantity              # name="quantity"
end

External widgets bind through the public helpers — the same three the custom-widget recipe uses:

f.field_name(:starts_at)   # "event[starts_at]"
f.field_id(:starts_at)     # "event_starts_at"
f.field_value(:starts_at)  # the model's current value