Reputation: 127
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
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
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
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
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