Blog

Forget about Nested Permission Attributes in Controllers with Form Objects

7 mins

If you are tired of big nested permission attributes in controllers and messed up relations and accepts_nested_attributes_for in models, then you will find much useful information in this article. Go on reading!

The best way to eliminate the redundancies mentioned above is to add another abstraction layer – form objects. Using this way, we will be able to leave associations, scopes, and finders in models only, and probably some persistent logic. What we’re going to do is to remove validations, business logic, and all the rest. In controllers’ actions endpoints will remain which call form objects and pass parameters into them.

Which facilities you’d better use:

At first, let’s have a look at Reform.

The creation of a FormObject for User entity is a pretty simple task:

class UserForm < Reform::Form
property :name
validates :name, presence: true
end

Then there is what we see concerning the controller:

class UsersController
def new
  @form = UserForm.new(User.new)
end

def create
  @form = UserForm.new(User.new)
  if @form.validate(params[:user])
    @form.save
    ...
  else
    ...
  end
end

def edit
  user = User.find(params[:id])
  @form = UserForm.new(user)
end

It’s also pretty easy to render a form into view:

= form_for @form do |f|
= f.input :name

The FormObject lets map data to be separated from models and validations:

class UserForm < Reform::Form
include Reform::Form::ActiveRecord
include Composition

model :company

property :name, on: :user
property :email, on: :user
property :company_name, on: :company, from: :name

validates :name, presence: truevalidates :email, presence: true
validates :company_name, presence: true

def save
  super
  model[:user].update(company: model[:company])
end
end

It will have the following view:

= simple_form_for @form do |f|
- f.input :name
- f.input :email
- f.input :company_name, label: 'Company Name'

Let’s consider a more complex example and see how the FormObject can help us in removing all these accepts_nested_attributes_for from the models we’ve built and business logic:

class User < ActiveRecord::Base
has_many :permissions, class_name: 'User::Permission'
has_many :pay_rates, class_name: 'User::PayRate'
has_many :addresses, class_name: 'User::Address', dependent: :destroy
has_many :licenses, class_name: 'User::License'
...
...

accepts_nested_attributes_for :permissions, reject_if: :all_blank, allow_destroy: true
accepts_nested_attributes_for :pay_rates, reject_if: :all_blank, allow_destroy: true
accepts_nested_attributes_for :addresses, reject_if: :all_blank, allow_destroy: true
accepts_nested_attributes_for :licenses, reject_if: :all_blank, allow_destroy: true
...
...

def build_dependencies
  self.permissions ||= User::Permission.new
  permissions.build_dependencies

  self.pay_rates ||= User::PayRate.new
  pay_rates.build_dependencies

  self.licenses ||= User::License.new
  licenses.build_dependencies

  self.addresses ||= User::Address.new
  addresses.build_dependencies
  ...
  ...
end
end

Now, it is time to see what we have in the controller:

Looks not really good, right?

Look what happens if we move validations and nesting objects to the FormObject:

class User::Permission < ActiveRecord::Base
belongs_to :user
...
...
end
class User::PermissionFormObject < Reform::Form
property :human_resources
property :payrol
property :clinical_assignments
property :patient_schedule
property :referral
property :md_order
property :visit_note
property :chha_assignmen
property :chha_supervisory_visit_note
property :lvn_supervisory_visit_not
property :case_conference
property :progress_report
property :plan_of_car
property :medication_profile
property :incident_repor
property :infection_report
property :discharge_summar
property :discharge_instruction
property :notice_of_non_coverag
property :hhabn
property :visit_note_co_sign
property :md_orders_co_sign

validate_presence_of :human_resources, :payroll,
                     :clinical_assignments, :patient_schedule,
                     :referral, :md_orders,
                     :visit_note, :chha_assignment,
                     :chha_supervisory_visit_note, :lvn_supervisory_visit_note,
                     :case_conference, :progress_report, :plan_of_care,
                     :medication_profile, :incident_report,
                     :infection_report, :discharge_summary,
                     :discharge_instruction, :notice_of_non_coverage,
                     :hhabn, :visit_note_co_sign, :md_orders_co_sign

# More complex validations for permissions
...
...
...
end

Then, let us consider the UserForm:

class UserForm < Reform::Form
# user properties
collection :permissions, form: User::PermissionForm, populate_if_empty: User::Permission
collection :pay_rates, form: User::PayRateForm, populate_if_empty: User::PayRate
collection :licenses, form: User::LicenseForm, populate_if_empty: User::License
collection :addresses, form: User::AddressForm, populate_if_empty: User::Address

# user validations
end

This is how we moved validations and mapping attributes from models to forms. Besides, we’ve simplified the process of forms rendering. Now, It’s not a big deal to implement such a task, do you agree? Follow our blog to learn other development techniques.