Module: DependentSelect::FormHelpers

Defined in:
lib/dependent_select/form_helpers.rb

Overview

Various helpers available for use in your view

Instance Method Summary collapse

Instance Method Details

#dependent_collection_select(object_name, method, collection, value_method, text_method, filter_method, options = {}, html_options = {}) ⇒ Object

Similar to collection_select form helper, but adds a “filter_method” parameter the generated code includes a javascript observer that modifies the select using the value of a form method.

Parameters

+object_name+:: The name of an object being modified by this select. Example: +:employee+
+method+:: The name of a method of the object designated by "object_name" that
           this select affects. Example: +:city_id+
+collection+:: The collection used for generating the +<options>+ on the
               select. Example: +@cities+
+value_method+:: The method that returns the value of each +<option>+ when
                 applied to each element on +collection+.
                 Example: +:id+ (for city.id)
+text_method+:: The method that returns the text of each +<option>+ when
                applied to each to each element on +collection+.
                Example: +:name+ (for city.name)
+filter_method:: The method being used for filtering. For example,
                 +:province_id+ will filter cities by province.
                 Important notice: this parameter also sets the DOM field
                 id that should be used for getting the filter value.
                 In other words, setting this to :province_id and the +object_name+ to
                 :employee will mean that somewhere on your form there will be a
                 field called "employee_province_id", used for filtering.
+options+:: (Optional) Usual options for +collection_select+ (such as
            +include_blank+) plus 3 new ones, detailed below.
+html_options+:: (Optional)The same html options as +collection_select+.
                 They are appended to the html +select+ as attributes.

Options

In addition to all options for collection_select, several new options are available

:collapse_spaces

By default, blank spaces are collapsed on browsers when printing the select option texts For example, given the following html:

<select>
   <option>This  option    should   have      lots of       spaces   on its text</option>
</select>

Most browsers will “collapse” the spaces, and present something like this instead:

"This option should have lots of spaces on its text"

Setting collapse_spaces to false will replace the blanks with the &nbsp; character. This is accomplised on the javascript function using code similar to the following

option_text.replace(/ /g, "\240"); // "\240" is the octal representation of charcode 160 (nbsp)

On the following example, a sale occurs on a store that belongs to a company. The store model has a method called name_for_selects that prints a two-column text - the first column has the company name on it, while the second has the store address. They are separated by a ‘|’ character, and padded with spaces when necesary (company name has 10 chars or less)

class Store < ActiveRecord::Model
  belongs_to :company
  has_many :sales
  validates_presence_of :address

  def name_for_selects
    "#{company.name.ljust(10)} | #{address}"
  end
end

Now on the edit/new sale view, we will need to use the :collapse_spaces option like this:

<%= dependent_collection_select(:sale, :store_id, @stores, :id, :name_for_selects, :company_id,
      {:collapse_spaces => true}, {:class => 'monospaced'})
 %>

It is recommended that you use this function in conjunction with a monospaced css style. The following rule should be available, for example on application.css:

.monospaced {
  font-family: monospaced;
}

:filter_field

The javascript employed for updating the dependent select needs a field for getting the “filter value”. The default behaviour is calculating this field name by using the :object_name and :filter_value parameters. For example, on this case:

<%= dependent_collection_select(:sale, :province_id, @provinces, :id, :name, :country_id) %>

:object_name is :sale, and :filter_id is :country_id, so the javascript will look for a field called ‘sale_country_id’ on the html.

It is possible to override this default behaviour by specifying a :filter_field option. For example, in this case:

<%= dependent_collection_select(:sale, :province_id, @provinces, :id, :name, 
      :country_id, {:filter_field => :state_id})
 %>

This will make the javascript to look for a field called ‘sale_state_id Notice that the chain ’sale_’ is still appended to the field name. It is possible to override this by using the :complete_filter_field option istead of this one.

The most common use of this property will be for dealing with multiple relationships between the same models. See the complex example below for details.

:complete_filter_field

Works the same way as :filter_field, except that it uses its value directly, instead of appending the :object_name at all. For example:

<%= dependent_collection_select(:sale, :province_id, @provinces, :id, :name, 
      :country_id, {:complete_filter_field => :the_province})
 %>

This will make the javascript to look for a field called ‘the_province’ on the page, and use its value for filtering.

:array_name

dependent_select generates a javascript array with all the options available to the dependent select. By default, the name of that variable is automatically generated using the following formula:

js_array_name = "ds_#{dependent_field_id}_array"

This can be overriden by using the js_array_name option (its value will be used instead of the previous)

This is useful because, by default, dependant_select forms will keep track of generated arrays, and *will not* generate the same array twice. This is very useful for situations in which lots of dependent_selects have to be generated, with the same data. For example, a flight has a destination and origin city:

<%= dependent_collection_select( :flight, :origin_city_id, @cities, :id, :name, :province_id,
      { :filter_field => :origin_province_id, js_array_name => 'cities_array' }
 %>

<%= dependent_collection_select( :flight, :destination_city_id, @cities, :id, :name, :province_id,
      { :filter_field => :destination_province_id, js_array_name => 'cities_array' }
 %>

This example will generate the first javascript array and call it cities_array. Then the second call to dependent_select is done, the form will already know that the javascript for this script is generated, so it will not generate an array.

The :array_name option can also be used in the opposite way: to force the generation of an array. This should happen very rarely - two dependent selects generate the same object name and method but are not supposed to use the same list of values.

Examples

Example 1: A store on a City

In a form for creating a Store, the three selects used for Store, Province and City.

views/Stores/new and views/Stores/edit:

<p>
  Country:
  <%= collection_select :store, :country_id, @countries, :id, :name %>
</p><p>
  Province:
  <%= dependent_collection_select :store, :province_id, @provinces, :id, :name, :country_id %>
</p><p>
  City:
  <%= dependent_collection_select :store, :city_id, @cities, :id, :name, :province_id %>
</p>

Notes:

 * The first helper is rail's regular +collection_select+, since countries don't
   "depend" on anything on this example.
 * You only need a +city_id+ on the +stores+ table (+belongs_to :city+).

You need to define methods on your model for obtaining a +province_id+ and
+country_id+. One of the possible ways is using rails' +delegate+ keyword. See
example below (and note the +:include+ clause)

  class Store < ActiveRecord::Base
    belongs_to :city, :include => [{:province => :country}] #:include not necessary, but nice
    delegate :country, :country_id, :country_id=, :to =>:city, :allow_nil => true
    delegate :province, :province_id, :province_id=, :to =>:city, :allow_nil => true
  end

This delegates the province_id and country_id methods to the :city object_name. So a City must be able to handle country-related stuff too. Again, using delegate, you can do:

class City < ActiveRecord::Base
  belongs_to :province, :include => [:country]
  delegate :country, :country_id, :country_id=, :to =>:province, :allow_nil => true
end

Note that I’ve also delegated province, province=, country and country= . This is so I’m able to do things like puts store.country.name. This is convenient but not needed.

Example 2: Using html_options

Imagine that you want your selects to be of an specific css class, for formatting. You can accomplish this by using the html_options parameter

<p>
  Country:
  <%= collection_select :store, :country_id, @countries, :id, :name,
        {:include_blanks => true}, {:class=>'monospaced'}
   %>
</p><p>
  Province:
  <%= dependent_collection_select :store, :province_id, @provinces, :id, :name,
        :country_id, {:include_blanks => true}, {:class=>'brightYellow'}
   %>
</p><p>
  City:
  <%= dependent_collection_select :store, :city_id, @cities, :id, :name,
        :province_id, {:include_blanks => true}, {:class=>'navyBlue'}
   %>
</p>

Notice the use of {} for the :options parameter. If we wanted to include html_options but not options, we would have had to leave empty brackets.

Example 3: Multiple relations and :filter_field

Imagine that you want your stores to have two cities instead of one; One for importing and another one for exporting. Let’s call them :import_city and :export_city:

Models/Store.rb:

 class Store < ActiveRecord::Base

  belongs_to :import_city, :class_name => "City", :include => [{:province => :country}]
  delegate :country, :country_id, :country_id=, :to =>:import_city,
    :allow_nil => true, :prefix => :import
  delegate :province, :province_id, :province_id=, :to =>:import_city,
    :allow_nil => true, :prefix => :import

  belongs_to :export_city, :class_name => "City", :include => [{:province => :country}]
  delegate :country, :country_id, :country_id=, :to =>:export_city,
    :allow_nil => true, :prefix => :export
  delegate :province, :province_id, :province_id=, :to =>:export_city,
    :allow_nil => true, :prefix => :import
end

In this case, the store doesn’t have a :city_id, :country_id or :province_id. Instead, it has :export_city_id, :export_country_id and :export_province_id, and the same for import.

We’ll have to use the :filter_field option in order to use the right field for updating the selects.

views/Stores/new and views/Stores/edit:

<p>
  Import Country:
  <%= collection_select :store, :import_country_id, @countries, :id, :name %>
</p><p>
  Import Province:
  <%= dependent_collection_select :store, :import_province_id, @provinces, :id, :name,
        :country_id, :filter_field => :import_country_id, :array_name => 'provinces_array'
   %>
</p><p>
  Import City:
  <%= dependent_collection_select :store, :import_city_id, @cities, :id, :name,
        :province_id, :filter_field => :import_province_id, :array_name => 'cities_array'
   %>
</p><p>
  Export Country:
  <%= collection_collection_select :store, :export_country_id, @countries, :id, :name %>
</p><p>
  Export Province:
  <%= dependent_collection_select :store, :export_province_id, @provinces, :id, :name,
        :country_id, :filter_field => :export_country_id, :array_name => 'provinces_array'
   %>
</p><p>
  Export City:
  <%= dependent_select :store, :export_city_id, @cities, :id, :name,
        :province_id, :filter_field => :export_province_id, :array_name => 'cities_array'
   %>
</p>

Notice the use of :array_name. This is optional, but greatly reduces the amount of js code generated



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/dependent_select/form_helpers.rb', line 267

def dependent_collection_select(object_name, method, collection, value_method, 
  text_method, filter_method, options = {}, html_options = {}
)
  object, options, extra_options = dependent_select_process_options(object_name, options)

  initial_collection = dependent_select_initial_collection(object,
    method, collection, value_method)
  
  tag, dependent_field_id = dependent_collection_select_build_tag(
    object_name, object, method, initial_collection, value_method, 
    text_method, options, html_options)

  script = dependent_select_js_for_collection(object_name, object, method, 
    collection, value_method, text_method, filter_method, options, html_options,
    extra_options, dependent_field_id)
    
  return "#{tag}\n#{script}"
end

#dependent_select(object_name, method, choices_with_filter, filter_method, options = {}, html_options = {}) ⇒ Object

Similar to select form helper, but generates javascript for filtering the results depending on the value on another field. Consider using dependent_collection_select instead of this one, it will probably help you more. And I’ll be updating that one more often.

Parameters

+object_name+:: The name of the object being modified by this select. 
                Example: +:employee+
+method+:: The name of the method on the object cadded "object_name" that this
           will affect. Example: +:city_id+
+choices_with_filter+:: The structure is +[[opt1_txt, opt1_value, opt1_filter],
                        [opt2_txt, opt2_value, opt2_filter] ... ]+.
+filter_method:: The method being used for filtering. For example,
                 +:province_id+ will filter cities by province.
                 Important notice: this parameter also sets the DOM field
                 id that should be used for getting the filter value.
                 In other words, setting this to :province_id and the +object_name+ to
                 :employee will mean that somewhere on your form there will be a
                 field called "employee_province_id", used for filtering.
+options+ and +html_options+:: See +dependent_collection_select+.

Examples

Example 1: Types of animals

Create an animal on a children-oriented app, where groups and subgroups are predefined constants.

models/animal.rb

class Animal < ActiveRecord::Base
  GROUPS = [['Invertebrates', 1], ['Vertebrates', 2]]
  SUBGROUPS = [
    ['Protozoa', 1, 1], ['Echinoderms',2,1], ['Annelids',3,1], ['Mollusks',4,1],
    ['Arthropods',5,1], ['Crustaceans',6,1], ['Arachnids',7,1], ['Insects',8,1],
    ['Fish',9,2], ['Amphibians',10,2], ['Reptiles',11,2], ['Birds',12,2],
    ['Mammals',13,2], ['Marsupials',14,2], ['Primates',15,2], ['Rodents',16,2],
    ['Cetaceans',17,2], ['Seals, Seal Lions and Walrus',18,2]
  ]
end

new/edit animal html.erb

<p>
  Group: <%= select :animal, :group_id, Animal::GROUPS %>
</p><p>
  Subgroup: <%= select :animal, :subgroup_id, :group, Animal::SUBGROUPS %>
</p>


333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# File 'lib/dependent_select/form_helpers.rb', line 333

def dependent_select(object_name, method, choices_with_filter, filter_method,
  options = {}, html_options = {})

  object, options, extra_options = dependent_select_process_options(object_name, options)

  initial_choices = dependent_select_initial_choices(object, method, choices_with_filter)
  
  tag, dependent_field_id = dependent_select_build_tag(
    object_name, object, method, initial_collection, value_method, 
    text_method, options, html_options)

  script = dependent_select_js(object_name, method, choices_with_filter,
    filter_method, options, html_options, extra_options)
  
  return "#{tag}\n#{script}"
end