I created a Form Object (a Ruby class that inherits from ActiveModel::Model) to add multiple records using a single form. I used Sam Slotsky’s Dynamically add nested forms post as a basis for my solution, but I wanted Cocoon to handle form nesting for my Rails 4.2 app.

Cocoon makes it trivial to add nested fields to your forms. However, it requires a model that accepts attributes for a child model via the ActiveRecord method accepts_nested_attributes_for.

I’ll illustrate my solution using Sam’s Contact model. Below is the form object class from his post:

require 'ostruct'
class ContactListForm
  include ActiveModel::Model

  attr_accessor :contacts
  
  def self.association association, klass
    @@attributes ||= {}
    @@attributes[association] = klass
  end

  association :contacts, Contact

  def self.reflect_on_association(association)
    data = { klass: @@attributes[association] }
    OpenStruct.new data
  end  

  def contacts_attributes=(attributes)
    @contacts ||= []
    attributes.each do |i, contact_params|
      @contacts.push(Contact.new(contact_params))
    end
  end
end

The changes I needed to make were simple. First, I followed Cocoon’s convention for the nested partials (ex. adding divs, naming the HTML classes after the associated model, etc). Secondly, I created an explicit initializer so that existing contacts are displayed as nested fields:

1
2
3
4
5
 def initialize(attributes={})
    super
    set_instance_variables # variables needed to render the form or to save objects
    @contacts ||= Contacts.where(...) # get the contacts based on the instance variables set above.
  end

Line 4 fetches any existing database records for the Contact model if the instance variable @contacts was not set by the contacts_attributes=(attributes) method shown in the earlier code block.

The initialize method calls contacts_attributes=(attributes) if the controller passed the params corresponding to the nested fields, i.e. contacts_attributes in our example.

The third and last thing I needed to handle is the params[_destroy] (see the Rails API) that Cocoon adds to indicate that a record should be destroyed. Here is my version of the contacts_attributes method that allows for records to be destroyed:

  def contacts_attributes=(attributes)  
    @contacts ||= []
    @destroy_contacts = []
    
    attributes.each do |index, contact_params|
      if contact_params['_destroy'] != "1" 
        @contacts.push(Contact.new(contact_params))
      else
        contact = Contact.find_by(contact_params) # you might want to add error handling here
        @destroy_contacts.push(contact)
      end
    end    
  end

Sam did not include a save method in the post, so here is mine:

  def save
    Contact.transaction do
      @destroy_contacts.destroy_all if @destroy_contacts.present?
      @contacts.each(&:save)
    end
  end