Search code examples
ruby-on-rails-4has-many-throughhas-onenested-form-forcocoon-gem

Nested form with nested has_many, and has_one inside


I am essentially running into the same issue as this post, though I have a slightly different situation: has_many nested form with a has_one nested form within it

But as someone else mentioned in that post, the provided answer does not solve the issue.

The relationship is set up so that Invoice has_many items and each Item has_one Modifier. I am attempting to make a single form_for Invoice that allows a use to create many items, each with a Modifier.

MODELS

class Invoice < ActiveRecord::Base
  has_many :items
  has_many :modifiers, through: :items

  accepts_nested_attributes_for :items
end

class Item < ActiveRecord::Base
  belongs_to :invoice
  belongs_to :modifier

  accepts_nested_attributes_for :modifier
end

class Modifier < ActiveRecord::Base
  has_one :item
end

CONTROLLER

class Invoice
  def new
    @invoice = Invoice.new
  end

  def edit
  end

  ...
end

VIEWS(haml)

invoice.html.haml:
= form_for @invoice do |f|
  = f.text_field :status

  = f.fields_for :items do |builder|
    = render partial: "items/fields", locals: { :f => builder }
  = link_to_add_association 'New Item', f, :items, partial: "items/fields", id: "add-item-button"

items/_fields.html.haml:
.nested-fields
  - @item = @invoice.items.build
  = f.fields_for :modifier, @item.build_modifier do |modifier|
    = modifier.text_field :name

Let's review what's happening. In order to build the nested has_one relationship, I build an Item in the nested-fields partial so that I can build the has_one Modifier. This is because rails requires that you explicitly call 'build_something' in a has_one relationship (usually this is called in the controller's new, but I only want to build once someone has clicked the New Item button). For creating new Invoices, this code works perfectly. Checking console, I see that the relationship is created and I can verify that the Modifier was created successfully.

However, when I go back to edit the invoice, cocoon knows that I already have a modifier, so it calls the partial once to create the necessary fields_for my single Modifier. These fields are empty. This makes sense though, because as cocoon is rendering that partial, it is building a new Code with a new modifier and setting the fields blank. I can confirm that is what is occuring because once I have my modifier saved properly, I can go into my partial, remove the two build calls, and view the edit page which properly displays the saved Modifier info just fine.

Of course, now that I've removed my build calls, the form no longer saves any Modifiers that I create. So essentially, I need the build calls in there to build new Modifiers, but I can't have them in there if I want to view them.

Does anyone have a solution to this situation? I have found multiple stack overflow questions and none of them resolves this issue.


Solution

  • You say has_one but in your model I see has_many ?

    You nested partial items/_fields is wrong: you build an extra item and there is not need for that. Coccon, in the link_to_add_association builds a new item to insert.

    There are two ways to do what you want to do.

    1) In the partial

    To handle it correctly in your partial you can do the following (items/_fields.html.haml) :

    .nested-fields
      - f.object.build_modifier if f.object.new_record?  
      = f.fields_for :modifier do |modifier|
        = modifier.text_field :name 
    

    In rails, to refer to the object of the form you can use f.object. Note this will work, but we have to check if it is a newly created object. Alternatively we could just check if the modifier is present.

    2) Using the :wrap_object option ( documentation )

    Cocoon allows to execute some extra code with the newly created object. So in your case that would become:

    = link_to_add_association('New item', f, :items,
            :wrap_object => Proc.new { |item| item.build_modifier; item })