How to maintain the ordering for nested attributes when using accepts_nested_attributes_for in a Rails application
Asked Answered
P

3

23

Here is the parent model:

class TypeWell < ActiveRecord::Base
   ...

  has_many :type_well_phases, :dependent => :destroy
  accepts_nested_attributes_for :type_well_phases, :reject_if => lambda { |a| a[:phase_id].blank? }, :allow_destroy => true

  ...
end

Here is the nested model:

class TypeWellPhase < ActiveRecord::Base

  belongs_to :type_well
  belongs_to :phase

end

Here is the Phase model:

class Phase < ActiveRecord::Base
  ... 
  has_many :type_well_phases
  ...
end

I add nested records in child table (TypeWellPhases) by copying ALL records from my phases (Phase model) table in the parent model's controller as shown below:

class TypeWellsController < ResourceController
   ...
  def new
    @new_heading = "New Type Well - Computed"
    @type_well   = TypeWell.new
    initialize_phase_fields
  end

  private

  def initialize_phase_fields
    Phase.order("id").all.each do |p|
      type_well_phase               = @type_well.type_well_phases.build
      type_well_phase.phase_id      = p.id
      type_well_phase.gw_heat_value = p.gw_heat_value
    end
  end
  ...
end

I do this because I want to maintain a specific order by the children fields that are added. The part of the code Phase.order("id") is for that since the phases table has these records in a specific order.

After this I use the simple_form_for and simple_fields_for helpers as shown below in my form partial:

= simple_form_for @type_well do |f|
    ...
    #type_well_phases
      = f.simple_fields_for :type_well_phases do |type_well_phase|
        = render "type_well_phase_fields", :f => type_well_phase

Everything works as desired; most of the times. However, sometimes the ordering of Child rows in the form gets messed up after it has been saved. The order is important in this application that is why I explicitly do this ordering in the private method in the controller.

I am using the "cocoon" gem for adding removing child records. I am not sure as to why this order gets messed up sometimes.

Sorry for such a long post, but I wanted to provide all the pertinent details up front.

Appreciate any pointers.

Bharat

Principe answered 8/5, 2012 at 20:16 Comment(0)
P
58

I'll explain you in a more generic way. Say, you have Product and Order models:

= form_for @product do |f|
    ...
    = f.fields_for :orders do |order_fields|
        = order_fields.text_field :name

If you want your orders to be sorted by name then just sort them :)

Instead of:

    = f.fields_for :orders do |order_fields|

put:

    = f.fields_for :orders, f.object.orders.order(:name) do |order_fields|

As you see, the f variable that is a parameter of the block of form_for has method object. It's your @product, so you can fetch its orders via .orders and then apply needed sorting via .order(:name) (sorry for this little confusion: order/orders).

The key to your solution that you can pass sorted orders as the second parameter for fields_for.

P.S. Your using the simple_form gem doesn't affect my solution. It'll work if you add 'simple_' to helpers. Just wanted my answer to be more helpful for others and not too task-related.

Palladium answered 8/5, 2012 at 20:39 Comment(8)
Thank you for your excellent explanation. I just have a follow up question: how did you know this? I read and re-read the documentation and could not even think of this. In other words, teach me to fish :)Principe
There is no magic, @Bharat. It's in official documentation in plain sight. Go to api.rubyonrails.org/classes/ActionView/Helpers/… and scroll down to the words "Or a collection to be used".Palladium
Helped me solving my Question her #13699785Transportation
Caveat : This solution does not keep pre-built objects, i.e. if you do @product.orders.build in your controller, you do not get a fresh object, only persisted ones.Cass
@Palladium Will this also work on ActiveRecord::Associations::CollectionProxy ?Levant
@Cass how to order pre-built objects in the form ?Levant
@DevR simply do not use .order(:name), but rather .sort_by{|o| o.name.to_s }, so that the sorting is operated on a ruby array rather than by the mean of an SQL query.Cass
Excellent generic approach explanation. It is important to highlight that you can break down your view by using collections: person_form.fields_for :projects, @green_projects do |project_fields| and thus have the absolute flexibility to generate other controller objects to work with person_form.fields_for :projects, @purple_projects do |project_fields|Luigi
E
1

If you are using Rails 2.3.14 or older you have to use:

f.fields_for :orders, f.object.orders.all(:order => :name) do |order_fields|
Enjoin answered 30/7, 2013 at 4:11 Comment(0)
S
1

I use this way:

class League < ActiveRecord::Base
  has_many :rounds, -> { sort_by_number }, dependent: :destroy
end

class League::Round < ActiveRecord::Base
  belongs_to :league
  scope :sort_by_number, -> { order('league_rounds.number ASC') }
end

In the view

= form_for league do |f|
  = f.fields_for :rounds do |round_form|
    # Here rounds are sorted by sort_by_number

This approach allows the use of any scope defined in the model. This approach allows the creation of several differently sorted associations.

Superheat answered 16/2, 2022 at 11:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.