Skip to content

Instantly share code, notes, and snippets.

@mattantonelli
Last active August 29, 2015 14:18
Show Gist options
  • Select an option

  • Save mattantonelli/53937d7f49e2e8c21204 to your computer and use it in GitHub Desktop.

Select an option

Save mattantonelli/53937d7f49e2e8c21204 to your computer and use it in GitHub Desktop.
Adding Cascading Selects to Rails Forms

Adding Cascading Selects to Rails Forms

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
end

This 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
end

Using 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_id is not referenced directly by a Terminal, so we use label_tag and collection_select as opposed to f.label and f.collection_select
  • Additionally, we leave the first two object and method values nil
  • We specify that this is the select for the exchange_id within the dynamic_selectable_url
  • We specify the ID of dependent select in the dynamic_selectable_target
    • Notice that we use the target #terminal_serving_area_id instead of #serving_area_id. We must do this because Rails will automatically transform :serving_area_id in the following collection_select into the id #terminal_serving_area_id as a consequence of using the form helper.
  • The helper method exchanges_with_serving_areas will 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 selected option 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_areas to 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment