Keeping order with has_many through

I have these models:

class App < ActiveRecord::Base
  has_many :inventories
  has_many :products, through: :inventories
end
    
class Inventory < ActiveRecord::Base
  belongs_to :app
  belongs_to :product
    
  default_scope { order('position ASC') }
end
    
class Product < ActiveRecord::Base
  has_many :inventories
  has_many :apps, through: :inventories
end

My goal is to be able to add products to apps and keep them in order. I have a position field on my inventories to keep track of the order. My implementation so far is to use <select>'s to list all the available products. I am also using cocoon to dynamically add and remove products.

I’m struggling to wrap my head around what parameters I need to accept (via strong_parameters) and how to format the view so they will be accepted/retrieved correctly from the database. I’m happy to post more code, I’m just very far down a few rat holes.

Any suggestions?

Update

I’m trying a different tactic. Instead of using form.fields_for :products, I’m trying form.fields_for :inventories. It’s sort of working so far. I’ll update this as I find new data.

I’m not much help on the specific issue, however, I would suggest writing tests for this behavior. Perhaps an integration test where you can verify that the items are displayed in the proper order. I know that doesn’t help you with your problem, but it’ll make it easier as you try different strategies.

Anyway, good luck! I hope some of the other folks can help you more!

Alright I think I have it figured out.

fields_for :inventories does work. The trick I had to use was setting the name for the select tag:

<%= select(:app, :inventories, products_for_select(f), {}, {name: "app[inventories_attributes][#{index(f)}][product_id]"}) %>
<%= hidden_field_tag "app[inventories_attributes][#{index(f)}][position]", index(f) %>

products_for_select and index are view helpers to keep the code a little cleaner

module AppsHelper
  def products_for_select(form)
    options_for_select(Product.all.map{ |product|
      [product.name, product.id]
    }, form.object.product_id)
  end
  
  def index(form)
    form.options[:child_index]
  end
end

This creates a params hash like this:

{"app"=>{"name"=>"Example",
  "inventories_attributes"=>{
    "0"=>{"product_id"=>"3", "position"=>"0", "_destroy"=>"false", "id"=>"68"},
    "1"=>{"product_id"=>"4", "position"=>"1", "_destroy"=>"false", "id"=>"42"}
  }
}, "commit"=>"Update App", "id"=>"3"}

The one caveat is when I create any new product, the position is new_inventories which the database treats as zero. To fix this, I made a before filter that scans the params for new objects and replaces the position:

def sanitize_position
    inventories = params[:app][:inventories_attributes]
    inventories.each_pair do |k, v|
      if v[:position] == 'new_inventories'
        params[:app][:inventories_attributes][k.to_sym][:position] = Inventory.where(app_id: params[:id]).last.position + 1
      end
    end
  end

The I assigned the before filter to the create and update actions:

  before_filter :sanitize_position, only: [:create, :update]

This is pretty hacky. If anyone has a better suggestion I’d love to hear it.