javascriptjquerycocoon-gem

After clicking on destroy with cocoon, how to recalculate invoice without destroyed record


Context I'm using the cocoon gem to create nested invoice_rows for invoice. Everything works as expected (deleting, creating nested records etc.).

In the invoice formI am also keeping tracking of the most actual total_invoice amount with Javascript. With my Javascript code I am able to recalculate the total_invoice amount when the price, quantity or VAT changes or when a new invoice_row is created with cocoon.

Issue The issue occurs when I delete a nested record. Following the documentation, I'm trying to use cocoon:after-remove to trigger the event, but nothing gets triggered after I delete an invoice_row in my view.

Other attempt I also tried a click event on the delete button (see also the code commented out in the script). Unfortunately, the class of the deleted nested record is still picked up by my Javascript code and consequently is entered in the calculation of the total_invoice amount. (hence the cocoon:after-remove command I guess)

Code

invoice_form.html.erb

<div class="form-container col col-sm-6 col-lg-12">
  <%= simple_form_for [@hotel, @invoice] do |f|%>
<h5><strong><u><%= t('.invoice') %> </u></strong></h5>
    <!-- headers -->
    <div class="row">
      <div class="col col-sm-3"><b>description</b></div>
      <div class="col col-sm-2"><b>unit price</b></div>
      <div class="col col-sm-2"><b>amount</b></div>
      <div class="col col-sm-2"><b>VAT (%)</b></div>
      <div class="col col-sm-2"><b>Total</b></div>
    </div>
    <div class="border-invoice"></div>
    <!-- headers -->
  <%= f.simple_fields_for :invoice_rows do |invoice_row| %>
  <div class="reservation-details">
    <%= render 'invoice_row_fields', f: invoice_row %>
  </div>
  <% end %>

  <div id="check">
    <%= link_to_add_association f, :invoice_rows do %>
    <div class="option-add-option-price">
      <div class="prices-border">
        <i class="fas fa-plus"></i> Add another invoice line
      </div>
    </div>
    <% end %>
  </div>

  <div class="border-invoice"></div>
  <div class="row">
    <div class="col-sm-8"></div>
    <div class="col-sm-1"><b>Subtotal</b></div>
    <div class="col col-sm-2"><input type="text" class="field nettotal form-control"></div>
  </div>
  <br>
  <div class="row">
    <div class="col-sm-8"></div>
    <div class="col-sm-1">VAT</div>
    <div class="col col-sm-2"><input type="text" class="field vat-total form-control"></div>
  </div>
  <br>
  <div class="row">
    <div class="col-sm-8"></div>
    <div class="col-sm-1"><b>Total</b></div>
    <div class="col col-sm-2"><input type="text" class="field gross-total form-control"></div>
  </div>

    <div class="row">
      <div class="col col-sm-6"> <%= f.button :submit, t(".invoice_button"), class: "create-reservation-btn"%>
      </div>
    </div>
    <% end %>
  </div>

script for invoice form

 // $(document).on('click', '.delete-vat', function() { /* recalculate */
$(document).ready(function(){
    $('#check')
  .on('cocoon:after-remove', function() { /* recalculate */
    var result = 0
    var vat_result = 0
    var price = [];
    var quantity = [];
    var vat = [];
    console.log('yes')

    $('.price').each(function(i, obj) {
      price.push((Math.round(+obj.value*100)/100).toFixed(2));
    });

    $('.quantity').each(function(i, obj) {
      quantity.push(+obj.value);
    });

    $('.vat').each(function(i, obj) {
      vat.push(+obj.value);
    });

    var result = 0
    price.forEach((o,i)=>{
    $(".gross-total").eq( i ).val(o*quantity[i]);

    result += o*quantity[i];
    // console.log(result)

    $(".gross-total").val(result);
    });

    var vat_result = 0
    price.forEach((o,i)=>{
    $(".vat-total").eq( i ).val(o*vat[i]);

    vat_result += o*vat[i]/100 * quantity[i];

    $(".vat-total").val(vat_result);
    });
    var sub = result - vat_result
     $(".nettotal").val(sub);
  })

_invoice_row_fields.html.erb

<div class="nested-fields">
    <div class="row test">
      <div class="col col-sm-3"><%= f.input :description, placeholder: "Product or service description", label: false %></div>
      <div class="col col-sm-2"><%= f.input :price, placeholder: "Price incl. VAT", label: false, input_html:{class: "field price"}  %></div>
      <div class="col col-sm-2 "><%= f.input :amount, label: false, input_html:{class: "field quantity"} %></div>
      <div class="col col-sm-2"><%= f.collection_select :vat_percentage, @hotel.vat_groups, :vat_percentage, :vat_percentage, {prompt: "Select a VAT"}, {class: "form-control vat"} %></div>
      <div class="col col-sm-2"><input type="text" class="field subtotal form-control"></div>


    <div class="col col-sm-1">
      <%= link_to_remove_association f do %>
      <i class="fas fa-trash delete-vat"></i>
      <% end %>
    </div>
    </div>
</div>

Solution

  • When removing items using cocoon, they are in general not really removed, just hidden. This is because the form (and all changes), are only saved when posting the form to the server.

    So if you added a nested item, without saving the form, and then remove it, it is physically removed from the form (DOM). However, if a once-saved nested item is removed, it needs to be hidden, and the _destroy flag set, so the server knows which items to remove.

    So the handle this correctly in your javascript, you can use the :visible selector. E.g. do something like

    $('.price:visible').each(function() { ...
    

    Furthermore, your cocoon:after-remove is now registered on the #check div? You should register it on a surrounding div of the nested items, e.g. the form itself would work better imho.