Rails applications that go beyond the most basic often require complicated forms, or forms not backed by an ActiveRecord model. Form objects help in this regard, and are a fantastic technique of moving code out of your models.
I like to use form objects whenever I have a slightly complicated form, a form which will write to 2 database models or if I'm creating a form that isn't backed by a database model at all. You can think of the form object as a model of the form itself as displayed to the user. This means you've got a spot to put logic which is related to only the form itself.
You may have heard of form objects before but perhaps you're a little confused on how to use them properly in your Rails applications. If you've got a subscription to RailsCasts you can watch the form object screencast which provides an excellent introduction, but here's the short version:
- Create a plain Ruby class.
include ActiveModel::Model(in Rails 3 you had to include
- Start using your new form class as if it were a regular ActiveRecord model, the biggest difference being that you cannot persist the data stored in this object.
Example form object implementation
Here's an example form object. I like to put these in
app/forms although many people would keep them in
class ProfileForm include ActiveModel::Model attr_accessor :name, :email, :subscribed_to_marketing_emails, :age validates_presence_of :name, :email validates_format_of :email, with: /.+@.+/ validates_numericality_of :age, allow_nil: true, greater_than: 0 # instead of doing this you could always remember to provide # a default value to the initializer instead: ProfileForm.new(subscribed_to_marketing_emails: true) # but that would be frustrating too. def subscribed_to_marketing_emails @subscribed_to_marketing_email ||= true end end
Your controller for the form might look a little like:
class ProfilesController < ApplicationController def new @profile_form = ProfileForm.new end ...
and your view might look like:
<%= simple_form_for @profile_form, url: profile_path do |f| %> <%= f.input :name %> <%= f.input :email %> <%= f.input :age, as: :integer %> <%= f.input :subscribed_to_marketing_emails, as: :boolean %> <% end %>
But there are some problems with our code.
- Having to specify the
urlin the form helper is frustrating. We can fix this by specifying the
model_nameon the class of the object we pass to the helper.
- Having to specify the html representation of the
:agefields is frustrating. With simple_form this doesn't happen if you use a normal ActiveRecord object because simple_form infers the type of the html element from the type of the database column.
- Setting default values for your attributes is a little bit messy. Note that since ActiveModel provides an initializer for our object which accepts a hash like a regular ActiveRecord object, it's not a good idea to override the
Recently I created a very small gem, called SimpleFormObject, which solves the issues above. To refactor the form object code above we'd do the following:
class ProfileForm include SimpleFormObject attribute :name attribute :email attribute :subscribed_to_marketing_emails, :boolean, default: true attribute :age, :integer validates_presence_of :name, :email validates_format_of :email, with: /.+@.+/ validates_numericality_of :age, allow_nil: true, greater_than: 0 end
In this version of the form object we don't need to provide a default using the attribute reader method so it's a little cleaner.
We wouldn't need to change our controller at all but our view is simplified to look like:
<%= simple_form_for @profile_form do |f| %> <%= f.input :name %> <%= f.input :email %> <%= f.input :age %> <%= f.input :subscribed_to_marketing_emails %> <% end %>
Notice here we don't need to specify the
url of the form. Nor do we have to specify which html elements we want to create. SimpleFormObject causes each attribute to pretend to be a database column which simple_form will convert automatically to the correct element.
I think this is a small, but useful improvement for form objects. Note that there other fantastic form gems out there. But often I want to stay as close to the Rails defaults as possible and this provides just enough sugar to let me do what I need to do, while getting out of my way.
Let me know what you think!