Come implementare in Rails 4 delle dropdown dipendenti con script jQuery non intrusivo

Di recente ho dovuto implementare in una view la classica ricerca con due select dipendenti l’una dall’altra. Volevo però lasciare il template .erb il più possibile pulito e rendere il codice che carica le option della select figlia il più possibile riutilizzabile e generico.

Supponiamo dunque di avere due modelli, SpecializationType e Specialization, così composti:

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

Come si puo’ notare, Specialization dipende dalla SpecializationType che andremo a selezionare.

Nel nostro template, creeremo le due dropdown in questo modo:

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 %>

Come si puo’ notare, la dropdown figlia indica in una serie di “data-option” tutto il necessario per poter caricarsi a runtime gli elementi.
In particolare:

“data-option-dependent” => true, indica che questa dropdown dipende da una dropdown padre
“data-option-observed” => “specialization_type_id”, specifica l’id della dropdown da monitorare. Il suo cambio di valore scatenerà la richiesta al server
“data-option-url” => “/specialization_type/:specialization_type_id:/specializations.json”, è l’url che andrò a chiamare sul server. Lo script andrà a sostituire :specialization_type_id: con l’id dell’elemento selezionato nella dropdown padre
“data-option-key-method” => :id, indica che userò il campo ID del json tornato dal server come attrbuto “chiave” della < option >
“data-option-value-method” => :name, indica che userò il campo NAME del json tornato dal server come attrbuto “valore” della < option >

Aggiungiamo poi in application.js il seguente codice per gestire tutte le dropdown dipendenti:

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);
});
});
}
});
});
});

Basterà ora definire nel file routes.rb la seguente rotta:

1

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

Ed infine implementare il metodo nel 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