# Form Builder

The model-bound FormBuilder: form_with(builder:) + f.input for one-call fields - label, value, errors, aria, and validation attributes all derived from the object; type inference into Poetry's vocabulary, f.association, and the poetry_form -> simple_form i18n chain.

## Default

```erb
<%# The whole story in one form: form_with(builder:) + one f.input per
    attribute. Label from i18n, value from the object, aria-required from
    the presence validation, maxlength from the length validation, the
    boolean as a setting-row switch, the collection as a Select, and
    f.submit's label from Rails' own i18n - nothing hand-wired. %>
<% profile_class = Class.new do
     include ActiveModel::Model
     include ActiveModel::Attributes

     attribute :name, :string
     attribute :email, :string
     attribute :email_alerts, :boolean
     attribute :role, :string

     validates :name, presence: true, length: { maximum: 50 }

     def self.name = "Profile"
   end
   profile = profile_class.new(name: "Ada Lovelace", role: "editor", email_alerts: true) %>
<%= form_with(model: profile, url: "/forms", method: :get,
              builder: Poetry::Ui::FormBuilder,
              class: "flex w-full max-w-md flex-col gap-6") do |f| %>
  <%= f.input(:name, hint: "Shown on your public profile.") %>
  <%= f.input(:email) %>
  <%= f.input(:email_alerts, switch: true, hint: "Applied the moment it flips.") %>
  <%= f.input(:role, collection: [["Admin", "admin"], ["Editor", "editor"], ["Viewer", "viewer"]]) %>
  <div><%= f.submit %></div>
<% end %>
```

## Errors

```erb
<%# The error quartet from model truth: errors.add flows into the Field -
    red label, message with an id, aria-describedby error-before-hint,
    aria-invalid on the control, data-invalid on the root. The dual-key
    lookup also surfaces association errors (:company) on the _id field. %>
<% signup_class = Class.new do
     include ActiveModel::Model
     include ActiveModel::Attributes

     attribute :email, :string
     attribute :password, :string

     validates :email, presence: true

     def self.name = "Signup"
   end
   signup = signup_class.new
   signup.errors.add(:email, "can't be blank")
   signup.errors.add(:password, "is too short (minimum is 12 characters)") %>
<%= form_with(model: signup, url: "/forms", method: :get,
              builder: Poetry::Ui::FormBuilder,
              class: "flex w-full max-w-md flex-col gap-6") do |f| %>
  <%= f.input(:email, hint: "We never share it.") %>
  <%= f.input(:password) %>
<% end %>
```

## Inference

```erb
<%# What f.input resolves without as:: text columns -> Textarea, booleans
    -> the horizontal Field checkbox, integers with numericality -> a
    NumberField carrying min/max/step, date columns -> DateField, and
    name heuristics type the string inputs (email/url/tel/password -
    password never round-trips). as: overrides any of it. %>
<% article_class = Class.new do
     include ActiveModel::Model
     include ActiveModel::Attributes

     attribute :summary, :string
     attribute :website_url, :string
     attribute :quantity, :integer
     attribute :published_on, :date

     validates :quantity, numericality: { greater_than_or_equal_to: 1,
                                          less_than_or_equal_to: 99, only_integer: true }

     def self.name = "Listing"
     def self.type_for_attribute(name)
       name == "summary" ? Struct.new(:type).new(:text) : super
     end
   end %>
<%= form_with(model: article_class.new, url: "/forms", method: :get,
              builder: Poetry::Ui::FormBuilder,
              class: "flex w-full max-w-md flex-col gap-6") do |f| %>
  <%= f.input(:summary, hint: "A text column renders the Textarea.") %>
  <%= f.input(:website_url, hint: "The _url name renders type=url.") %>
  <%= f.input(:quantity, hint: "numericality becomes min=1 max=99 step=1.") %>
  <%= f.input(:published_on, hint: "A date column renders the DateField.") %>
<% end %>
```

## Layout

```erb
<%# Sections without leaving the builder: f.fieldset(legend:) frames a
    group and yields the builder; orientation:/hint_position: pass
    through to the Field (the setting-row and hint-above surfaces). %>
<% account_class = Class.new do
     include ActiveModel::Model
     include ActiveModel::Attributes

     attribute :display_name, :string
     attribute :bio, :string

     def self.name = "Account"
     def self.type_for_attribute(name)
       name == "bio" ? Struct.new(:type).new(:text) : super
     end
   end %>
<%= form_with(model: account_class.new(display_name: "Ada"), url: "/forms", method: :get,
              builder: Poetry::Ui::FormBuilder,
              class: "flex w-full max-w-md flex-col gap-6") do |f| %>
  <%= f.fieldset(legend: "Identity") do |section| %>
    <%= section.input(:display_name) %>
    <%= section.input(:bio, hint: "Markdown is fine.", hint_position: :above) %>
  <% end %>
  <div><%= f.submit("Save changes") %></div>
<% end %>
```
