Alexander
Alexander

Reputation: 127

Rails 4 problems with updating/deleting fields in a model with nested fields

I've been working on introducing a nested form in my app following Railscast episode 196 http://railscasts.com/episodes/196-nested-model-form-revised and the remastered version for rails 4 https://github.com/dnewkerk/nested-model-form.

Let's say we have a 1-to-many association between receipts and articles.

Here's how their models look like:

receipt.rb:

class Receipt < ActiveRecord::Base
  has_many :articles, dependent: :destroy
  accepts_nested_attributes_for :articles, allow_destroy: true, reject_if: :all_blank

  belongs_to :shop
  belongs_to :user

  def display_name
    self.name
  end
end

article.rb:

class Article < ActiveRecord::Base
  belongs_to :receipt

  def name_with_brand
    "#{name} #{brand}"
  end
end

Here's how the receipts_controller.rb looks like:

class ReceiptsController < ApplicationController
  before_action :set_shop, only: [:show, :edit, :update, :destroy]

  respond_to :html, :xml, :json

  def index
    @receipts = current_user.receipts
    respond_with(@receipts)
  end

  def show
    respond_with(@receipt)
  end

  def new
   @receipt = Receipt.new
   2.times do
     @receipt.articles.build
   end
   respond_with(@receipt)
  end

  def edit
  end

  def create
    @receipt = Receipt.new(receipt_params)
    user_id = current_user.id

    @receipt.articles.each do |article|
      warranty_time = article.warranty_time
      article.warranty_expires = @receipt.shopping_date.advance(months: warranty_time)
    end

    @receipt.user_id = user_id
    @receipt.save
    respond_with(@receipt)
  end

  def update
    if @receipt.update(receipt_params)
      redirect_to @receipt, notice: "Successfully updated receipt."
    else
      render :edit
    end
  end

  def destroy
    @receipt.destroy
    respond_with(@receipt)
  end

  private

  def set_shop
    @receipt = Receipt.find(params[:id])
  end

  def receipt_params
    params.require(:receipt).permit(:name, :shopping_date, :shop_id, :file, 
    articles_attributes: [:id, :name, :brand, :warranty_time, :warranty_expires, 
                          :receipt_id,  :_destroy])
  end
end

Here's how my receipts.js.coffee looks like:

jQuery ->
  $('#receipt_shopping_date').datepicker(dateFormat: 'yy-mm-dd')
  $.datepicker.setDefaults($.datepicker.regional['PL']);


  $('form').on 'click', '.remove_fields', (event) ->
  $(this).prev('input[type=hidden]').val('1')
  $(this).closest('fieldset').hide()
  event.preventDefault()

  $('form').on 'click', '.add_fields', (event) ->
  time = new Date().getTime()
  regexp = new RegExp($(this).data('id'), 'g')
  $(this).before($(this).data('fields').replace(regexp, time))
  event.preventDefault()


$(document).ready(jQuery)
$(document).on('page:load', jQuery)

And finally here's my view for adding a new receipt and adding articles to it:

(other fields...)

<div class="large-12 columns">
<p>Add articles on the receipt:</p>
</div>

<div class="field">
  <div class="large-12 columns">


  <%= f.fields_for :articles do |builder| %>
        <div class="article_fields">
    <%= render "article_fields", :f => builder %>
        </div>
        <% end %>

    <%= link_to_add_fields "Add another article", f, :articles %>

  </div>
</div>


<div class="actions">
<div class="large-12 columns">
    <%= f.submit "Sumbit Receipt" %>
</div>
</div>


<% end %>

As you can see I'm using a link_to_add_fields helper method, here's how it looks like:

def link_to_add_fields(name, f, association)
new_object = f.object.send(association).klass.new
id = new_object.object_id
fields = f.fields_for(association, new_object, child_index: id) do |builder|
  render(association.to_s.singularize + "_fields", f: builder)
end
link_to(name, '#', class: "add_fields small button", data: {id: id, fields: fields.gsub("\n", "")}) 
end

And finally as you can see I'm generating a partial called _article_fields.html.erb, here's how it looks like:

<fieldset style="width:1400px">
<legend>new article</legend>

<div class="large-2 columns">
<%= f.text_field :name%>
</div>

<div class="large-2 columns">
<%= f.text_field :brand%>
</div>

<div class="large-2 columns">
<%= f.text_field :warranty_time, class: "warranty" %>
</div>

<div class="large-12 columns">
<%= link_to "delete article", '#', class: "remove_fields button small alert" %>
</div>

</fieldset>

Now let's get down to my problem. When creating a receipt for the first time everything is fine - I see the number of articles in a receipt in my show view and the warranty_expires in every article.

Things get messed up when I'm updating or deleting article_fields through receipts/edit:

1) When I edit a receipt and want to remove any of the articles (although visually in my edit view they disappear - the JS seems to work), the fields are not removed from my DB, thus the show view remains exactly the same like it was before.

Simple example:

before edit: my receipt has 6 articles

during edit: pressed 3 times the 'delete article' button, so the receipt should have 3 articles

after edit: the receipt has still 6 articles

2) When I edit a receipt and want to add an another article field, the value warranty_expires is always nil - how can I make it work with the update action in my receipts controller? I tried using the same code as in my create action:

@receipt.articles.each do |article|
warranty_time = article.warranty_time
article.warranty_expires = @receipt.shopping_date.advance(months: warranty_time)
end

but it won't work. Any idea why?

Simple example:

A receipt has 2 articles already. When I add the 3rd one I get the following result:

3 articles - all of them have names and warranty_time fields, but only 2 of them have a warranty_expires value.

All of your help would be deeply appreciated. Thank you in advance.

Upvotes: 1

Views: 529

Answers (4)

Charles Williams
Charles Williams

Reputation: 1

This is an issue where .hide() is called in receipts.js.coffee. The simplest way to fix this I can think of is to simply replace .hide() with .remove()

Upvotes: 0

Jorge Najera T
Jorge Najera T

Reputation: 1551

I think you can use some callbacks in your Article model for solve your 2nd problem,

Start deleting this, try to keep your controller as simple as possible and handle the operations in your models.

 @receipt.articles.each do |article|
  warranty_time = article.warranty_time
  article.warranty_expires = @receipt.shopping_date.advance(months: warranty_time)
end

In your Article Model add some callbacks

class Article < ActiveRecord::Base
  belongs_to :receipt

  def name_with_brand
    "#{name} #{brand}"
  end

  before_update :set_warranty_expires
  before_create :set_warranty_expires

  def set_warranty_expires
    self.warranty_expires = self.receipt.shopping_date.advance(months: self.warranty_time)
  end

end

Code its not tested, but its the idea. Hope it helps.

Check this two gems simple_form and nested_form this helps a lot when writing large forms and they play well with each other.

Upvotes: 2

monsur
monsur

Reputation: 601

First of all i noticed that, you have a loop in your reciepts_controller new action

2.times do 
  @receipt.articles.build 
end

This means that, article will be created for only 2 times for that reciept.

Better remove the loop so that you can add as many as article you want. For Issue number two add bellow line to edit action to your controller

@receipt.articles.build

I guess that would help you.

Also nested_form is a great gem for manage this kind of tasks.

 https://github.com/ryanb/nested_form

Check it out.

Upvotes: 0

Alexander
Alexander

Reputation: 127

Update: I managed to fix the 1st issue.

The fix for the 1st solution is the following:

the hidden field :_destroy was missing for removing articles.

So I needed to changed the following code:

<div class="large-12 columns">
<%= link_to "delete article", '#', class: "remove_fields button small alert" %>
</div>

to:

<div class="large-12 columns">
<%= f.hidden_field :_destroy %>
<%= link_to "delete article", '#', class: "remove_fields button small alert" %>
</div>

Still have no idea how to fix the 2nd issue though.

Upvotes: 0

Related Questions