13

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">&#8594; <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 %>
Community
  • 1
  • 1
Dudo
  • 4,002
  • 8
  • 32
  • 57
  • 1
    I'm not entirely clear on what's going wrong, but you may want to look into the Cocoon gem if possible: https://github.com/nathanvda/cocoon If you could be more clear on where and when the errors are occurring, it could help to clarify. Lots of code up there to sift through =) – Dan Dec 07 '13 at 02:21
  • Cocoon looks nice, but I'm not quite sure what it'd be doing for me. First issue, though... AJAXing my `_details partial` into the form fails. (The site I linked to has broken code in it that lets the partial in, but won't create anything upon submitting the form). Using `f.fields_for` gives me `` and if there were 2, the next should be 1, then 2, etc. Am I making sense? lol. I've been wracking my brain all day with this, I'm hanging on by a thread, haha! – Dudo Dec 07 '13 at 02:32
  • Please provide me the new action in your controller,Have you written build in the new action for order details like @order.order_details.build – kanna Dec 07 '13 at 05:31
  • Only thing in the controller is what you see. What should I be calling? – Dudo Dec 07 '13 at 06:14

1 Answers1

20

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:

  1. You need to handle the AJAX on the controller (route + controller action)
  2. You need to put your f.fields_for into partials (so they can be called with Ajax)
  3. 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!"
Richard Peck
  • 76,116
  • 9
  • 93
  • 147
  • Thanks for your help, Rich! Great explanation. I have the partial in place, and I'm using rails' 'built in' ajaxing. So, I click a button to add the child, `<%= link_to 'add line', add_detail_orders_path(company_id: params[:company_id], f: f, child_index: Time.now.to_i), remote: true %>`. Then I get `undefined method 'fields_for' for "#":String`... `params = {"child_index"=>"1386466882", "company_id"=>"1", "f"=>"#"}` – Dudo Dec 08 '13 at 01:58
  • Okay, your problem is that you're passing the `f` variable through to the controller from the view. The problem is that `f` is a FormBuilder object, and only works when it's called. Sending it via a path does not retain its state, which is why you're having this error – Richard Peck Dec 08 '13 at 12:01
  • The way around this is to build a new form in the partial you're using to create new fields (as can be seen in our `#app/views/messages/add_subscriber.html.erb` above -- this creates a new form object, which will allow you to create the fields you need with the original partial (from when you created the original form). This will work because you're using a `timestamp` for the `child_index`, ensuring each field will be unique & sequentially greater than the previous – Richard Peck Dec 08 '13 at 12:03
  • is `#app/views/messages/add_subscriber.html.erb` a partial? I'm used to them starting with an _underscore – Dudo Dec 09 '13 at 17:27
  • You're right, all partials start with an underscore. The `add_subscriber.html.erb` is called directly from the `add_subscriber` action in the `messages` controller, hence why it's not a partial – Richard Peck Dec 09 '13 at 18:32
  • http://www.ecorebox.com/orders/new?company_id=1 - I'm close... I added the `form_for` to the partial, and the child_index is playing nice (you're right, that's really the ticket, here - awesome). When I submit the form, only the very first child is saved. Any idea why, by looking at my HTML? Even though I render the same partial on page load, and with AJAX, the first instance doesn't have the form above it, while the rest do. (updated question) – Dudo Dec 09 '13 at 19:17
  • removing those 'orphan forms' from the html before submitting does nothing... hmmm – Dudo Dec 09 '13 at 20:54
  • Which orphan forms buddy? – Richard Peck Dec 09 '13 at 21:31
  • I'm just calling them orphan for lack of a better term. If you check that link... and click 'New Line' a few times... then inspect the html. Each of the `.row0`s have a form in them that's there just to help build the `fields_for`. They're moot, though, only one record is saved regardless of their presence, so the issue is somewhere else. I can't seem to pinpoint it – Dudo Dec 09 '13 at 21:45
  • Ohh that's something I forgot to mention - you need to use the JS to only append the f.fields_for data, leaving the form. I'll provide code tomorrow morning if you can wait until then? – Richard Peck Dec 09 '13 at 21:57
  • http://railscasts.com/episodes/196-nested-model-form-revised - I followed Ryan's guide... totally different than how either of us were doing it, but it works great! Thanks for your help, Rich! =) – Dudo Dec 09 '13 at 22:49
  • No problem! The RailsCast is great, but it's very inefficient (appends entire code to the DOM, and only works for single ID's). The method I explained was a way to keep the system DRY & scaleable – Richard Peck Dec 10 '13 at 09:22
  • I agree, it does load the code into the button, which is a bit weird. But, it does work for multiple ID's. It's a revised episode and he uses Time.now to get several unique children. /shrug It works for now =) – Dudo Dec 10 '13 at 21:24
  • Ohh! I'm not a pro subscriber, so I didn't see the revised episode. As long as it works for you! – Richard Peck Dec 11 '13 at 10:16
  • For some reason the "child-index" doesn't work for me, what worked for me is to say "index" as in here: "fields_for item, index: Time.now.to_i do |f|" Does somebody has experienced the same behaviour, I'm using Rails 3.2 – Tonatiuh Jan 10 '14 at 22:22
  • It might be a Rails 3.2 issue, but also, how are you doing your `fields_for`? Are you calling the form builder object before your `fields_for`, like this: `f.fields_for`? If you post a question about it, post the link here, and I'll see if I can write an answer for you! – Richard Peck Jan 11 '14 at 11:25
  • will some explain the difference between the partials "resources/message_subscriber_fields" and "message_subscriber_fields"? – androidharry Jan 30 '14 at 09:34
  • `resources/message_subscriber_fields` is for individual form elements, whereas the `message_subscriber_fields` will contain a form builder object – Richard Peck Jan 30 '14 at 09:39
  • Although looking at the code, I've confused myself!! We can go to chat if you need help? – Richard Peck Jan 30 '14 at 09:40
  • Your first link is no longer valid. Is there another tutorial you are aware of for this? – Rob Allen Mar 19 '16 at 22:01
  • 3
    Thanks for pointing this out - I was going to write a tutorial as I have not seen any others of comparable quality. Besides, that tutorial was for Rails 3. Would you like me to do this and post the link? – Richard Peck Mar 20 '16 at 07:39