I bet you have had an issue with dynamically populating a select that depends on the chosen value of another select. For example, when I choose a country, the state select should be populated with that country’s states. There are many solutions to this problem. You can find them with Google, but none suited me because of bugs or approaches involving caching. I had this issue again and again, and my googling was unsuccessful. Eventually, some time ago, I created a jQuery library and an approach that solves this problem and completely suits me. This approach assumes that you don’t use JavaScript frameworks like Ember.js or Angular.js. Otherwise, my solution may be unnecessary in your application.
Problem
This image will clarify the problem without any words:

Solution
The first thing I should draw attention to in my solution is a jQuery library:
app/assets/javascripts/jquery-dynamic-selectable.coffee
$.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>"
# reinitialize target select
@reinitializeTarget()
else
@reinitializeTarget()
reinitializeTarget: ->
@$targetSelect.trigger("change")
clearTarget: ->
@$targetSelect.html('<option></option>')
constructUrl: (id) ->
if id && id != ''
@urlTemplate.replace(/:.+_id/, id)This piece of code extends jQuery with a dynamicSelectable function that you can call on the select whose changes should trigger population of the dependent select. The watched select should have these data attributes: selectable-url and selectable-target. Their names explain what they are for:
selectable-url is a URL pattern with the model ID. For example: /dynamic_select/:country_id/states. In this case, the pattern should be populated with the chosen country ID (assuming that we add a listener to the country select), and a request will go to this URL to get JSON data for populating the related select.
selectable-target is a CSS selector for the select that should be populated with the JSON data received from the server.
I call the dynamicSelectable function for every select on the page that has both the selectable-url and selectable-target data attributes:
app/assets/javascripts/application.coffee
...
$ ->
$('select[data-dynamic-selectable-url][data-dynamic-selectable-target]').dynamicSelectable()To make our application work, we should have a controller responsible for the route /dynamic_select/:country_id/states. Firstly, have a look at how I generate the route:
config/routes.rb
...
namespace :dynamic_select do
get ':country_id/states', to: 'states#index', as: 'states'
end
...And this is how my controller looks:
app/controllers/dynamic_select/states_controller.rb
module DynamicSelect
class StatesController < ApplicationController
respond_to :json
def index
@states = State.where(:country_id => params[:country_id])
respond_with(@states)
end
end
endAnd this is a template for the index action:
app/views/dynamic_select/states/index.json.jbuilder
json.array!(@states) do |state|
json.extract! state, :name, :id
endThe JSON data returned from the server should not include root elements:
config/intializers/wrap_parameters.rb
...
ActiveSupport.on_load(:active_record) do
self.include_root_in_json = false
endAnd the last thing I should focus on is the form:
<div class="field">
<%= f.label :country_id %><br>
<%= f.collection_select :country_id, Country.all, :id, :name,
{include_blank: true},
{ data: {
dynamic_selectable_url: dynamic_select_states_path(':country_id'),
dynamic_selectable_target: '#event_state_id'
}
} %>
</div>
<div class="field">
<%= f.label :state_id %><br>
<%= f.collection_select :state_id, @event.country.try(:states) || [], :id, :name, :include_blank => true %>
</div>Check out the code on github and the live demo here.
If you have any suggestions concerning this solution, or if you have spotted a typo, bug, inconsistency or gap, feel free to contact me.