Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails Nested Attributes form_for for has_many with unlimited nested models using javascript

So I've got a pretty simple model with nested attributes set up:

class Property < ActiveRecord::Base
  has_many :trees
  accepts_nested_attributes for :trees
end

Then I build them in my controller:

class PropertiesController < ApplicationController

  def new
    @property = Properties.new
    @trees = @property.trees.build
  end
end

And then in my view (using slim):

= form_for @property |p|
  = p.text_field :name

  .tree
    = p.fields_for :trees do |t|
      = t.text_field :fruit
      = t.select :quantity, (1..1000).to_a

    = link_to 'Add Another Tree', new_properties_path, id: 'new-tree'

And then in my js (coffeescript) file, bound to the click event of the '#new-tree' anchor:

event.preventDefault()
tree = $(event.target).parents('.tree')
tree.after tree.clone(true)

This works as you would expect, however when I submit the form, only the last tree params get submitted. This is because in the generated html of the form_for I get something like this:

<input type="text" id="property_trees_attributes_0_fruit" name="property[trees_attributes][0][fruit]">
<select id="property_trees_attributes_0_quantity" name="property[trees_attributes][0][quantity]" class="valid">
  <option value="1">1</option>
  <option value="2">2</option>
  <option value="3">3</option>
  ...
</select>

And when I clone this html I get the name and id with the 0 in it, though I believe it should be a 1 to add additional tree params, etc.

I tried to compensate with the js by doing something like this:

event.preventDefault()
tree = $(event.target).parents('.tree')
clone = tree.clone()
number = parseInt(clone.find('input[type=text]').attr('id').match /[0-9]/)
next = number + 1
clone = clone.html().replace /_(#{number})_|\[(#{number})\]/, next
section.after clone

The point of the regex is to replace any numbers between two underscores or two brackets, like _0_ or [0]

My regex doesn't seem to work though. It replaces _0_ with just 1 and this solution seems messy to me anyways. I would make the regex simpler, however then I have to worry about it changing the value of the select menu options.

Any suggestions for cleaning this up? Am I missing something really obvious?

like image 356
goddamnyouryan Avatar asked Dec 11 '25 05:12

goddamnyouryan


1 Answers

The problem you have is you're just duplicating DOM elements - this is IMO a hack, as you're not building the objects you need before-hand (won't persist the data etc)

We've implemented an "add extra fields to f.fields_for" using Ajax before - there's a great tutorial here and a RailsCast here


child_index

The answer to your question is to use the child_index option in the fields_for helper, and to make that use an integer timestamp:

<% index = Time.now.to_i %>
<%= f.fields_for :trees, child_index: index do |fields| %>
    #your fields here
<% end %>

Code

Here's what we do:

#app/views/properties/new.html.erb
<%= form_for @property do |f| %>
   <%= render partial: "trees_fields", locals: { f: f, child_index: Time.now.to_i } %>
   <%= link_to "New Field", new_field_property_path %>
<% end %>

#app/views/properties/_trees_fields.html.erb
<%= f.fields_for :trees, child_index: child_index do |trees| %>
   <%= trees.text_field :your_attr %>
<% end %>

#app/controllers/properties_controller.rb
def new_field
    @property = Property.new
    @property.trees.build
    render "add_tree", layout: false
end

#app/views/properties/add_tree.html.erb
<%= form_for @property do |f| %>
      <%= render partial: "trees_fields", locals: {f: f, child_index: Time.now.to_i} %>
<% end %>

 #app/assets/javascripts/application.js.coffee
$ ->
  $(document).on "click", "#add_tree", (e) ->
     e.preventDefault();
     $.ajax
       url: '/messages/add_subscriber'
       success: (data) ->
              el_to_add = $(data).html()
          $('#trees_form').append(el_to_add)
       error: (data) ->
          alert "Sorry, There Was An Error!"
like image 153
Richard Peck Avatar answered Dec 13 '25 21:12

Richard Peck



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!