a guide to doing nested model forms in rails (3.1)

One recurring problem when doing Ruby on Rails development is a nested model form.

Nested Model Form: a single form that contains multiple, nested models.

For example a project with its tasks. A nested model form will allow you to create or edit, in 1 form, the project and each of its tasks.

With the use of cocoon, this post will describe how to create a nested model form for some (all?) of the possible relations.

All these examples start from the project, so we will just start with a dummy project as follows:

rails g scaffold Project name:string

That's all we need to get started. In these examples i am using simple_form and slim (which looks a lot like haml, but is much faster).

The simple :has_many

The simple :has_many is the most occurring relation (unfortunately no scientific data to back that up). The simple :has_many is a relation where the child cannot exist without the parent, and where each child can have only one parent. A typical example is the project and his tasks. Each task is unique, each task only exists because of the project. If the project is gone, so are the tasks.

First the models:

class Project < ActiveRecord::Base 
  has_many :tasks 
  accepts_nested_attributes_for :tasks, :reject_if => :all_blank, :allow_destroy => true
end 

class Task < ActiveRecord::Base 
  belongs_to :project, inverse_of: :tasks 
end

The accepts_nested_attributes_for method makes sure that when posting the project-data, it will accept the attributes for the nested model.

Secondly we add the views. Inside the project _form.html.slim we add

  #tasks
    = f.simple_fields_for :tasks do |task|
      = render 'task_fields', :f => task
    .links
      = link_to_add_association 'add task', f, :tasks

Create a partial called projects_task_fields.html.slim which looks like:

.nested-fields
  = f.input :name
  = f.input :description
  = f.input :done, :as => :boolean
  = link_to_remove_association "remove task", f

The nested :has_many

Actually this is a simple extension of the previous example. For instance, a project with tasks, and each tasks has sub-tasks.

Our models:

  class Task < ActiveRecord::Base 
    has_many :sub_tasks 
    accepts_nested_attributes_for :sub_tasks, :reject_if => :all_blank, :allow_destroy => true 
end 

class SubTask < ActiveRecord::Base 
end

Edit the projects_task_fields.html.slim :

.nested-fields 
  = f.input :name 
  = f.input :description 
  = f.input :done, :as => :boolean 
  .sub_tasks 
    = f.simple_fields_for :sub_tasks do |sub_task|
    = render 'sub_task_fields', :f => sub_task 
    .links 
      = link_to_add_association 'add sub-task', f, :sub_tasks
  = link_to_remove_association "remove task", f

and add the view projects_sub_tasks.html.slim (which bears a lot of resemblance to the previous tasks-partial):

.nested-fields 
  = f.input :name 
  = f.input :description 
  = link_to_remove_association "remove sub-task", f

The look-up or create :belongs_to

A simple :belongs_to would mean that the parent would already exist, and a simple look-up (dropdown list) would suffice. If the parent is always unique, it can be solved in the same way as the :has_many. So, let's describe a solution for the case where we either select the item from a list, or create a new one.

Our project has an owner (which is a person):

    class Project < ActiveRecord::Base
      belongs_to :owner, :class_name => 'Person'
      accepts_nested_attributes_for :owner, :reject_if => :all_blank
    end
    class Person < ActiveRecord::Base
    end

inside the _projects/_form.html.slim we add the following:

    #owner
      #owner_from_list
        = f.association :owner, :collection => Person.all(:order => 'name'), :prompt => 'Choose an existing owner'
      = link_to_add_association 'add a new person as owner', f, :owner

Here we use the built-in way from simple_form to select an association from a look-up list: f.association. But secondly, we place the link_to_add_association that will render the partial projects/_owner_fields.html.slim and create a new Person and link to him.

The partial itself is pretty straightforward:

    .nested-fields
      = f.input :name
      = f.input :role
      = f.input :description
      = link_to_remove_association "remove owner", f

Now if you implement this, the form is functioning but not really user-friendly.

What we want is that when clicking the add a new person as owner-link, that the drop-down box and the link itself disappear. When we choose to remove the to-be created owner, that the drop-down and add-link reappear.

So we need to add a bit of extra javascript.

Open up app\javascripts\projects.js and add:

    $(document).ready(function() {
      $("#owner a.add_fields").
        data("association-insertion-position", 'before').
        data("association-insertion-node", 'this');

      $('#owner').bind('insertion-callback',
         function() {
           $("#owner_from_list").hide();
           $("#owner a.add_fields").hide();
         });
      $('#owner').bind("removal-callback",
         function() {
           $("#owner_from_list").show();
           $("#owner a.add_fields").show();
         });
    });

The first two lines control where the new partial (the new owner) will appear.

Upon insertion or removal, cocoon will trigger callbacks that are defined on the parent-container of the add-link. The last lines add those callbacks, and these will make sure that the link and dropdownbox will be hidden or shown again.

The :has_many :through relation

A classic example of this relation are tags. Something has tags, those can be chosen from an exisiting list of tags, one or more, or one could create new tags. This relation is, again, an extension of the previous solution.

This solution is a bit more complicated, because there is a bit more javascript involved here.

The models:

    class Project < ActiveRecord::Base
      has_many :project_tags
      has_many :tags, :through => :project_tags, :class_name => 'Tag'

      accepts_nested_attributes_for :tags
      accepts_nested_attributes_for :project_tags
    end
 
    class ProjectTag < ActiveRecord::Base
      belongs_to :tag
      belongs_to :project

      accepts_nested_attributes_for :tag, :reject_if => :all_blank
    end

    class Tag < ActiveRecord::Base
    end

inside the projects/_form.html.slim add:

    #tags
      = f.simple_fields_for :project_tags do |project_tag|
        = render 'project_tag_fields', :f => project_tag
      = link_to_add_association 'add a tag', f, :project_tags

Create a file projects/_project_tag_fields.html.slim containing:

.nested-fields.project-tag-fields
  #tag_from_list
    = f.association :tag, :collection => Tag.all(:order => 'name'), :prompt => 'Choose an existing tag'
  = link_to_add_association 'or create a new tag', f, :tag
  = link_to_remove_association "remove tag", f

And create another file projects/_tag_fields.html.slim:

.nested-fields
  = f.input :name, :hint => 'how should it be tagged'

When entering all this, it should be working, but it looks a bit confusing. The dropdown box does not disappear (as before). So we need the following bit of javascript to projects.js:

    $("#tags a.add_fields").
      data("association-insertion-position", 'before').
      data("association-insertion-node", 'this');

    $('#tags').on('cocoon:after-insert',
         function() {
             $(".project-tag-fields a.add_fields").
                 data("association-insertion-position", 'before').
                 data("association-insertion-node", 'this');
             $('.project-tag-fields').on('cocoon:after-insert',
                  function() {
                    $(this).children("#tag_from_list").remove();
                    $(this).children("a.add_fields").hide();
                  });
         });

While this javascript is doing almost the same as with the :belongs_to it is a bit more complicated, because the tag-partial is not yet inside in the page, so neither is the parent-div. Once that is clear, it is actually quite obvious what it does (I hope). Upon insertion of a new (project)tag, it also adds the callbacks for the insertion and removal of a new tag.

Example Code

All these examples are demonstrated in a working project: cocoon-simple-form-demo. If you can think of more relations you would like to see solved, or see an example of, please let me know.

Hope this helps.


Comments
Chris Habgood 2011-12-05 01:44:29 UTC

I have this relation, problem has_many :solutions. solution belongs_to :product. Product just has a name. Is there a way to get the product text box to show up properly nested without having to add a link to display it in the solutions partial? solutions partial: Solution '30X6' %&gt; product partial:

nathanvda 2011-12-05 07:31:12 UTC

(first off: your code samples are totally messed up, unfortunately) It depends: is the set of products fixed? If yes, I would just use the lookup `:belongs_to` in the solution-fields partial. So you just use <code> f.association :product, :collection => ... select the correct products here ... </code> If you need to be able to create new products, then you would need to use my third example from above, under a has_many. Hope this helps.

Tom 2012-10-05 21:07:13 UTC

I have a working nested form but for one thing ... I do not know how to dynamically add some extra attributes to one section of the form. The tricky part of this, at least for me, is that my nested form is centered on the join table, codelines. To complicate matters the extra attributes that I would like to add are from multiple models. The first three attributes are from the codes table and the last attribute is from the codelines table. I am an accountant, and so I do not have the sophistication to modify Ryan Bates' "link_to_add_fields" button to suit my needs. Nor, could I even hope to modify your solution "link_to_add_association", button. I have been learning rails through tutorials that I have bought online and I am excited about the process. Lately though I have hit a roadblock ... that is for the last six months. I was astounded to find your site and to read your guide addressing especially for me the belongs_to association. So ... let me come to the point ... I would like to keep the nested form that I have designed so far ... it is funtional, except that I cannot add extra rows of billing codes should I need. It here that I am trying to find a professional who would be willing to design that functionality for me, commented so I can learn, for a fee naturally. Would you be interested, and if so, I could send you a word document that defines the functionality that I am looking for. Once you have looked it over, maybe you could give me a bid on how much it would cost me to have you design this functionality ... that is if this functionality is even possible? Of course if I can afford you, I would count myself lucky to have stumbled on your site since I found your guide to be professional and of the quality of work I am willing to pay for. Thanks for your time. Tom.

Duncan Stuart 2014-08-16 21:02:23 UTC

Hi - looks like the way callbacks are handled has changed - could you possibly update? My guess is that cocoon:before-insert and cocoon:after-remove are the correct replacements?

nathanvda 2014-08-17 18:41:12 UTC

Hi @Duncan, since you already know the new event names, I gather you read the documentation on github, where the documentation actually _is_ updated? I updated it here now as well, thanks for bringing that to my attention. If it was not already clear, the relevant documentation is here: https://github.com/nathanvda/cocoon#callbacks-upon-insert-and-remove-of-items

Add comment

Recent comments

Tags

ruby on rails 34 ruby 26 rails3 17 rails 15 oracle 11 rspec 9 rspec2 7 jquery 7 ubuntu 5 javascript 5 windows 5 activerecord 3 refactoring 3 geoserver 3 gis 3 arrrrcamp 3 actionmailer 2 oracle spatial 2 tdd 2 postgis 2 routing 2 rvm 2 mongoid 2 csharp 2 thin 2 win32 2 gem 2 rails4 2 git 2 service 2 haml 2 cucumber 2 view testing 2 i18n 1 displaysleep 1 spatial 1 gemsets 1 wubi 1 oracle_enhanced_adapter 1 migrations 1 watchr 1 ci 1 plugins 1 coderetreat 1 ie8 1 ssl 1 oci 1 nested model form 1 wcf 1 11.04 1 jsonp 1 ruby-oci8 1 teamcity 1 engines 1 pgadmin 1 soap 1 content_for 1 word automation 1 plugin 1 capybara 1 xml 1 bootstrap 1 migrate to rails3 1 mvc 1 unity 1 rendering 1 word2007 1 x64 1 limited stock 1 fast tests 1 pl/sql 1 delayed_job 1 pdf 1 test coverage 1 optimization 1 processing 1 borland 1 method_missing 1 cross-browser 1 devise 1 schema_plus 1 mongo 1 mongrel 1 dual boot 1 usability 1 mongrel_service 1 dba 1 mission statement 1 model 1 metadata 1 rcov 1 exceptions 1 image_tag 1 attachments 1 bde 1 css 1 yield 1 ajax 1 generative art 1 rails-assets 1 coordinate systems 1 submodules 1 netzke 1 ora-01031 1 authlogic 1 postgresql 1 shopping cart 1 agile 1 fast_tagger 1 subjective 1 wice_grid 1 generators 1 nvidia 1 mongodb 1 etsyhacks 1 staleobjecterror 1 session 1 jeweler 1 wordpress hacked 1 jasmine 1 heroku 1 rjs 1 life 1 unobtrusive-javascript 1 render_anywhere 1 html5 1 rails31 1 json 1 cocoon 1 mingw32 1 observe_field 1 osx 1 actionwebservice 1 testing 1 debugging 1 strings 1