Creating Form Objects with ActiveModel and Virtus

You might have heard of “fat models” referring to (mostly) ActiveRecord models turning into huge classes responsible for everything from user authentication to accepting attributes for entirely different models. This violates one of my favourite rules of [SOLID][solid], the single responsibility principle.

One of the patterns you can use to help balance your SOLID karma is with form objects. We use form objects extensively to back our forms and encapsulate the data submitted by a user.

It’s often tempting to reach for the ever ready ActiveRecord model when building forms in Rails because it Just Works™ with the Rails Form Helpers. I’m going to show you how to have your cake and eat it by backing your forms with objects.

Why use form objects?

When looking to refactor your app it’s always a good idea to keep the single responsibility principle in mind.

…the single responsibility principle states that every class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class…

SRP helps you make better design decisions around what a class should be responsible for. Your database table model for example (an ActiveRecord model in the context of Rails) represents a single database record in code, there is no reason for it to be concerned with anything your user is doing.

This is where form objects come in. A form object is responsible for representing a form in your application. So each input field can be treated as an attribute in the class, it can validate that those attributes meet some rules and it pass the “clean” data to where it needs to go. This could be your database models or perhaps your search query builder.

Using Virtus and ActiveModel to create form objects

Building a form object is really easy, Rails 4+ provides ActiveModel::Model so it “quacks” like an ActiveRecord object and works seamlessly with form_for, including giving you access to all the validation helpers you’re already familiar with including methods such as .valid?.

Virtus provides a neat DSL to define attributes on the object with useful options such as defining the data type and default value - very similar to what ActiveRecord is doing behind the scenes. Virtus is not essential for your form object but it saves a lot of boilerplate code.

The foundation for your form object is simple, just include ActiveModel::Model and Virtus. Although form objects are technically models I prefer to create a new directory called forms in the app directory to store my form objects separately from my ActiveRecord models.

I often find people are scared to create directories outside of the Rails scaffold but you don’t need to be, Rails will load them all - just remember to restart your server processes when you do.

A form object might look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# app/forms/user_expense_form.rb
class UserExpenseForm
  include ActiveModel::Model
  include Virtus

  # Attributes (DSL provided by Virtus)
  attribute :email, String
  attribute :amount, Integer
  attribute :paid, Boolean, default: false)

  # Access the expense record after it's saved
  attr_reader :expense

  # Validations
  validates :email, presence: true
  validates :amount,  numericality: { only_integer: true, greater_than: 0 }

  def save
    if valid?
      persist!
      true
    else
      false
    end
  end

  private

  def persist!
    user = User.create!(email: email)
    @expense = user.expenses.create!(amount: amount, paid: paid)
  end
end

You’ll notice we’ve created two database records in two different tables from this single form. Your form objects don’t need to be any more complicated than that. The same is true in your controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# app/controller/expenses_controller.rb
class ExpensesController < ApplicationController
  def new
    @user_expense_form = UserExpenseForm.new
  end

  def create
    @user_expense_form = UserExpenseForm.new(user_expense_form_params)
    if @user_expense_form.save
      redirect_to dashboard_url, notice: "Expense ID #{@user_expense_form.expense.id} has been created"
    else
      render :new
    end
  end

  private

  # Using strong parameters
  def user_expense_form_params
    params.require(:user_expense_form).permit!(:email, :amount, :paid)
  end
end

This should look very similar to how you might already be working with your ActiveRecord models. And finally just for completeness in the view with form_for.

1
2
3
4
5
6
7
8
9
10
11
12
<%= form_for @user_expense_form, url: expenses_path do |f| %>
  <%= f.label :email %>
  <%= f.email_field :email %>

  <%= f.label :amount %>
  <%= f.number_field :amount %>

  <%= f.label :paid %>
  <%= f.check_box :paid %>

  <%= f.submit %>
<% end %>

And there we have it, this form object is responsible for user input for this form and handing it off to the other models for saving to the database.

What does this mean for your ActiveRecord models?

Now your model doesn’t directly accept a user’s input then you can remove your validations from the model because your form object is taking care of that.

If that makes you nervous consider where any invalid data would be reaching as far as your database model without being valid and do something about it before it gets that far.

Need i18n support? It’s already there!

Because we’re backed by ActiveModel::Model, internationalization (i18n for short) is already baked in so you can set the attribute labels and validation messages the same as you can when working with ActiveRecord models.

1
2
3
4
5
6
7
8
9
10
11
en:
  activemodel:
    attributes:
      user_expense_form:
        paid: "Has this been paid?"
    errors:
      models:
        user_expense_form:
          attributes:
            amount:
              greater_than: "must be greater than $%{count}"

Final thoughts

With form objects we are not attempting to replace Rails “magic” in fact we are working with it to leverage more power and flexibility by modelling our forms on the data we want them to receive from the user and not whatever columns are in our database table. They also allow you to back forms that might not have a map to a database table such as a search form.

By decoupling from the database schema you allow yourself to make better design decisions around your business logic instead of trying to force your database schema to map to your views so that your form helper will work.