Multiple Model Form via FormObject Pattern

Sometimes in life you just have to make your life easer. And the first step anybody can take to make their life easier is to not use Accepts_nested_attributes_for when making multiple model forms. The Problem with accepts_nested_attributes_for is that connecting more than two models in a single form is a headache. In Rails the more graceful way to conveniently connect multiple models is to use the Form Object Pattern.

It’s infinitely easier to set up and gives you a higher level of control when it comes to creating very complex forms. The Process goes like this:

  1. Create a PORO in the Model folder of your rails app
  2. Inside the PORO Include ActiveModel::Model
  3. Inside the PORO create attr_accessors representing all the fields of your form
  4. Add your validations for each accessor attribute
  5. Inside the PORO, Create a submit method specifying where you want your data to be submitted to.
  6. Call “FormObjectName”.new and “FormObjectName”.submit when you want to process your form.

The above process allows you to represent the form you want through a PORO not constrained by the rules of ActiveRecord. Its a very simple process and is extensible in a way, that accepts_nested_attributes_for is not. <> Here is an example of how you would setup a form object:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class MeetingForm
  Include ActiveModel::Model
  attr_accessor :meeting_name, :start_time, :end_time, :location_zone, :first_name

  validates_presence_of :meeting_name, :start_time, :end_time, :location_zone, :first_name

  def submit(options{})
    user =  User.where(first_name: params[:first_name]).take
    event = create_meeting(params[:meeting_name], params[:start_time], params[:end_time], user)
    location =  create_location(params[:location_zone], event)

  end

  def create_meeting(name, start_t, end_t, user)
    Meeting.create(meeting_name: name, start_time: start_t, end_time: end_t, user: user)
  end


  def create_location(zone, meeting)
    Location.create(location_zone: zone, meeting: meeting)
  end

  def valid?
    false
  end
end

This is how you make the form object available in the controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MeetingsController < ApplicationController
  def new
    @meeting = MeetingForm.new
    @users = User.all
  end

  def create
    @meeting = MeetingForm.new(params)

    if @meeting.valid?
         @meeting.submit(params)
         flash[:notice] = 'Sccessfully created a new meeting'
         redirect_to root_path
    end
  end

end

Notice Above that you didn’t call save on the meetingform object. ActiveModel doesnt have a save method, it has an equivalent ActiveModel#valid? method. This method allows you to check that your validations are passing. Also notice that there is no strong paramters being included in the MeetingController. The Validations are happening on the ActiveModel Object themselves once you run the #valid? method.

And lastly, Here is the form that you need to create to use a FormObject:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<h2>Create Meeting</h2>
<div>
<%= form_for @meeting do |meet| %>
  <%= meet.label :name %>
  <%= meet.text_field :meeting_name %>
</br></br>
  <%= meet.label :start%>
  <%= meet.time_select :start_time, {:ampm => true} %>
</br></br>
  <%= meet.label :end %>
  <%= meet.time_select :end_time, {:ampm => true} %>
</br></br>
  <%= meet.label 'Meeting Location'%>
  <%= meet.select(:zone, Location::ZONES)%>
</br></br>
  <%= meet.label 'Add Participant' %>
  <%= meet.select :first_name , options_for_select(@users), {:size => 5 }%>
</br></br>
  <%= meet.submit "Create Meeting"%>
<%end%>
</div>

Notice that this form is not a nested model form. It is a simple and straightforward form. Its purpose is solely to take in the requested data. It has no knowledge of multiple models, and its supported by one simple PORO. However this simple PORO is used to join 3 different models at one time - Meeting,Location,User.

So in Summary:

- A FormObject is good for connecting multiple models in a form.

- A FormObject allows you to build a smart form without using ActiveRecord.

- A FormObject allows you to build a simple form but smart form

- A FormObject is a simple Ruby Object