dynamic drop downs for product search

Hey Everyone,

I'm about to embark on having a search function for the products on my app via dynamic dropdowns. A dropdown for make would only leave AVAILABLE models of that make. selecting year would only leave available makes from that year etc etc.

Any suggestions or good read ups ?

Any suggestions or good read ups ?

One place to start is with the Railscast on dynamic select menus:

It is a bit old, (I believe it uses Prototype) but it is a good starting point.

Eric

So I have implemented my own version of dynamic drop down menus. (This is my first rails project nearing an end. Feel free to point out better methods, criticism taken construction-ally)

So one thing to keep in mind with the drop downs are they don't have to be used in a linear fashion. The three options are year, make, model. Model is by default empty due to how many potential models they have in stock. (Oh yeah we only display in-stock vehicles). So you could start the search by either selecting Year or Make. If you Select a particular make, Models of that make will become available. As well Years will be refreshed to only display Years with that particular Make. Then you could further refine the options. To a particular Year. This will adjust the Models dropdown again to only show Models of that particular Year and Make.

You may step backward with any of the dropdown by re-selecting the prompt option.

We're dealing with a Vehicle Model. the attributes explain themselves. I have a bad tendency of using random variable names which I tend to clean up later. This is pre-clean up so if something looks weird it probably is lol.

To start,

In my vehicle_controller I use a before filter:   before_filter :vehicle_select, :only => [:index, :preauctionspecials, :show]

and as a private method in application_controller:   def vehicle_select     @availableYears = Vehicle.find(:all, :select => "DISTINCT year", :order => "year DESC")     @availableMakes = Vehicle.find(:all, :select => "DISTINCT make", :order => "make DESC")   end

These are the default lists to fill the drop down menus. Which we will use in the application.html erb layout file:

<div class="box" id="searchBox"> <form id="vehicleSearch" action="/vehicles">   <ul>     <li id="yearSelect"><%= render :partial => 'vehicles/ makeSelect', :locals => { :list => @availableYears, :box => :year, :selected => nil } %></li>     <li id="makeSelect"><%= render :partial => 'vehicles/ makeSelect', :locals => { :list => @availableMakes, :box => :make, :selected => nil } %></li>     <li id="modelSelect"><%= render :partial => 'vehicles/ makeSelect', :locals => { :list => , :box => :model, :selected => nil } %></li>     <li><input type="submit" value="Search" /></li>   </ul> </form> </div>

Which utilizes the following partial: Select <%= box %>: <%= collection_select(:Vehicle, box, list, box, box,                     {:prompt => "Search by #{box}", :selected => selected},                     {:onchange => "#{remote_function(:url => {:action => "update_dropdowns"},                                                      :with => "'current_id='+id+

'&current_value='+value+

'&fields='+

'year='+$(Vehicle_year).value+','+

'make='+$(Vehicle_make).value+','+

'model='+$(Vehicle_model).value")}"}) %>

Defining :box allows me to both use the single parameter to fill the prompt, text outside the options as well as the select name (Vehicle[year],Vehicle[make] or Vehicle[model])

Each dropdown then uses the remote_function ajax helper. It passes the current contents of all the boxes as well as defines which box the change is coming from. I need to know all this information every time because of the non-linear ability to change the boxes. We must see which boxes have already been changed to make the current values :selected option as well as refine all options in every field.

At this point when an option is changed we goto out vehicle_controller:

  def update_dropdowns

    toSend = # creates an empty array     cond = {} # creates an empty hash

    if !params.blank?       fields = params[:fields].split(',').collect{ |s| s.split('=').collect } # this will accept all params, find the fields passed from the form and separate the content. All the fields and values are passed in a single parameter to avoid parsing every paramter passed. Instead we just check one parameter with every value. Otherwise the authenticity_token, action, controller parameters were all getting parsed as well.

# After reading all the field values appropriately generate required parameters based off the non-null values       fields.each do |param|         if (!param[1].blank?)           cond.store(param[0], param[1])         end       end     end

    if !cond.empty?       fields.each do |param|         @objs = Vehicle.find(:all,                               :conditions => cond,                               :select => "DISTINCT #{param[0]}",                               :order => "#{param[0]} DESC"                               )

        toSend << [param[0], @objs, param[1]] #Save the results of the query as well as the dropdown it came from       end     else #if no values are selected (all prompts are selected again by the user) reset all fields       toSend << ['year', Vehicle.find(:all, :select => "DISTINCT year", :order => "year DESC"), nil]       toSend << ['make', Vehicle.find(:all, :select => "DISTINCT make", :order => "make DESC"), nil]       toSend << ['model', , nil]

    end

#once we have a complete array of the new contents for the dropdowns pass the array to the page display function:     change_selects(toSend)

  end

Due to some values being numbers (year and price_sticker, currently removed) A check has to occur. If the Year string is not turned into and integer The dropdown_select will not recognize it as the :selected item. If the string comes from a numeric field we change it to an integer before passing it back to the partial.

protected

  def change_selects(selectsToChange)     render :update do |page|       selectsToChange.each do |item|         page.replace_html item[0].to_s+"Select", :partial => "vehicles/ makeSelect", :locals => { :list => item[1], :box => item[0], :selected => item[0] == 'year' || item[0] == 'price_sticker' ? item[2].to_i : item[2] }       end     end   end

Well that is it. It's probably the first non-standard functionality I've really written myself with rails so I understand if it needs some work.

I'd be more then happy to hear suggestions on improving my rails abilities,

thanks for reading,

brianp