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.
1 | rails new calibrator |
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.
1 | rake db:migrate |
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:

Thanks a lot!!!!
I have spent days on figuring out how to do this.
I was looking a step by step example and yo did exactly that.
Zillion Thanks!
Thanks for your post, it’s awesome, I can learn playing with nested forms :)
Doing what you do in my application is OK but when it comes to play with ‘edit’ action in the ‘calibration’ controller, the form isn’t OK.
There is something wrong with the builder I think but I can’t figure what is going wrong with my code.
Can you show me what ‘edit’ and ‘update’ actions should look like ?
Please :) I’m blocked !
Hi, Pierre.
I’ll update this post to show the “edit” part. Hopefully before this weekend ends. -it’s Saturday noon the moment I replied this in my timezone-
Thanks a lot :)
Hope you’ll have time to do it soon :)
I spent hours on this view without any idea on how to make it works !
Hi, Pierre.
So sorry that it took me so long to finally update this. I’ve created a new short post about the editing part. Hope it helps. http://iqbalfarabi.net/2011/06/07/rails-nested-form-with-has-many-through-association-part-2/
i already follows all your tutorial. i am copy and paste the code so i think there is no mistake.
the problem is when i run the new request, the radio button doesn’t show up so the record for the calibration_step doesn’t recorded.
i hope you can understand my english and my problem.
thanks for all of your kindness.. :)
Hello sir,
i am new to ROR, and was facing many problems in getting started with associations, but many of doubts got cleared after reading your post
your post was realy helpful
followed each step given above, but at the end got a SyntaxError in Calibrations#new
___________________________________________________________________________
SyntaxError in Calibrations#new
Showing app/views/calibrations/new.html.erb where line #22 raised:
compile error
/home/shubhi/ror/calibrator/app/views/calibrations/new.html.erb:22: syntax error, unexpected ‘)’
… :calibration_results do |a| ).to_s); @output_buffer.concat …
^
/home/shubhi/ror/calibrator/app/views/calibrations/new.html.erb:36: syntax error, unexpected kEND, expecting ‘)’
; end; @output_buffer.concat “\n \n \n \n ”
^
/home/shubhi/ror/calibrator/app/views/calibrations/new.html.erb:42: syntax error, unexpected kEND, expecting ‘)’
; end ; @output_buffer.concat “\n\n”
^
/home/shubhi/ror/calibrator/app/views/calibrations/new.html.erb:46: syntax error, unexpected kENSURE, expecting ‘)’
/home/shubhi/ror/calibrator/app/views/calibrations/new.html.erb:48: syntax error, unexpected kEND, expecting ‘)’
Extracted source (around line #22):
19: Note
20:
21:
22:
23:
24:
25:
_______________________________________________________________________
m using rails 2.3.11
it would be gr8 help if you could reply as early as possible