Guide

# Form classes

Subclass Forms::Base, declare fields where self IS the form, render it anywhere.

## The shape

A form class subclasses `Forms::Base` and implements one hook — `#fields`. Inside it, `self` is the form, so the whole builder surface (`field`, `row`, `group`, `Input`, `submit`, `fields_for`, …) is available as bare calls:

```ruby
class UserForm < Forms::Base
  def fields
    field :email
    row do
      field :first_name
      field :last_name
    end
    submit :primary
  end
end
```

```ruby
render UserForm.new(model: @user)
```

The class renders `Form`'s full chrome — derived action/method, CSRF token, method override, multipart encoding — then your declared fields. A subclass without `#fields` raises `NotImplementedError`.

## Inherited defaults: form_options

`form_options` sets class-level defaults — positional form modifiers and any keyword `Form` accepts. They are **inherited and merged** down the subclass chain: a child's defaults beat its parent's, and instance arguments beat both.

```ruby
class ApplicationForm < Forms::Base
  form_options :spaced, field_variants: [:primary]
end

class AdminForm < ApplicationForm
  form_options theme: :plain          # merged over the parent's
end

AdminForm.new(model: record, url: "/override")  # instance args win
```

## The render-time block

A block passed at render time appends **after** the declared fields — handy for a page-specific hidden field or an extra action without subclassing:

```ruby
render UserForm.new(model: @user) do |f|
  f.Hidden(:return_to)
end
```

## Testing form classes

A form class is a Phlex component — `#call` renders it to a string with no controller, no request, no view context:

```ruby
it "renders the email field bound to the model" do
  output = UserForm.new(model: User.new(email: "a@b.c")).call

  expect(output).to include('name="user[email]"')
  expect(output).to include('value="a@b.c"')
end
```