Anatomy of a Form Object — Rails
And how to have your form object work nicely with Rails.
This article will show you the way to make form object work with Rails, also a bonus on how to avoid the dreaded accepts_nested_attributes_for
and nested_forms. Our goal is to isolate logic into the Form object to keep our codebase clean and having them work nicely with Rails.
This tutorial uses Rails 5.12
What and why Form Object?
Form Object is very much like hybrid of Service and Presenter class to keep logic out of our models and controller and are meant to be easily testable.
What’s inside?
- Form related view logic
- Business logic
Sounds simple enough? Let’s get started!
The Application
Let’s create an app where our users can add/update their emergency contact and profile photo, all in different models for example sake.
I’ll try my best to explain every step step of the way to keep this beginner and advance friendly. Let’s start with our models.
User Model
# app/models/user.rbclass User < ApplicationRecord
has_many :contacts
has_one :photo, as: :imageable
end
Contact Model
#app/models/contact.rbclass Contact < ApplicationRecord
belongs_to :user
validates :relation, :name, :number, presence: true
end
Photo Model
#app/models/photo.rbclass Photo < ApplicationRecord
belongs_to :imageable, polymorphic: true # dragonfly image gem
dragonfly_accessor :image
validates :image, presence: trueend
The model is pretty clear cut. Notice that I did not use accepts_nested_attributes_for
this can be done in our form object.
- Our user
has_one
profile photo. - User
has_many
contacts.
The Controller
Implementing the controller is simple and can be done in a few lines thanks to our form object. We’ll call itProfileForm
since it updates the user’s details, profile photo and contacts. Also gives us a nice route named /profile
.
class ProfileController < ApplicationController def show
@form = ProfileForm.new(current_user)
end def update
@form = ProfileForm.new(current_user, form_params) if @form.save
flash[:success] = "Successfully updated"
redirect_to profile_path
else
render :error
end
end private def form_params
params.require(:profile_form).permit(
user_attributes: [:first_name, :last_name]
photo_attributes: [:photo]
contact_attributes: []
)
endend
The controller looks slim indeed!
Our form implements #save
that returns a boolean for our controller to decide where to direct our users to next. Easy enough.
The params key generated will be our Form class name, underscored, thus profile_form
. Next, let’s construct our form class which hopefully explains why we nest our attributes inuser_attributes
.
The Form Class
To keep things dry, what I suggest is creating a BaseForm
the rest of your other forms can inherit from.
# app/forms/base_form.rbclass BaseForm
include ActiveModel::Model
include ActiveModel::Validations def initialize(params={})
super(params)
end
end
Including ActiveModel::Model and ActiveModel::Validations makes this object act like an ActiveRecord model and implements validation methods like eg. valid?
and validates_presence_of
. Click on the links to read more.
We can then create the Form inheriting from BaseForm
without having to include the modules every time we create a form. Onto our form object.
# app/forms/profile_form.rbclass ProfileForm < BaseForm
attr_reader :user, :photo, :contacts
delegate :attributes=, to: :user, prefix: true
delegate :attributes=, to: :photo, prefix: true
delegate :attributes=, to: :contacts, prefix: true def initialize(user, params={})
@user = user
@photo = user.photo || user.build_photo
super(params) end
end
A breakdown of what’s happening here.
attr_reader
allows us to use @form.user
, @form.photo
which also provides access for our form builder in the view.
delegate :attributes=, to: <model>, prefix: true
is a shorthand for a setter below.
def user_attributes=(attribute)
user.attributes = attribute
end
Calling super(params)
in initialize
will assign the attributes to the object without having to assign them manually, sweet. We can test this in rails console to verify
# form = ProfileForm.new(User.new, {
user_attributes: { first_name: "My", last_name: "Boo" }
})=> #<ProfileForm:0x007fa9ce1862b0 @user=#<User id: nil, first_name: "My", last_name: "Boo", created_at: nil, updated_at: nil>># puts "#{form.user.first_name} #{form.user.last_name}"
=> My Boo