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:

app/forms/user_form.rb
class UserForm < Forms::Base
  def fields
    field :email
    row do
      field :first_name
      field :last_name
    end
    submit :primary
  end
end
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.

app/forms/application_form.rb
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:

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:

spec/forms/user_form_spec.rb
it "renders the email field bound to the model" do
  output = UserForm.new(model: User.new(email: "[email protected]")).call

  expect(output).to include('name="user[email]"')
  expect(output).to include('value="[email protected]"')
end