I tend to avoid using nested form in my rails code. If needed, I always use form_tag rather than fields_for. But in my recent project, I give a try on using nested form. It turns out that Rails’ way to build a nested form is… how to say this delicately… beautiful. So, I think, I would like to post an article on this.
As usual, I learn how to use nested form from Ryan Bates’ Railscasts and then modify it to meet my need. In this post, I particularly focus on how to use it with three models which share a has many through association among them, just like what I did in the actual project. And for your reference, you can also check the Railscasts’ episodes here and here.
The Project
In my actual project, I had to do a calibration on something (well, I can’t name what, I can’t violate the NDA). The calibration involved several steps that can be dynamically managed. And then, for every step, I had to mark whether the calibrated thing pass the step or not and I had to enable the tester to write some notes about the step conducted. In short, I translated this scheme into a three models wtih has many through association. The models are: Calibration, CalibrationStep, and CalibrationResult. Calibration has many CalibrationStep through CalibrationResult.
1. Generation
Just like any other development, let Rails kindly does our needed auto-generated codes. Of course, we start from generating the simple app we’re going to use through this whole tutorial. I’d like to name it “calibrator”. Somehow it sounds stupid, but let it be.
For both Calibration and CalibrationStep, we will generate the full MVC. Here it goes the Calibration:
1
| rails generate scaffold Calibration name:string tester:string |
And here goes the CalibrationStep
1
| rails generate scaffold CalibrationStep step_name:string step_number:integer |
While for the CalibrationResult, we will only generate the model:
1
| rails generate model CalibrationResult calibration_id:integer calibration_step_id:integer result:boolean note:string |
And then we end this step by migrating our database.
2. Setting up the association
Now, let’s code. I start from the Calibration model, here is what we code:
1 2 3 4 5
| class Calibration < ActiveRecord::Base
has_many :calibration_steps, :through => :calibration_results
has_many :calibration_results
accepts_nested_attributes_for :calibration_results
end |
You should note that it’s just ordinary association that we put in our Calibration model with a slight addition the accepts_nested_attributes_for for our nested form later on.
And then, here is our CalibrationStep model, nothing special really:
1 2 3 4
| class CalibrationStep < ActiveRecord::Base
has_many :calibrations, :through => :calibration_results
has_many :calibration_results
end |
And the last, our CalibrationResult model:
1 2 3 4
| class CalibrationResult < ActiveRecord::Base
belongs_to :calibration
belongs_to :calibration_step
end |
3. Modifying the controllers
The only controller we need to worry about is our CalibrationsController class. We should modify its show and new method. Here is our modification:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class CalibrationsController < ApplicationController
# some lines are deleted...
def show
@calibration = Calibration.find(params[:id])
@calibration_steps = CalibrationStep.find(:all, :order => 'step_number ASC')
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => @calibration }
end
end
def new
@calibration = Calibration.new
@calibration_steps = CalibrationStep.find(:all, :order => 'step_number ASC')
calibration_result = @calibration.calibration_results.build()
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => @calibration }
end
end
# some lines are deleted....
end |
You should notice that we added a variable called calibration_result with value generated by a method called build. This build method is the key ingridients of our nested form. This method, according to Rails documentation,
returns a new object of the collection type that has been instantiated with attributes and linked to this object through the join table, but has not yet been saved.
For detailed description about this, you can look it up here with keyword collection.build().
We can leave the other controllers as they are.
4. Modifying the views
There are at least three views that we need to tweak. The first one is our app/views/calibrations/_form.html.erb file:
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 27 28
| <%= form_for(@calibration) do |f| %>
# some lines are deleted....
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
</div>
<div class="field">
<%= f.label :tester %><br />
<%= f.text_field :tester %>
</div>
<div class="field">
<table border="1">
<tr>
<td>Step Name</td>
<td>Result</td>
<td>Note</td>
</tr>
<% @calibration_steps.each do |calibration_step| %>
<%= f.fields_for :calibration_results do |builder| %>
<%= render 'calibration_result_fields', :f => builder, :calibration => @calibration, :calibration_step => calibration_step %>
<% end %>
<% end %>
</table>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %> |
Then, we need to add the partial view we described in our form earlier by adding this app/views/calibrations/_calibration_result_fields.html.erb file:
1 2 3 4 5 6 7 8 9 10 11
| <tr>
<td><%=h calibration_step.step_name %></td>
<td>
<%= f.radio_button :result, true %>Pass
<%= f.radio_button :result, false %>Fail
</td>
<td>
<%= f.text_field :note %>
<%= f.hidden_field :calibration_step_id, :value => calibration_step.id %>
</td>
</tr> |
And the last, we need to show the result. For this tutorial, I will just put it in my Calibration’s view that is in app/views/calibrations/show.html.erb file:
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| <p id="notice"><%= notice %></p>
<p>
<b>Name:</b>
<%= @calibration.name %>
</p>
<p>
<b>Tester:</b>
<%= @calibration.tester %>
</p>
<table border="1">
<tr>
<td>Step Name</td>
<td>Result</td>
<td>Note</td>
</tr>
<% @calibration_steps.each do |calibration_step| %>
<tr>
<td><%= calibration_step.step_name %></td>
<% calibration_result = CalibrationResult.find(
:first, :conditions => {:calibration_step_id => calibration_step.id, :calibration_id => @calibration.id}) %>
<% if !calibration_result.nil? %>
<td>
<% if !calibration_result.result.nil? %>
<%= calibration_result.result==true ? "Pass" : "Fail" %>
<% else %>
Not yet tested
<% end %>
</td>
<td><%=h calibration_result.note %></td>
<% else %>
<td>Not yet tested</td>
<td>Not yet tested</td>
<% end %>
</tr>
<% end %>
</table>
<%= link_to 'Edit', edit_calibration_path(@calibration) %> |
<%= link_to 'Back', calibrations_path %> |
5. That’s it
That’s it. Now, you can try to add your calibration steps and then try to use the nested form in your calibration’s view. Here is a screenshot from my own trial:

the look of the nested form