Rails accepts_nested_attributes_for with f.fields_for and AJAX
I'm curious how to properly use accepts_nested_attributes_for
and f.fields_for
.
views/orders/new.html.erb
<%= form_for @order, html:{role: "form"} do |f| %>
<%= f.submit "Don't push...", remote: true %>
<%= f.text_field :invoice %>
<%= f.text_field :ordered_on %>
<%= f.text_field :delivered_on %>
<table id='order_form'>
<h3>Details</h3>
<tbody>
<%= render 'order_details/details', f: f %>
</tbody>
<%= link_to 'add line', new_order_detail_path(company_id: params[:company_id]), remote: true %>
<%= link_to 'new box', new_box_path, remote: true %>
</table>
<% end %>
views/order_details/_details.html.erb
<tr class='row0'>
<%= f.fields_for :order_details, child_index: child_index do |d| %>
<td><%= d.collection_select :box_id, @boxes, :id, :uid, {},
{ name: "box_id", class: 'form-control'} %></td>
<td><%= d.text_field :quantity, class: 'form-control' %></td>
<td><%= d.text_field :box_price, class: 'form-control' %></td>
<td><%= d.text_field :cb_price, class: 'form-control' %></td>
<td><%= d.text_field :mould_fees, class: 'form-control' %></td>
<td>$$$</td>
<% end %>
</tr>
<tr class='box0'>
<td colspan="6">→ <b><%= @b.uid %></b> | length: <%= @b.length %> | width: <%= @b.width %> | height: <%= @b.height %> | weight: <%= @b.weight %></td>
</tr>
controllers/orders_controller.rb (I'm pretty sure this is wrong... any help here would be greatly appreciated)
def create
@order = Order.create(params[:order])
if @order.save
flash[:success] = "Order #{@order.invoice} added!"
redirect_to current_user
else
render 'orders/new'
end
end
models/order.rb
class Order < ActiveRecord::Base
attr_accessible ..., :order_details_attributes
has_many :order_details
accepts_nested_attributes_for :order_details
end
The only way I've been able to get the partial to play nice is if I actually call the fields_for
as fields_for Order.new.order_details.build
. But that doesn't build the nested object at all. I need to use the f.fields_for
nomenclature to build the Order, and the OrderDetail. I can only build one, though. Which is my next issue.
See how there are buttons in there? It AJAXs rows into the form. If you click add line
, I get
NameError in Order_details#new
Showing D:/Dropbox/Apps/rails_projects/erbv2/app/views/order_details/new.js.erb where line #3 raised:
undefined local variable or method `f' for #<#<Class:0x5cf0a18>:0x5cbd718>
views/orders/add_detail.js.erb
$('#order_form tr.total').before("<%= j render partial: 'orders/details', locals: {f: @f, child_index: @ci} %>")
I don't know how to define f
... I checked out Rails AJAX: My partial needs a FormBuilder instance and a few others.
Any suggestions on how I should handle this? Using the code I have here... I was able to create a new order, with an associated order_details, but the box_id didn't save, and the company_id didn't save. I know this is kind of nasty, but I don't know where else to go.
UPDATE
routes:
resources :orders do
collection { get :add_detail }
end
this is way better than having a separate resource for the details. I didn't think of this before!
HTML form:
<%= form_for @order, company_id: params[:company_id], html:{role: "form"} do |f| %>
f. ...
<%= render partial: 'details', locals: { f: f } %> #first child
<%= link_to 'add line', add_detail_orders_path(company_id: params[:company_id]), remote: true %> #add subsequent children
<% end %>
Orders Controller:
def add_detail
@order = Order.build
@boxes = Company.find(params[:company_id]).boxes
@b = @boxes.first
@ci = Time.now.to_i
respond_with(@order, @boxes, @b, @ci)
end
_details partial
<%= form_for @order do |f| %>
<%= f.fields_for :order_details, child_index: @ci do |d| %>
<td><%= d.collection_select :box_id, @boxes, :id, :uid, {},
{class: 'form-control'} %></td>
<td><%= d.text_field :quantity, class: 'form-control' %></td>
<td><%= d.text_field :box_price, class: 'form-control' %></td>
<td><%= d.text_field :cb_price, class: 'form-control' %></td>
<td><%= d.text_field :mould_fees, class: 'form-control' %></td>
<td>$$$</td>
<% end %>
<% end %>
It Is Possible
There's a very, very good tutorial on this here: http://pikender.in/2013/04/20/child-forms-using-fields_for-through-ajax-rails-way/
We also recently implemented this type of form on one of our development apps. If you goto & http://emailsystem.herokuapp.com, sign up (free) and click "New Message". The "Subscribers" part uses this technology
BTW we did this manually. Cocoon actually looks really good, and seems to use the same principles as us. There's also a RailsCast, but this only works for single additions (I think)
f.fields_for
The way you do it is to use a series of partials which dynamically build the fields you need. From your code, it looks like you have the fundamentals in place (the form is working), so now it's a case of building several components to handle the AJAX request:
- You need to handle the AJAX on the controller (route + controller action)
- You need to put your f.fields_for into partials (so they can be called with Ajax)
- You need to handle the
build
functionality in the model
Handling AJAX With The Controller
Firstly, you need to handle the Ajax requests in the controller
To do this, you need to add a new "endpoint" to the routes. This is ours:
resources :messages, :except => [:index, :destroy] do
collection do
get :add_subscriber
end
end
The controller action then translates into:
#app/controllers/messages_controller.rb
#Ajax Add Subscriber
def add_subscriber
@message = Message.build
render "add_subscriber", :layout => false
end
Add Your f.fields_for
Into Partials
To handle this, you need to put your f.fields_for
into partials. Here is the code form our form:
#app/views/resources/_message_subscriber_fields.html.erb
<%= f.fields_for :message_subscribers, :child_index => child_index do |subscriber| %>
<%= subscriber.collection_select(:subscriber_id, Subscriber.where(:user_id => current_user.id), :id, :name_with_email, include_blank: 'Subscribers') %>
<% end %>
#app/views/messages/add_subscriber.html.erb
<%= form_for @message, :url => messages_path, :authenticity_token => false do |f| %>
<%= render :partial => "resources/message_subscriber_fields", locals: {f: f, child_index: Time.now.to_i} %>
<% end %>
#app/views/messages/new.html.erb
<% child_index = Time.now.to_i %>
<div id="subscribers">
<div class="title">Subscribers</div>
<%= render :partial => "message_subscriber_fields", locals: {f: f, child_index: child_index } %>
</div>
Extend Your Build Functionality To Your Model
To keep things dry, we just created a build
function in the model, which we can call each time:
#Build
def self.build
message = self.new
message.message_subscribers.build
message
end
Child_Index
Your best friend here is child_index
If you're adding multiple fields, the big problem you'll have is incrementing the [id]
of the field (this was the flaw we found with Ryan Bates' tutorial)
The way the first tutorial I posted solved this was to just set the child_index
of the new fields with Time.now.to_i
. This sets a unique id, and because the actual ID of the new field is irrelevant, you'll be able to add as many fields as you like with it
JQuery
#Add Subscriber
$ ->
$(document).on "click", "#add_subscriber", (e) ->
e.preventDefault();
#Ajax
$.ajax
url: '/messages/add_subscriber'
success: (data) ->
el_to_add = $(data).html()
$('#subscribers').append(el_to_add)
error: (data) ->
alert "Sorry, There Was An Error!"