#-- # Copyright (C) 2007 Dimitrij Denissenko # Please read LICENSE document for more information. #++ # This plugin adds a new helper to the great RubyOnRails framework that # allows to generate multiple, dependent HTML select tags. It handles # the relations with simple JavaScript (requires prototype.js library), # so you do not need to code any AJAX callbacks (can be handy when you # only deal with a small amount of data). This plugin works also # well with RJS templates and supports recursive pre-selection of the # related select tags. module ActionView::Helpers::RelatedSelectFormHelper # Return a dependent select tag with options related to the # parent_select_tag selection for the given object and method using # options_from_collection_for_select to generate the list of option tags. # # === Arguments: # # * 'object', 'method', 'collection', 'value_method', 'text_method', # 'options' & 'html_options' are used exactly the same way as in # the standard collection_select helper method. # * 'parent_element' specifies, as the name says, the parent # select tag; argument can be passed as an array # [:parent_object, :method] or directly as string referencing the # tag id (e.g. "parent_object_method") # * Parameter 'reference_method' specifies the method that is used to get # a reference to parent selection. # # Additionally the 'options' argument can include a ':selected' attribute, # that will override the default pre-selection behaviour (which uses to call # '@object.method' to determine the to be selected option). # # # === Example usage: # # tables # # car_companies: id, name # car_models: id, name, car_company_id # # view # # <%= collection_select( # :car_company, :id, CarCompany.find(:all), :id, :name) %> # <%= related_collection_select(:car_model, :id, [:car_company, :id], # CarModel.find(:all), :id, :name, :car_company_id) %> # # The code above will create two drop-down select tags. The 1st allows the # selection of a car company. Based on this decision the 2nd select tag shows # company specific car models. def related_collection_select(object, method, parent_element, collection, value_method, text_method, reference_method, options = {}, html_options = {}) relations = collection.inject({}) do |result, record| reference_value = record.send(reference_method).to_s record_value = record.send(value_method).to_s result[reference_value] ||= OrderedOptions.new result[reference_value][record_value] ||= [] result[reference_value][record_value] << record.send(text_method) result end parent_tag_id = parent_element.is_a?(Array) ? parent_element.collect{|t| t.to_s}.join('_') : parent_element.to_s ActionView::Helpers::InstanceTag.new(object, method, self, nil, options.delete(:object)). to_related_collection_select_tag(parent_tag_id, relations, options, html_options) end end ActionView::Base.send :include, ActionView::Helpers::RelatedSelectFormHelper class ActionView::Helpers::InstanceTag #:nodoc: include ActionView::Helpers::JavaScriptHelper def to_related_collection_select_tag(parent_tag_id, relations, options = {}, html_options = {}) #:nodoc: html_options.stringify_keys! add_default_name_and_id(html_options) store_collection_relations_for_current_request(parent_tag_id, relations) selected_value = options.has_key?(:selected) ? options[:selected] : value(object) (related_select_cache.size == 1 ? javascript_tag(javascript_code_for_related_select_extension) : '') + content_tag("select", '', html_options) + javascript_tag(javascript_code_for_relation_hookup(parent_tag_id, relations, selected_value, options)) + javascript_tag(javascript_code_for_preselection(selected_value)) end private def store_collection_relations_for_current_request(parent_tag_id, relations) inverted_relation_values = relations.inject({}) do |result, (parent_value, relation)| relation.keys.each { |child_value| result[child_value.to_s] ||= parent_value.to_s } result end related_select_cache[tag_id] ||= {:parent => parent_tag_id, :relations => inverted_relation_values} end def related_select_cache unless @template_object.respond_to?(:related_select_cache) @template_object.class.send(:attr_accessor, :related_select_cache) end @template_object.related_select_cache ||= {} end def javascript_code_for_preselection(selected_value, element_id = tag_id) return "$('#{element_id}').refresh();" if selected_value.blank? code = ["$('#{element_id}').select('#{selected_value}');"] while relations = related_select_cache[element_id] element_id = relations[:parent] selected_value = relations[:relations][selected_value.to_s] code << "$('#{element_id}').select('#{selected_value}');" end ' ' + code.reverse.join(' ') end def javascript_code_for_relation_hookup(parent_tag_id, relations, selected_value, options = {}) javascript_relations = javascript_code_for_relation_hash(relations, selected_value, options) " if ($('#{parent_tag_id}').extended_html_select_object != true) Object.extend($('#{parent_tag_id}'), HTMLRelatedSelectStruct); if ($('#{tag_id}').extended_html_select_object != true) Object.extend($('#{tag_id}'), HTMLRelatedSelectStruct); $('#{parent_tag_id}').child_add($('#{tag_id}')); $('#{tag_id}').select_parent = $('#{parent_tag_id}'); $('#{tag_id}').relation_hash = { #{javascript_relations} }; " end def javascript_code_for_related_select_extension " var HTMLRelatedSelectStruct = { select_children: undefined, select_parent: undefined, relation_hash: undefined, extended_html_select_object: true, select: function(value) { for (i=0; i < this.options.length; i++) { if (this.options[i].value == value) this.selectedIndex = i; } this.refresh_children(); }, child_add: function(child) { if (this.select_children == undefined) this.select_children = []; this.select_children.push(child); }, refresh_children: function() { if (this.select_children != undefined) { this.select_children.each( function(child){ child.refresh(); } ); } }, refresh: function() { this.options.length = 0; if (this.select_parent != undefined && this.relation_hash != undefined && this.select_parent.selectedIndex > -1) { opts = this.relation_hash[this.select_parent.options[this.select_parent.selectedIndex].value]; if (opts != undefined) { for (i=0; i