Search code examples
ruby-on-railsnestedclonecocoon-gem

Add clone button on each nested child object with Cocoon and Rails


I have a parent, which I can add multiple children and I would like to add a clone button with cocoon in each child.

Following the next solution I have made this code:

These are the models:

class MantenimientoSetup < ApplicationRecord
  has_many :actuation_setups, inverse_of: :mantenimiento_setup, validate: true, autosave: true, dependent: :destroy
  accepts_nested_attributes_for :actuation_setups, allow_destroy: true, reject_if: :all_blank
end
class ActuationSetup < ApplicationRecord
  belongs_to :mantenimiento_setup, inverse_of: :actuation_setups
end

I have got to add the clone button out of child partial... parent and child form

but I need this button for each new child nested, like this: enter image description here

These are the parent and child partials:

<!-- views/mantenimiento_setups/_form.html.erb -->
<%= form_for @mantenimiento_setup do |f| %>
  ...
  ...
  ...

  <div class="panel panel-default">
    <div class="panel-heading">
      <h4 class="panel-title">Configuraciones</h4>
    </div>
    <div class="panel-body mantenimiento-setup-element">
      <%= f.fields_for :actuation_setups do |act_setup_builder| -%>
        <%= render 'actuation_setup_fields', f: act_setup_builder %>
        <%= link_to_add_association 'Clone', f, :actuation_setups, data: {"association-insertion-node" => ".mantenimiento-setup-element", "association-insertion-method" => "append"}, wrap_object: Proc.new {|d| d = act_setup_builder.object.dup; d}, class: "btn btn-info btn-sm" %>
      <% end -%>
    </div>
    <div class="panel-footer">
      <%= link_to_add_fields 'Add new setup', f, :actuation_setups, {mantenimiento_setup: f.object}, {class: "btn btn-success", nested_form: "/mantenimiento_setups/actuation_setup_fields" }%>
    </div>
  </div>

  <%= save_button %>
<% end %>
<!-- views/mantenimiento_setups/_actuation_setup_fields.html.erb -->
<div class='row fields'>
  <div class="col-md-3 actuation-type-div-selector">
    
  ...
  ...
  ...

  <div class="col-md-1">
    <label class="control-label invisible">...</label><br/>
    <%= link_to_remove_fields "Remove", f%>
  </div>

</div>

If I paste the clone button in the child partial, close to remove button...

<!-- views/mantenimiento_setups/_actuation_setup_fields.html.erb -->
<div class='row fields'>
  <div class="col-md-3 actuation-type-div-selector">
    
  ...
  ...
  ...

  <div class="col-md-1">
    <label class="control-label invisible">...</label><br/>
    <%= link_to_remove_fields "Remove", f%>
  </div>

  <div class="col-md-1">
    <label class="control-label invisible">...</label><br/>
    <%= link_to_add_association 'Clone', f, :actuation_setups, data: {"association-insertion-node" => ".mantenimiento-setup-element", "association-insertion-method" => "append"}, wrap_object: Proc.new {|d| d = act_setup_builder.object.dup; d}, class: "btn btn-info btn-sm" %>
  </div>
</div>

It returns this error because the association does not exist due to I am calling from child to child association:

Association actuation_setups doesn't exist on ActuationSetup

How can I add a clone button for each child?


Solution

  • The link_to_add_association needs the form-object (f) from the parent (where the associations are defined). So at the nested level, you need to be aware of the parent-form-object.

    Also: we would only want to render the Clone button if the record already exists. Maybe I should explain that better: the form and all Clone-partials are rendered on the server, and thus will not copy fields if they are edited/changed in the form. Is that clear? To be able to do that, we would need more javascript-code, and this might also be a very valid approach, maybe simpler: trigger the link_to_add_association link and in the cocoon:after-insert event we can prefill the fields copied from the to-be-cloned item, if there is one. But, as said, that would be a pure javascript solution.

    To remain close to your initial suggestion, your views would look like (I did not copy the entire view)

    <!-- views/mantenimiento_setups/_form.html.erb --> 
    
    <%= f.fields_for :actuation_setups do |act_setup_builder| -%>
      <%= render 'actuation_setup_fields', f: act_setup_builder, parent_f: f %>
    <% end -%>
    

    and then in your nested form you can write:

    <!-- views/mantenimiento_setups/_actuation_setup_fields.html.erb -->
    <div class='row fields'>
      <div class="col-md-3 actuation-type-div-selector">
            
      <div class="col-md-1">
        <label class="control-label invisible">...</label><br/>
        <%= link_to_remove_fields "Remove", f%>
      </div>
    
      <% if local_assigns.has_key?(:parent_f) %>
        <div class="col-md-1">
        
          <%= link_to_add_association 'Clone', parent_f, :actuation_setups, data: {"association-insertion-node" => ".mantenimiento-setup-element", "association-insertion-method" => "append"}, wrap_object: Proc.new {|d| d = act_setup_builder.object.dup; d}, class: "btn btn-info btn-sm" %>
        </div>
      <% end %>
    </div>
    

    (my erb is a little rusty, I always write haml or slim as I find them more readable and less typing-work, so there might be some typos).

    In short: for each existing :actuation_setup we hand down the parent_f parameter (the parent-form-object), and in the nested child we check if this parameter was given, and if so we know it was an existing child, so then we add the clone-link, and use the parent-form-object to be able to add a new nested child as a copy of the existing/given nested child.