HOWTO use form objects with Rails

Form objects are an informal pattern for separating validation logic from persistence logic and reducing the complexity of your models, making them easier to test and reason about. This guide shows you how to use form objects in your Rails application.


The form object

First step, add the formeze gem to your Gemfile (and run bundler to install):

gem 'formeze', '~> 5'

You can then define a form object by inheriting from Formeze::Form and declaring the form fields. For example, here’s how you might define a form for adding products:

app/forms/product_form.rb
require 'formeze'

class ProductForm < Formeze::Form
  field :title, scrub: [:strip]
  field :description, multiline: true, maxlength: 2000, scrub: [:strip]
  field :size, required: false, values: %w(Small Medium Large)
end

The different field options are detailed in the formeze README.

This class can then be used to automatically parse, scrub, and validate form data submitted to your application, irrespective of where or how the data is persisted.

The view

The view corresponding to the form object above might look something like this:

app/views/products/new.html.erb
<%= form_tag action: :create do %>
  <input type="text" name="title" value="<%= @form.title %>">

  <textarea name="description"><%= @form.description %></textarea>

  <select name="size">
    <%= options_for_select %w(Small Medium Large), @form.size %>
  </select>

  <input type="submit" value="Add product">
<% end %>

The field values can be accessed as attributes on the form object. Either you can use regular HTML for the form inputs, the Rails form helpers, or a combination of both.

The controller

The controller corresponding to the form object and view above might look like this:

app/controllers/products_controller.rb
class ProductsController < ApplicationController
  def new
    @form = ProductForm.new
  end

  def create
    @form = ProductForm.new.parse(request)

    if @form.valid?
      # save form data
      # @form.to_h returns a hash of field names and values
      # fields can also be accessed as attributes like @form.description
    else
      render :new, status: :unprocessable_entity
    end
  end
end

If the form is valid you’ll typically pass the data to your model layer. If the form is invalid you’ll typically re-render the form so the user can make changes and re-submit.

Displaying validation errors

Re-rendering the form without any indication of why it won’t save is confusing for users, so in most cases you’ll want to display the validation errors to the user, for example:

app/views/products/new.html.erb
<% if @form.errors? %>
  <div class="errors">
    <ul>
      <% @form.errors.each do |error| %>
        <li><%= error %></li>
      <% end %>
    </ul>
  </div>
<% end %>

This is similar to how you would display ActiveRecord validation errors in views, except that the validations and errors are defined on the form object, not your model object.


That’s all there is to it. Give it a try in your next application!