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: [ruby] rails g scaffold Project name:string [/ruby] That's all we need to get started. In these examples i am using
slim (which looks a lot like haml, but is much faster).
:has_many is the most occuring 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: [ruby] class Project < ActiveRecord::Base has_many :tasks accepts_nested_attributes_for :tasks, :reject_if => :all_blank, :allow_destroy => true end class Task < ActiveRecord::Base end [/ruby] 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 [ruby] #tasks = f.simple_fields_for :tasks do |task| = render 'task_fields', :f => task .links = link_to_add_association 'add task', f, :tasks [/ruby] Create a partial called
projects/_task_fields.html.slim which looks like: [ruby] .nested-fields = f.input :name = f.input :description = f.input :done, :as => :boolean = link_to_remove_association "remove task", f [/ruby]
Actually this is a simple extension of the previous example. For instance, a project with tasks, and each tasks has sub-tasks. Our models: [ruby] 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 [/ruby] Edit the
projects\_task_fields.html.slim : [ruby] .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 [/ruby] and add the view
projects\_sub_tasks.html.slim (which bears a lot of resemblance to the previous tasks-partial): [ruby] .nested-fields = f.input :name = f.input :description = link_to_remove_association "remove sub-task", f [/ruby]
The look-up or create
: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): [ruby] 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 [/ruby] inside the
_projects/_form.html.slim we add the following: [ruby] #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 [/ruby] 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: [ruby] .nested-fields = f.input :name = f.input :role = f.input :description = link_to_remove_association "remove owner", f [/ruby] Now if you implement this, the form is functioning but not really user-friendly. What we want is that when clicking the
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.
:has_many :through relation
projects/_form.html.slim add: [ruby] #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 [/ruby] Create a file
projects/_project_tag_fields.html.slim containing: [ruby] .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 [/ruby] And create another file
: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.
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.