Edit: I have created a gem that makes it simple to set up cascading selects in Rails. Check it out here!
The following guide details how to get up and running with cascading selects to forms in Rails using jquery-dynamic-select.
Cascading selects allows you to select an option in one select field, and have it dynamically populate another field on the form based on your selection. For example, if you had a form for entering car information, the field Make would determine the values to be shown in Model. (e.g. Nissan → Altima, Maxima, etc.) Using this jQuery library will allow you to populate the second select field without having to reload the page.
The example implementation will implement a cascading select for a Terminal form. A Terminal belongs to a Serving Area, which itself belongs to an Exchange. In order to properly create a Terminal, we must first select an Exchange, and from that Exchange select one of its associated Serving Areas.
To start, create a file in your javascripts directory, like app/assets/javascripts/jquery-dynamic-selectable.coffee, with the following code:
$.fn.extend
dynamicSelectable: ->
$(@).each (i, el) ->
new DynamicSelectable($(el))
class DynamicSelectable
constructor: ($select) ->
@init($select)
init: ($select) ->
@urlTemplate = $select.data('dynamicSelectableUrl')
@$targetSelect = $($select.data('dynamicSelectableTarget'))
$select.on 'change', =>
@clearTarget()
url = @constructUrl($select.val())
if url
$.getJSON url, (data) =>
$.each data, (index, el) =>
@$targetSelect.append "<option value='#{el.id}'>#{el.name}</option>"
@reinitializeTarget()
else
@reinitializeTarget()
reinitializeTarget: ->
@$targetSelect.trigger("change")
clearTarget: ->
@$targetSelect.html('<option></option>')
constructUrl: (id) ->
if id && id != ''
@urlTemplate.replace(/:.+_id/, id)
$ ->
$('select[data-dynamic-selectable-url][data-dynamic-selectable-target]').dynamicSelectable()Thanks to the two lines at the bottom of this script, dynamicSelectable will automatically be called on an select with the attributes selectable-url and selectable-target.
Since the above script uses Coffeescript, you will want to explicitly require coffee-script in your Gemfile if you have not already:
gem 'coffee-script'The next step is to create routes that will be used to look up the data necessary to populate the select fields.
namespace :select do
get ':exchange_id/serving_areas', to: 'serving_areas#index', as: :serving_areas
endThis route will be used to look up the Serving Areas for a given Exchange by its id. This action is facilitated by a controller as follows:
class Select::ServingAreasController < ApplicationController
# Include this if you are authenticating users with Devise
# skip_before_filter :authenticate_user!
def index
@serving_areas = ServingArea.where(exchange_id: params[:exchange_id]).select('id, name').order('name ASC')
render json: @serving_areas
end
endUsing the provided exchange_id, the index method of this controller will look up the corresponding Serving Areas and grab their id and name values, which will be used to populate the select field. In this example, the Serving Areas are also ordered alphabetically by name.
Now that everything is in place, we can add the two select fields to our form:
<div class="form-group">
<%= label_tag :exchange_id %>
<%= collection_select nil, nil, exchanges_with_serving_areas, :id, :code,
{ selected: exchange_id(@terminal), include_blank: true },
{ class: 'form-control', data: { dynamic_selectable_url: select_serving_areas_path(':exchange_id'),
dynamic_selectable_target: '#terminal_serving_area_id' } } %>
</div>
<div class="form-group">
<%= f.label :serving_area_id %>
<%= f.collection_select :serving_area_id, exchange_serving_areas(@terminal), :id, :name, {}, class: 'form-control' %>
</div>Note the following about the first field:
- An
exchange_idis not referenced directly by a Terminal, so we uselabel_tagandcollection_selectas opposed tof.labelandf.collection_select - Additionally, we leave the first two
objectandmethodvalues nil - We specify that this is the select for the
exchange_idwithin thedynamic_selectable_url - We specify the ID of dependent select in the
dynamic_selectable_target- Notice that we use the target
#terminal_serving_area_idinstead of#serving_area_id. We must do this because Rails will automatically transform:serving_area_idin the following collection_select into the id#terminal_serving_area_idas a consequence of using the form helper.
- Notice that we use the target
- The helper method
exchanges_with_serving_areaswill populate the collection with Exchanges that have Serving Areas attached to them, in order to prevent the selection of an Exchange with no Serving Areas. - You can use the
selectedoption to automatically select he current Exchange by its id in the event that you have reached this form using an edit action
Note the following about the second field:
- This field is formatted as a pretty standard collection_select
- We use the helper method
exchange_serving_areasto populate the collection with the Serving Areas of the current Exchange if we have reached this form using an edit action. Otherwise, the value here will be [].
Now that we have everything in place, we should be good to go. Selecting an Exchange from the first select will issue a call to our Select::ServingAreasController which will return JSON containing id-name pairs for each of the Serving Areas under the provided exchange_id. This data will populate the second select and we will be able to select from a list of Serving Areas belonging to the Exchange that we first selected.