How to implement dependent dropdowns in Rails 4 with an unobtrusive jQuery script

I recently had to implement a view with the classic search form with two select dependent on each other. My aim was to leave the page as clean as possible and make the code that loads the options for the daughter dropdown reusable.

Suppose, therefore, to have two models, SpecializationType and Specialization, defined as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

class SpecializationType < ActiveRecord::Base
# FIELDS
# RELATIONS
has_many :specializations
# TRIGGERS
# VALIDATIONS
validates :name, :presence => true
validates :description, :presence => true
# SCOPES
# OTHER
def to_s
name
end
end


class Specialization < ActiveRecord::Base
# FIELDS
# RELATIONS
belongs_to :specialization_type
has_and_belongs_to_many :users
# TRIGGERS
# VALIDATIONS
validates :name, :presence => true
validates :description, :presence => true
validates :specialization_type, :presence => true
# SCOPES
# OTHER
def to_s
name
end
end

In our template, we will create two dropdowns in this way:

1
2
3
4
5
6
7
8
9
10
11

<%= form_tag users_path, {:method => :get, :class => "users_search_form"} do %>
<%= select_tag :specialization_type_id, options_from_collection_for_select(SpecializationType.all, "id", "name"), :prompt => "Select a specialization type" %>
<%= select_tag :specialization_id, options_from_collection_for_select([], "id", "name"),
"data-option-dependent" => true,
"data-option-observed" => "specialization_type_id",
"data-option-url" => "/specialization_types/:specialization_type_id:/specializations.json",
"data-option-key-method" => :id,
"data-option-value-method" => :name %>
<br/>
<%= submit_tag "Cerca" %>
<% end %>

How we can see, the doughter dropdown indicates many “date-option” attributes that provide everything it needs to load elements at runtime.

In particular:

“data-option-dependent” => true, indicates that this depends on a dropdown dropdown father
“data-option-observed” => “specialization_type_id”, specifies the id of the dropdown to be monitored. If its value changes, we’ll trigger the request to the server
“data-option-url” => “/specialization_type/:specialization_type_id:/specializations.json” is the url that we are going to call on the server. The script will replace :specialization_type_id: the id of the selected item in the dropdown father.
“data-option-key-method” => :id, indicates that we’ll use the ID field of the json returned by the server as attribute “key” for < option >
“data-option-value-method” => :name, indicates that we’ll use the NAME field in the json returned by the server as attribute “value” of < option >

Let’s add this jQuery script in the application.js file, in order to handle all dependent dropdown of our code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

jQuery(document).ready(function () {
$('select[data-option-dependent=true]').each(function (i) {
var observer_dom_id = $(this).attr('id');
var observed_dom_id = $(this).data('option-observed');
var url_mask = $(this).data('option-url');
var key_method = $(this).data('option-key-method');
var value_method = $(this).data('option-value-method');
var prompt = $(this).has('option[value=]').size() ? $(this).find('option[value=]') : $('<option value=\"\">').text('Select a specialization');
var regexp = /:[0-9a-zA-Z_]+:/g;
var observer = $('select#' + observer_dom_id);
var observed = $('#' + observed_dom_id);

if (!observer.val() && observed.size() > 1) {
observer.attr('disabled', true);
}
observed.on('change', function () {
observer.empty().append(prompt);
if (observed.val()) {
url = url_mask.replace(regexp, observed.val());
$.getJSON(url, function (data) {
$.each(data, function (i, object) {
observer.append($('<option>').attr('value', object[key_method]).text(object[value_method]));
observer.attr('disabled', false);
});
});
}
});
});
});

Now we need to configure the following route in routes.rb file:

1

get "specialization_types/:specialization_type_id/specializations" => "application#specializations", :as => "specializations", :format => :json

And finally, implement the method in the controller:

1
2
3
4
5
6

def specializations
specialization_type = SpecializationType.find(params[:specialization_type_id])
respond_to do |format|
format.json { render :json => specialization_type.specializations }
end
end