Anatomy of a Form Object — Rails

Elijah Goh
3 min readJan 29, 2019

--

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: true
end

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: []
)
end
end

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

--

--

Elijah Goh

Elijah is a programmer, gamer, and cat daddy, who enjoys writing once in awhile.