views:

194

answers:

2

I'm following Railscast 88 to create a dynamic dependent dropdown menu. http://railscasts.com/episodes/88-dynamic-select-menus

I'm rendering these dropdowns inside a partial that I'm using in a multi-model form. The form I'm using follows the Advanced Rails Recipes process by Ryan Bates. Because I'm rendering the dropdown inside a partial, I had to depart from strictly following the Railscast code. On the Railscast link provided above, comments 30-31 and 60-62 address these issues and provide an approach that I used.

For new records, everything is working great. I select a parent object from the dropdown, and the javascript dynamically limits the child options to only those items that are associated with the parent I selected. I'm able to save my selections and everything works great.

The problem is that when I go back to the edit page, and I click on the child selection dropdown, the constraints tying it to the parent object are no longer in place. I'm now able to select any child, whether or not it's connected to the parent. This is a major user experience issue because the list of child objects is just too long and complicated. I need the child options to always depend on the parent that is selected.

Here's my code:

Controller#javascripts

def dynamic_varieties
 @varieties = Variety.find(:all)
  respond_to do |format|
    format.js
  end
end

Views#javascripts #dynamic_varieties.js.erb

var varieties = new Array();
<% for variety in @varieties -%>
  varieties.push(new Array(<%= variety.product_id %>, '<%=h variety.name %>', <%= variety.id %>));
<% end -%>

function collectionSelected(e) {
  product_id = e.getValue();
  options = e.next(1).options;
  options.length = 1;
  varieties.each(function(variety) {
    if (variety[0] == product_id) {
      options[options.length] = new Option(variety[1], variety[2]);
    }
  }); 
}

Views#users #edit.html.erb

<% javascript 'dynamic_varieties' %>
<%= render :partial => 'form' %>

View#users #_form.html.erb

<%= add_season_link "+ Add another product" %>  
<%= render :partial => 'season', :collection => @user.seasons %>

view#users #_season.html.erb

<div class="season">
<% new_or_existing = season.new_record? ? 'new' : 'existing' %>
<% prefix = "user[#{new_or_existing}_season_attributes][]" %>

<% fields_for prefix, season do |season_form| -%>
  <%= error_messages_for :season, :object => season %>
   <div class="each">
    <p class="drop">
      <label for = "user_product_id">Product:</label> <%= season_form.collection_select :product_id, Product.find(:all), :id, :name, {:prompt => "Select Product"}, {:onchange => "collectionSelected(this);"} %>
     <label for="user_variety_id">Variety:</label>
   <%= season_form.collection_select :variety_id, Variety.find(:all), :id, :name, :prompt => "Select Variety" %>
    </p>

    <p class="removeMarket">
      <%= link_to_function "- Remove Product", "if(confirm('Are you sure you want to delete this product?')) $(this).up('.season').remove()" %>
    </p>     
   </div>
<% end -%>

+1  A: 

I think you have two options:

  1. Give one of the products(or simply the first element of the product list) a "selected" attribute which will force the browser to select that one always.
  2. Trigger the "collectionSelected" function on "dom ready" or "window.onload" with giving the product list selectbox as its parameter.

And a note: never, ever trust JavaScript to force the user to send proper data to the server.

BYK
I'm a javascript noob. Don't totally understand #2. Could you explain a little more? Thanks! Regarding the note, my concern is really just user experience. By limiting the variety list, it becomes manageable and avoids unintentional errors in selecting varieties that are not related to the selected product.
MikeH
BYK
Tell me what you need to see, and I'll revise my question.
MikeH
First of all what is your select box's id? Second, do you use a JS framework if so, which one? Do you already have a window.onload or dom:ready event handler?
BYK
+2  A: 

Here's your culprit:

<%= season_form.collection_select :variety_id, Variety.find(:all),
  :id, :name, :prompt => "Select Variety" %>

Works perfectly on a new record because it's showing everything, and gets overwritten when the select changes on the other select box.

You need to do something like this:

<% varieties = season.product ? season.product.varieties : Variety.all %>
<%= season_form.select :variety_id, 
  options_from_collection_for_select(varieties, :id, 
  :name, season.variety_id), :prompt => "Select Variety" %>

Which will use only the Varieties linked to season.product. If season.product doesn't exist it lists all of them. It will also automatically select the right one if the existing record had a variety_id.

It also wouldn't hurt to change.

<%= season_form.collection_select :product_id, Product.find(:all), 
  :id, :name, {:prompt => "Select Product"}, 
  {:onchange => "collectionSelected(this);"} %>

to

<%= season_form.select :product_id,  
  options_from_collection_for_select(Product.find(:all),
  :id, :name, season.product), {:prompt => "Select Product"},
  {:onchange => "collectionSelected(this);"} %>

Which will select the proper product on page load. This second part is essentially the Rails way of doing what BYK's first suggestion was. However, given the nature of the onchange method given to the select box, this line on its own would not solve the problem. It would just enhance the user experience by highlighting the product associated with the season.

EmFi
YES! You rock. Been trying to solve that one for a month.
MikeH
Nicely done =) I think I have to start learning Ruby =)
BYK
All of that is Rails, which does most of heavy lifting.
EmFi
Hey EmFi, quick little follow-up if you could. This wasn't in my original question, but I had added :order => 'name' to that variety list. So it was Variety.find(:all, :order =>'name'). Been trying to fit the :order parameter in somewhere in the new setup, but nothing I've tried seems to work. Any ideas?
MikeH
You can chain a find call onto any association to limit the selection. You want to do: `season.product.varieties.find(:order => name)` If you find that you're always sorting by name it might make sense to set that up as your default scope.
EmFi
Thanks. Very helpful.
MikeH