Creating Form Objects with ActiveModel
A form object is an object designed specifically to be passed to form_for
.
It is often used to aggregate data to create multiple objects or to receive ephemeral data that is used and then discarded.
Rails 4 introduced a small handy module called ActiveModel::Model
.
A Ruby class can mix in this module and gain a ton of functionality, including:
-
initialization with a hash of attributes
-
validation of attributes
-
presentation of errors
-
interaction with view helpers like
form_for
and the newform_with
It basically allows a Ruby object to quack like an ActiveRecord
model without being backed by a database table.
Exactly what we need to implement a form object.
A contact form is one of the easiest ways to acquire leads. Here’s how we want our view to look like — a simple form with 3 inputs and a button:
<%= form_for @contact do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.input :email %>
<%= f.email_field :email %>
<%= f.input :message %>
<%= f.text_field :message %>
<%= f.button :submit, 'Send message' %>
<% end %>
How could we make this work with a Ruby object? Simple! Include ActiveModel::Model
:
class Contact
include ActiveModel::Model
attr_accessor :email, :message, :name
end
Now here’s how we want our controller to look like:
class ContactsController < ApplicationController
def new
@contact = Contact.new
end
def create
@contact = Contact.new(params[:contact])
if @contact.valid?
# contact_email and to_h methods left as an exercise to the reader
Mailer.contact_email(@contact.to_h).deliver
redirect_to root_url, notice: 'Email sent!'
else
render :new
end
end
end
valid?
is there to guarantee that a contact is valid before we send an email.
Let’s add some validations like we would with any ActiveRecord
model:
class Contact
# ...
validates :email, :message, :name, presence: true
end
An ActiveRecord
model has an errors
object
that is populated when a validation fails — usually after calling save
.
An ActiveModel
object behaves in a similar fashion.
Calling valid?
on @contact
will run its validations and populate its errors object.
Afterwards we can use any of these methods in our code:
@contact.errors.messages
#=> { email: ["can't be blank"], name: ["can't be blank"] }
@contact.errors.full_messages
#=> ["Email can't be blank", "Name can't be blank"]
@contact.errors[:email]
#=> ["can't be blank"]
@contact.errors.full_messages_for(:email)
#=> ["Email can't be blank"]
And that’s it!
Points worthy of note:
-
valid?
has an alias calledvalidate
. It also has an opposite method calledinvalid?
. We can use these methods to validate anActiveModel
object and to populate its errors object. -
Some people like to add the suffix
Form
to their form objects. SoContactForm
instead ofContact
. This prevents naming collisions withActiveRecord
models — for example we could have both aUser
and aUserForm
. Here I opted for the shorter term. In Rails, controllers are suffixed withController
but models aren’t suffixed. I view form objects more as models not connected to any databases. -
Rails automatically loads all files placed in the
app
folder. I always place form objects inapp/forms
. I would save thisContact
class inapp/forms/contact.rb
. But this is a convention, not a rule.
Now go on and create your own form objects. Add contextual validations. Simplify your code!