dependent_select 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +183 -0
- data/init.rb +1 -0
- data/lib/dependent_select/dependent_select.rb +28 -0
- data/lib/dependent_select/form_helpers.rb +521 -0
- data/lib/dependent_select/includes_helper.rb +9 -0
- data/lib/dependent_select.rb +19 -0
- data/public/javascripts/dependent_select/dependent_select.js +45 -0
- metadata +61 -0
data/README.rdoc
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
|
2
|
+
=dependent_select
|
3
|
+
|
4
|
+
This package extends rails with some helpers that allow "selects that update dynamically
|
5
|
+
depending on other fields"
|
6
|
+
|
7
|
+
Demo application can be found in http://github.com/splendeo/dependent_select
|
8
|
+
|
9
|
+
Mailing list on http://groups.google.com/group/dependent_select/
|
10
|
+
|
11
|
+
==Quick example: Store with country and province
|
12
|
+
|
13
|
+
On your layout:
|
14
|
+
<%= javascript_include_tag :defaults %>
|
15
|
+
<%= dependent_select_includes %>
|
16
|
+
|
17
|
+
On your new/edit views:
|
18
|
+
|
19
|
+
<% form_for :store do |f| %>
|
20
|
+
Country:<br/>
|
21
|
+
<% f.collection_select :country_id, @countries, :id, :name, :include_blanks => true %> <br />
|
22
|
+
Province:<br/>
|
23
|
+
<% f.dependent_collection_select :province_id, @provinces, :id, :name, :country_id, :include_blanks => true %> <br />
|
24
|
+
City:<br/>
|
25
|
+
<% f.dependent_collection_select :city_id, @cities, :id, :name, :province_id, :include_blanks => true %> <br />
|
26
|
+
<% end %>
|
27
|
+
|
28
|
+
In order for this to work properly, the Store model must have methods for +country_id+ and +store_id+.
|
29
|
+
The best way I've found for implementing this is by using +delegate+. So the model would be:
|
30
|
+
|
31
|
+
class Store < ActiveRecord::Base
|
32
|
+
belongs_to :city, :include => [{:province => :country}] #useful to preload these
|
33
|
+
delegate :country, :country_id, :country_id=, :to =>:city
|
34
|
+
delegate :province, :province_id, :province_id=, :to =>:city
|
35
|
+
end
|
36
|
+
|
37
|
+
Notice that I've delegated the country to the city - so the city should probably have another +delegate+ line:
|
38
|
+
|
39
|
+
class City < ActiveRecord::Base
|
40
|
+
belongs_to :province, :include => [:country] #again, useful but not needed
|
41
|
+
delegate :country, :country_id, :country_id=, :to =>:province
|
42
|
+
end
|
43
|
+
|
44
|
+
Finally, the controller might look like this:
|
45
|
+
|
46
|
+
class StoresController < ApplicationController
|
47
|
+
before_filter :fill_selects :only => [:new, :edit, :update, :create]
|
48
|
+
|
49
|
+
{...} # Standard scaffold-generated methods
|
50
|
+
|
51
|
+
protected
|
52
|
+
def fill_selects
|
53
|
+
@countries = Country.find(:all, :order => 'name ASC')
|
54
|
+
@provinces = Province.find(:all, :order => 'name ASC') # all provinces for all countries
|
55
|
+
@cities = City.find(:all, :order => 'name ASC') # all cities for all provinces
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
This will generate a regular +collection_select+ for the country and a +dependent_collection_select+
|
60
|
+
for province. The later will be a regular +collection_select+ followed by a js +<script>+ tag that:
|
61
|
+
* Will create an array with all the provinces. (+var array=[province1, province2...];+)
|
62
|
+
* Will place a listeners on the +country_id+ +select+ in order to update the provinces select if
|
63
|
+
the countries select is modified
|
64
|
+
* Fill up the provinces select with appropiate values.
|
65
|
+
|
66
|
+
*Note that this will not work if you haven't followed the installation procedure - see below*
|
67
|
+
|
68
|
+
There's a more complex example at the end of this document.
|
69
|
+
|
70
|
+
==Installation
|
71
|
+
|
72
|
+
===As a gem
|
73
|
+
Copy this on config/environment.erb, inside the gems section
|
74
|
+
config.gem "splendeo-dependent_select", :lib => 'dependent_select', :source => "http://gems.github.com"
|
75
|
+
Then execute
|
76
|
+
rake gems:install
|
77
|
+
|
78
|
+
The first time you initialize your server after this (presumably with script/server) the necesary javascript files and css will be copied to the public/ directory.
|
79
|
+
|
80
|
+
===As a plug-in
|
81
|
+
I actually haven't tried this, sorry I don't know how to do it.
|
82
|
+
|
83
|
+
==Re-installation
|
84
|
+
===As a gem
|
85
|
+
Several steps are needed:
|
86
|
+
* It is recommended that you uninstall the gem before installing a new version.
|
87
|
+
* You must remove this file: public/javascripts/dependent_select/dependent_select.js
|
88
|
+
* And then install the new version
|
89
|
+
|
90
|
+
In other words:
|
91
|
+
sudo gem uninstall splendeo-dependent_select
|
92
|
+
rm public/javascripts/dependent_select/dependent_select.js
|
93
|
+
rake gems:install
|
94
|
+
|
95
|
+
===As a plug-in
|
96
|
+
I haven't looked into that yet.
|
97
|
+
|
98
|
+
==No AJAX? I can't sent the client all my cities!
|
99
|
+
|
100
|
+
No AJAX for now, sorry. Just plain old javascript.
|
101
|
+
|
102
|
+
However, it might interest you that you'll be generating this:
|
103
|
+
|
104
|
+
<script>
|
105
|
+
var whatever = [['opt1',1,1],['opt2',2,1],['opt3',3,1]...];
|
106
|
+
</script>
|
107
|
+
|
108
|
+
Instead of this :
|
109
|
+
|
110
|
+
<option value='1'>opt1</option>
|
111
|
+
<option value='2'>opt2</option>
|
112
|
+
<option value='3'>opt3</option>
|
113
|
+
|
114
|
+
In our tests, generating information for 8000 cities took arond 20k - the size of a small image.
|
115
|
+
|
116
|
+
Make the test and then decide. It will not take you more than 10 minutes.
|
117
|
+
|
118
|
+
==Complex example: Employee with home city and work city
|
119
|
+
|
120
|
+
On this case we have an employee model with 2 relationships with cities. So the employee model
|
121
|
+
might look like the one below. Notice that the +delegates+ get a bit more complicated.
|
122
|
+
|
123
|
+
class Employee < ActiveRecord::Base
|
124
|
+
|
125
|
+
belongs_to :home_city, :class_name => "City", :include => [{:province => :country}]
|
126
|
+
delegate :country, :country_id, :country_id=, :to =>:home_city,
|
127
|
+
:allow_nil => true, :prefix => :home
|
128
|
+
delegate :province, :province_id, :province_id=, :to =>:home_city,
|
129
|
+
:allow_nil => true, :prefix => :home
|
130
|
+
|
131
|
+
belongs_to :work_city, :class_name => "City", :include => [{:province => :country}]
|
132
|
+
delegate :country, :country_id, :country_id=, :to =>:work_city,
|
133
|
+
:allow_nil => true, :prefix => :work
|
134
|
+
delegate :province, :province_id, :province_id=, :to =>:work_city,
|
135
|
+
:allow_nil => true, :prefix => :home
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
On your layout:
|
140
|
+
<%= javascript_include_tag :defaults %>
|
141
|
+
<%= dependent_select_includes %>
|
142
|
+
|
143
|
+
On your new/edit views, the "filter" for provinces isn't +:country_id+ any more, but +:home_country_id+ or
|
144
|
+
+:work_country_id+. The same happens with the cities and the provinces. You have to tell the selects
|
145
|
+
where to find the right filter fields, using the +filter_field+ option.
|
146
|
+
|
147
|
+
<% form_for :employee do |f| %>
|
148
|
+
Home Country:<br/>
|
149
|
+
<% f.collection_select :home_country_id, @countries, :id, :name, :include_blanks => true %> <br />
|
150
|
+
Home Province:<br/>
|
151
|
+
<% f.dependent_collection_select :home_province_id, @provinces, :id, :name, :country_id,
|
152
|
+
:filter_field => :home_country_id,
|
153
|
+
:include_blanks => true %> <br />
|
154
|
+
Home City:<br/>
|
155
|
+
<% f.dependent_collection_select :home_city_id, @cities, :id, :name, :city_id,
|
156
|
+
:filter_field => :home_province_id,
|
157
|
+
:include_blanks => true %> <br />
|
158
|
+
Work Country:<br/>
|
159
|
+
<% f.collection_select :work_country_id, @countries, :id, :name, :include_blanks => true %> <br />
|
160
|
+
Work Province:<br/>
|
161
|
+
<% f.dependent_collection_select :work_province_id, @provinces, :id, :name, :country_id,
|
162
|
+
:filter_field => :work_country_id,
|
163
|
+
:include_blanks => true %> <br />
|
164
|
+
Work City:<br/>
|
165
|
+
<% f.dependent_collection_select :work_city_id, @cities, :id, :name, :city_id,
|
166
|
+
:filter_field => :work_province_id,
|
167
|
+
:include_blanks => true %> <br />
|
168
|
+
<% end %>
|
169
|
+
|
170
|
+
On your controller:
|
171
|
+
|
172
|
+
class EmployeesController < ApplicationController
|
173
|
+
before_filter :fill_selects :only => [:new, :edit, :update, :create]
|
174
|
+
|
175
|
+
{...} # Standard scaffold-generated methods
|
176
|
+
|
177
|
+
protected
|
178
|
+
def fill_selects
|
179
|
+
@countries = Country.find(:all, :order => 'name ASC')
|
180
|
+
@provinces = Province.find(:all, :order => 'name ASC') # all provinces for all countries
|
181
|
+
@cities = City.find(:all, :order => 'name ASC') # all cities for all provinces
|
182
|
+
end
|
183
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/lib/dependent_select.rb"
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module DependentSelect
|
2
|
+
|
3
|
+
# Returns the default_options hash. These options are by default provided to every calendar_date_select control, unless otherwise overrided.
|
4
|
+
#
|
5
|
+
# Example:
|
6
|
+
# # At the bottom of config/environment.rb:
|
7
|
+
# DependentSelect.default_options.update(
|
8
|
+
# :collapse_spaces => false
|
9
|
+
# )
|
10
|
+
def self.default_options
|
11
|
+
@default_options ||= { :collapse_spaces => true }
|
12
|
+
end
|
13
|
+
|
14
|
+
# By default, spaces are collapsed on browsers when printing the select option texts
|
15
|
+
# For example:
|
16
|
+
# <select>
|
17
|
+
# <option>This option should have lots of spaces on its text</option>
|
18
|
+
# </select>
|
19
|
+
# When you browse that, some browsers collapse the spaces, so you get an option that says
|
20
|
+
# "This option should have lots of spaces on its text"
|
21
|
+
# Setting collapse_blanks to false will replace the blanks with the character, using
|
22
|
+
# javascript
|
23
|
+
# option_text.replace(/ /g, "\240"); // "\240" is the octal representation of charcode 160 (nbsp)
|
24
|
+
def self.collapse_spaces=(value)
|
25
|
+
default_options[:collapse_spaces] = value
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,521 @@
|
|
1
|
+
# Various helpers available for use in your view
|
2
|
+
module DependentSelect::FormHelpers
|
3
|
+
|
4
|
+
# Similar to collection_select form helper, but adds a "filter_method" parameter
|
5
|
+
# the generated code includes a javascript observer that modifies the select
|
6
|
+
# using the value of a form method.
|
7
|
+
#
|
8
|
+
# == Parameters
|
9
|
+
# +object_name+:: The name of an object being modified by this select. Example: +:employee+
|
10
|
+
# +method+:: The name of a method of the object designated by "object_name" that
|
11
|
+
# this select affects. Example: +:city_id+
|
12
|
+
# +collection+:: The collection used for generating the +<options>+ on the
|
13
|
+
# select. Example: +@cities+
|
14
|
+
# +value_method+:: The method that returns the value of each +<option>+ when
|
15
|
+
# applied to each element on +collection+.
|
16
|
+
# Example: +:id+ (for city.id)
|
17
|
+
# +text_method+:: The method that returns the text of each +<option>+ when
|
18
|
+
# applied to each to each element on +collection+.
|
19
|
+
# Example: +:name+ (for city.name)
|
20
|
+
# +filter_method:: The method being used for filtering. For example,
|
21
|
+
# +:province_id+ will filter cities by province.
|
22
|
+
# Important notice: this parameter also sets the DOM field
|
23
|
+
# id that should be used for getting the filter value.
|
24
|
+
# In other words, setting this to :province_id and the +object_name+ to
|
25
|
+
# :employee will mean that somewhere on your form there will be a
|
26
|
+
# field called "employee_province_id", used for filtering.
|
27
|
+
# +options+:: (Optional) Usual options for +collection_select+ (such as
|
28
|
+
# +include_blank+) plus 3 new ones, detailed below.
|
29
|
+
# +html_options+:: (Optional)The same html options as +collection_select+.
|
30
|
+
# They are appended to the html +select+ as attributes.
|
31
|
+
# == Options
|
32
|
+
# In addition to all options for +collection_select+, several new options are available
|
33
|
+
#
|
34
|
+
# === :collapse_spaces
|
35
|
+
# By default, blank spaces are collapsed on browsers when printing the select option texts
|
36
|
+
# For example, given the following html:
|
37
|
+
# <select>
|
38
|
+
# <option>This option should have lots of spaces on its text</option>
|
39
|
+
# </select>
|
40
|
+
# Most browsers will "collapse" the spaces, and present something like this instead:
|
41
|
+
# "This option should have lots of spaces on its text"
|
42
|
+
# Setting collapse_spaces to false will replace the blanks with the character.
|
43
|
+
# This is accomplised on the javascript function using code similar to the following
|
44
|
+
#
|
45
|
+
# option_text.replace(/ /g, "\240"); // "\240" is the octal representation of charcode 160 (nbsp)
|
46
|
+
#
|
47
|
+
# On the following example, a sale occurs on a store that belongs to a company. The store model
|
48
|
+
# has a method called +name_for_selects+ that prints a two-column text - the first column has
|
49
|
+
# the company name on it, while the second has the store address. They are separated by a '|'
|
50
|
+
# character, and padded with spaces when necesary (company name has 10 chars or less)
|
51
|
+
#
|
52
|
+
# class Store < ActiveRecord::Model
|
53
|
+
# belongs_to :company
|
54
|
+
# has_many :sales
|
55
|
+
# validates_presence_of :address
|
56
|
+
#
|
57
|
+
# def name_for_selects
|
58
|
+
# "#{company.name.ljust(10)} | #{address}"
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# Now on the edit/new sale view, we will need to use the :collapse_spaces option like this:
|
63
|
+
#
|
64
|
+
# <%= dependent_collection_select(:sale, :store_id, @stores, :id, :name_for_selects, :company_id,
|
65
|
+
# {:collapse_spaces => true}, {:class => 'monospaced'})
|
66
|
+
# %>
|
67
|
+
#
|
68
|
+
# It is recommended that you use this function in conjunction with a monospaced css style. The following
|
69
|
+
# rule should be available, for example on application.css:
|
70
|
+
#
|
71
|
+
# .monospaced {
|
72
|
+
# font-family: monospaced;
|
73
|
+
# }
|
74
|
+
#
|
75
|
+
# === :filter_field
|
76
|
+
# The javascript employed for updating the dependent select needs a field for getting
|
77
|
+
# the "filter value". The default behaviour is calculating this field name by using the
|
78
|
+
# +:object_name+ and +:filter_value+ parameters. For example, on this case:
|
79
|
+
#
|
80
|
+
# <%= dependent_collection_select(:sale, :province_id, @provinces, :id, :name, :country_id) %>
|
81
|
+
#
|
82
|
+
# +:object_name+ is +:sale+, and +:filter_id+ is +:country_id+, so the javascript will look
|
83
|
+
# for a field called +'sale_country_id'+ on the html.
|
84
|
+
#
|
85
|
+
# It is possible to override this default behaviour by specifying a +:filter_field+ option.
|
86
|
+
# For example, in this case:
|
87
|
+
#
|
88
|
+
# <%= dependent_collection_select(:sale, :province_id, @provinces, :id, :name,
|
89
|
+
# :country_id, {:filter_field => :state_id})
|
90
|
+
# %>
|
91
|
+
#
|
92
|
+
# This will make the javascript to look for a field called +'sale_state_id+
|
93
|
+
# Notice that the chain 'sale_' is still appended to the field name. It is possible to override this
|
94
|
+
# by using the +:complete_filter_field+ option istead of this one.
|
95
|
+
#
|
96
|
+
# The most common use of this property will be for dealing with multiple relationships between the same
|
97
|
+
# models. See the complex example below for details.
|
98
|
+
#
|
99
|
+
# === :complete_filter_field
|
100
|
+
# Works the same way as :filter_field, except that it uses its value directly, instead
|
101
|
+
# of appending the :object_name at all. For example:
|
102
|
+
#
|
103
|
+
# <%= dependent_collection_select(:sale, :province_id, @provinces, :id, :name,
|
104
|
+
# :country_id, {:complete_filter_field => :the_province})
|
105
|
+
# %>
|
106
|
+
#
|
107
|
+
# This will make the javascript to look for a field called +'the_province'+ on the
|
108
|
+
# page, and use its value for filtering.
|
109
|
+
#
|
110
|
+
# === :array_name
|
111
|
+
# dependent_select generates a javascript array with all the options available to the dependent select.
|
112
|
+
# By default, the name of that variable is automatically generated using the following formula:
|
113
|
+
#
|
114
|
+
# js_array_name = "ds_#{dependent_field_id}_array"
|
115
|
+
#
|
116
|
+
# This can be overriden by using the js_array_name option (its value will be used instead of the previous)
|
117
|
+
#
|
118
|
+
# This is useful because, by default, dependant_select forms will keep track of generated arrays, and *will not*
|
119
|
+
# generate the same array twice. This is very useful for situations in which lots of dependent_selects have
|
120
|
+
# to be generated, with the same data. For example, a flight has a destination and origin city:
|
121
|
+
#
|
122
|
+
# <%= dependent_collection_select( :flight, :origin_city_id, @cities, :id, :name, :province_id,
|
123
|
+
# { :filter_field => :origin_province_id, js_array_name => 'cities_array' }
|
124
|
+
# %>
|
125
|
+
#
|
126
|
+
# <%= dependent_collection_select( :flight, :destination_city_id, @cities, :id, :name, :province_id,
|
127
|
+
# { :filter_field => :destination_province_id, js_array_name => 'cities_array' }
|
128
|
+
# %>
|
129
|
+
#
|
130
|
+
# This example will generate the first javascript array and call it cities_array. Then the second
|
131
|
+
# call to +dependent_select+ is done, the form will already know that the javascript for this script
|
132
|
+
# is generated, so it will not generate an array.
|
133
|
+
#
|
134
|
+
# The +:array_name+ option can also be used in the opposite way: to force the generation of an array.
|
135
|
+
# This should happen very rarely - two dependent selects generate the same object name and method but are not
|
136
|
+
# supposed to use the same list of values.
|
137
|
+
#
|
138
|
+
# == Examples
|
139
|
+
#
|
140
|
+
# === Example 1: A store on a City
|
141
|
+
# In a form for creating a Store, the three selects used for Store, Province and City.
|
142
|
+
#
|
143
|
+
# views/Stores/new and views/Stores/edit:
|
144
|
+
#
|
145
|
+
# <p>
|
146
|
+
# Country:
|
147
|
+
# <%= collection_select :store, :country_id, @countries, :id, :name %>
|
148
|
+
# </p><p>
|
149
|
+
# Province:
|
150
|
+
# <%= dependent_collection_select :store, :province_id, @provinces, :id, :name, :country_id %>
|
151
|
+
# </p><p>
|
152
|
+
# City:
|
153
|
+
# <%= dependent_collection_select :store, :city_id, @cities, :id, :name, :province_id %>
|
154
|
+
# </p>
|
155
|
+
#
|
156
|
+
# Notes:
|
157
|
+
# * The first helper is rail's regular +collection_select+, since countries don't
|
158
|
+
# "depend" on anything on this example.
|
159
|
+
# * You only need a +city_id+ on the +stores+ table (+belongs_to :city+).
|
160
|
+
#
|
161
|
+
# You need to define methods on your model for obtaining a +province_id+ and
|
162
|
+
# +country_id+. One of the possible ways is using rails' +delegate+ keyword. See
|
163
|
+
# example below (and note the +:include+ clause)
|
164
|
+
#
|
165
|
+
# class Store < ActiveRecord::Base
|
166
|
+
# belongs_to :city, :include => [{:province => :country}] #:include not necessary, but nice
|
167
|
+
# delegate :country, :country_id, :country_id=, :to =>:city, :allow_nil => true
|
168
|
+
# delegate :province, :province_id, :province_id=, :to =>:city, :allow_nil => true
|
169
|
+
# end
|
170
|
+
#
|
171
|
+
# This delegates the +province_id+ and +country_id+ methods to the +:city+ object_name.
|
172
|
+
# So a City must be able to handle country-related stuff too. Again, using +delegate+, you can
|
173
|
+
# do:
|
174
|
+
#
|
175
|
+
# class City < ActiveRecord::Base
|
176
|
+
# belongs_to :province, :include => [:country]
|
177
|
+
# delegate :country, :country_id, :country_id=, :to =>:province, :allow_nil => true
|
178
|
+
# end
|
179
|
+
#
|
180
|
+
# Note that I've also delegated +province+, +province=+, +country+ and +country=+ .
|
181
|
+
# This is so I'm able to do things like +puts store.country.name+.
|
182
|
+
# This is convenient but not needed.
|
183
|
+
#
|
184
|
+
# === Example 2: Using html_options
|
185
|
+
# Imagine that you want your selects to be of an specific css class, for formatting.
|
186
|
+
# You can accomplish this by using the +html_options+ parameter
|
187
|
+
#
|
188
|
+
# <p>
|
189
|
+
# Country:
|
190
|
+
# <%= collection_select :store, :country_id, @countries, :id, :name,
|
191
|
+
# {:include_blanks => true}, {:class=>'monospaced'}
|
192
|
+
# %>
|
193
|
+
# </p><p>
|
194
|
+
# Province:
|
195
|
+
# <%= dependent_collection_select :store, :province_id, @provinces, :id, :name,
|
196
|
+
# :country_id, {:include_blanks => true}, {:class=>'brightYellow'}
|
197
|
+
# %>
|
198
|
+
# </p><p>
|
199
|
+
# City:
|
200
|
+
# <%= dependent_collection_select :store, :city_id, @cities, :id, :name,
|
201
|
+
# :province_id, {:include_blanks => true}, {:class=>'navyBlue'}
|
202
|
+
# %>
|
203
|
+
# </p>
|
204
|
+
#
|
205
|
+
# Notice the use of +{}+ for the +:options+ parameter. If we wanted to include +html_options+
|
206
|
+
# but not options, we would have had to leave empty brackets.
|
207
|
+
#
|
208
|
+
# === Example 3: Multiple relations and +:filter_field+
|
209
|
+
# Imagine that you want your stores to have two cities instead of one; One for
|
210
|
+
# importing and another one for exporting. Let's call them +:import_city+ and
|
211
|
+
# +:export_city+:
|
212
|
+
#
|
213
|
+
# Models/Store.rb:
|
214
|
+
#
|
215
|
+
# class Store < ActiveRecord::Base
|
216
|
+
#
|
217
|
+
# belongs_to :import_city, :class_name => "City", :include => [{:province => :country}]
|
218
|
+
# delegate :country, :country_id, :country_id=, :to =>:import_city,
|
219
|
+
# :allow_nil => true, :prefix => :import
|
220
|
+
# delegate :province, :province_id, :province_id=, :to =>:import_city,
|
221
|
+
# :allow_nil => true, :prefix => :import
|
222
|
+
#
|
223
|
+
# belongs_to :export_city, :class_name => "City", :include => [{:province => :country}]
|
224
|
+
# delegate :country, :country_id, :country_id=, :to =>:export_city,
|
225
|
+
# :allow_nil => true, :prefix => :export
|
226
|
+
# delegate :province, :province_id, :province_id=, :to =>:export_city,
|
227
|
+
# :allow_nil => true, :prefix => :import
|
228
|
+
# end
|
229
|
+
#
|
230
|
+
# In this case, the store doesn't have a +:city_id+, +:country_id+ or +:province_id+. Instead, it has
|
231
|
+
# +:export_city_id+, +:export_country_id+ and +:export_province_id+, and the same for import.
|
232
|
+
#
|
233
|
+
# We'll have to use the +:filter_field+ option in order to use the right field for
|
234
|
+
# updating the selects.
|
235
|
+
#
|
236
|
+
# views/Stores/new and views/Stores/edit:
|
237
|
+
#
|
238
|
+
# <p>
|
239
|
+
# Import Country:
|
240
|
+
# <%= collection_select :store, :import_country_id, @countries, :id, :name %>
|
241
|
+
# </p><p>
|
242
|
+
# Import Province:
|
243
|
+
# <%= dependent_collection_select :store, :import_province_id, @provinces, :id, :name,
|
244
|
+
# :country_id, :filter_field => :import_country_id, :array_name => 'provinces_array'
|
245
|
+
# %>
|
246
|
+
# </p><p>
|
247
|
+
# Import City:
|
248
|
+
# <%= dependent_collection_select :store, :import_city_id, @cities, :id, :name,
|
249
|
+
# :province_id, :filter_field => :import_province_id, :array_name => 'cities_array'
|
250
|
+
# %>
|
251
|
+
# </p><p>
|
252
|
+
# Export Country:
|
253
|
+
# <%= collection_collection_select :store, :export_country_id, @countries, :id, :name %>
|
254
|
+
# </p><p>
|
255
|
+
# Export Province:
|
256
|
+
# <%= dependent_collection_select :store, :export_province_id, @provinces, :id, :name,
|
257
|
+
# :country_id, :filter_field => :export_country_id, :array_name => 'provinces_array'
|
258
|
+
# %>
|
259
|
+
# </p><p>
|
260
|
+
# Export City:
|
261
|
+
# <%= dependent_select :store, :export_city_id, @cities, :id, :name,
|
262
|
+
# :province_id, :filter_field => :export_province_id, :array_name => 'cities_array'
|
263
|
+
# %>
|
264
|
+
# </p>
|
265
|
+
# Notice the use of +:array_name+. This is optional, but greatly reduces the amount of js code generated
|
266
|
+
#
|
267
|
+
def dependent_collection_select(object_name, method, collection, value_method,
|
268
|
+
text_method, filter_method, options = {}, html_options = {}
|
269
|
+
)
|
270
|
+
object, options, extra_options = dependent_select_process_options(object_name, options)
|
271
|
+
|
272
|
+
initial_collection = dependent_select_initial_collection(object,
|
273
|
+
method, collection, value_method)
|
274
|
+
|
275
|
+
tag, dependent_field_id = dependent_collection_select_build_tag(
|
276
|
+
object_name, object, method, initial_collection, value_method,
|
277
|
+
text_method, options, html_options)
|
278
|
+
|
279
|
+
script = dependent_select_js_for_collection(object_name, object, method,
|
280
|
+
collection, value_method, text_method, filter_method, options, html_options,
|
281
|
+
extra_options, dependent_field_id)
|
282
|
+
|
283
|
+
return "#{tag}\n#{script}"
|
284
|
+
end
|
285
|
+
|
286
|
+
|
287
|
+
# Similar to +select+ form helper, but generates javascript for filtering the
|
288
|
+
# results depending on the value on another field.
|
289
|
+
# Consider using +dependent_collection_select+ instead of this one, it will probably
|
290
|
+
# help you more. And I'll be updating that one more often.
|
291
|
+
# == Parameters
|
292
|
+
# +object_name+:: The name of the object being modified by this select.
|
293
|
+
# Example: +:employee+
|
294
|
+
# +method+:: The name of the method on the object cadded "object_name" that this
|
295
|
+
# will affect. Example: +:city_id+
|
296
|
+
# +choices_with_filter+:: The structure is +[[opt1_txt, opt1_value, opt1_filter],
|
297
|
+
# [opt2_txt, opt2_value, opt2_filter] ... ]+.
|
298
|
+
# +filter_method:: The method being used for filtering. For example,
|
299
|
+
# +:province_id+ will filter cities by province.
|
300
|
+
# Important notice: this parameter also sets the DOM field
|
301
|
+
# id that should be used for getting the filter value.
|
302
|
+
# In other words, setting this to :province_id and the +object_name+ to
|
303
|
+
# :employee will mean that somewhere on your form there will be a
|
304
|
+
# field called "employee_province_id", used for filtering.
|
305
|
+
# +options+ and +html_options+:: See +dependent_collection_select+.
|
306
|
+
# == Examples
|
307
|
+
#
|
308
|
+
# === Example 1: Types of animals
|
309
|
+
# Create an animal on a children-oriented app, where groups and subgroups
|
310
|
+
# are predefined constants.
|
311
|
+
#
|
312
|
+
# models/animal.rb
|
313
|
+
#
|
314
|
+
# class Animal < ActiveRecord::Base
|
315
|
+
# GROUPS = [['Invertebrates', 1], ['Vertebrates', 2]]
|
316
|
+
# SUBGROUPS = [
|
317
|
+
# ['Protozoa', 1, 1], ['Echinoderms',2,1], ['Annelids',3,1], ['Mollusks',4,1],
|
318
|
+
# ['Arthropods',5,1], ['Crustaceans',6,1], ['Arachnids',7,1], ['Insects',8,1],
|
319
|
+
# ['Fish',9,2], ['Amphibians',10,2], ['Reptiles',11,2], ['Birds',12,2],
|
320
|
+
# ['Mammals',13,2], ['Marsupials',14,2], ['Primates',15,2], ['Rodents',16,2],
|
321
|
+
# ['Cetaceans',17,2], ['Seals, Seal Lions and Walrus',18,2]
|
322
|
+
# ]
|
323
|
+
# end
|
324
|
+
#
|
325
|
+
# new/edit animal html.erb
|
326
|
+
#
|
327
|
+
# <p>
|
328
|
+
# Group: <%= select :animal, :group_id, Animal::GROUPS %>
|
329
|
+
# </p><p>
|
330
|
+
# Subgroup: <%= select :animal, :subgroup_id, :group, Animal::SUBGROUPS %>
|
331
|
+
# </p>
|
332
|
+
#
|
333
|
+
def dependent_select(object_name, method, choices_with_filter, filter_method,
|
334
|
+
options = {}, html_options = {})
|
335
|
+
|
336
|
+
object, options, extra_options = dependent_select_process_options(object_name, options)
|
337
|
+
|
338
|
+
initial_choices = dependent_select_initial_choices(object, method, choices_with_filter)
|
339
|
+
|
340
|
+
tag, dependent_field_id = dependent_select_build_tag(
|
341
|
+
object_name, object, method, initial_collection, value_method,
|
342
|
+
text_method, options, html_options)
|
343
|
+
|
344
|
+
script = dependent_select_js(object_name, method, choices_with_filter,
|
345
|
+
filter_method, options, html_options, extra_options)
|
346
|
+
|
347
|
+
return "#{tag}\n#{script}"
|
348
|
+
end
|
349
|
+
|
350
|
+
private
|
351
|
+
#holds the names of the arrays already generated, so repetitions can be avoided.
|
352
|
+
def dependend_select_array_names
|
353
|
+
@dependend_select_array_names ||= {}
|
354
|
+
end
|
355
|
+
|
356
|
+
# extracts any options passed into calendar date select, appropriating them to either the Javascript call or the html tag.
|
357
|
+
def dependent_select_process_options(object_name, options)
|
358
|
+
options, extra_options = DependentSelect.default_options.merge(options), {}
|
359
|
+
for key in [:collapse_spaces, :filter_field, :complete_filter_field, :array_name]
|
360
|
+
extra_options[key] = options.delete(key) if options.has_key?(key)
|
361
|
+
end
|
362
|
+
|
363
|
+
object = options.delete(:object) || instance_variable_get("@#{object_name}")
|
364
|
+
|
365
|
+
options[:object]=object
|
366
|
+
|
367
|
+
[object, options, extra_options]
|
368
|
+
end
|
369
|
+
|
370
|
+
# generates the javascript that will follow the dependent select
|
371
|
+
# contains:
|
372
|
+
# * An array with all the possible options (structure [text, value, filter])
|
373
|
+
# * An observer that detects changes on the "observed" field and triggers an update
|
374
|
+
# * An extra observer for custon events. These events are raised by the dependent_select itself.
|
375
|
+
# * An first call to update_dependent_select, that sets up the initial stuff
|
376
|
+
def dependent_select_js(object_name, object, method, choices_with_filter,
|
377
|
+
filter_method, options, html_options, extra_options, dependent_field_id)
|
378
|
+
|
379
|
+
# the js variable that will hold the array with option values, texts and filters
|
380
|
+
js_array_name = extra_options[:array_name] || "ds_#{dependent_field_id}_array"
|
381
|
+
|
382
|
+
js_array_code = ""
|
383
|
+
|
384
|
+
if(dependend_select_array_names[js_array_name].nil?)
|
385
|
+
dependend_select_array_names[js_array_name] = true;
|
386
|
+
js_array_code += "#{js_array_name} = #{choices_with_filter.to_json};\n"
|
387
|
+
end
|
388
|
+
|
389
|
+
observed_field_id = dependent_select_calculate_observed_field_id(object_name, object,
|
390
|
+
filter_method, html_options, extra_options)
|
391
|
+
initial_value = dependent_select_initial_value(object, method)
|
392
|
+
include_blank = options[:include_blank] || false
|
393
|
+
collapse_spaces = extra_options[:collapse_spaces] || false
|
394
|
+
|
395
|
+
js_callback =
|
396
|
+
"function(e) { update_dependent_select( '#{dependent_field_id}', '#{observed_field_id}', #{js_array_name}, " +
|
397
|
+
"'#{initial_value}', #{include_blank}, #{collapse_spaces}, false); }"
|
398
|
+
|
399
|
+
javascript_tag(js_array_code +
|
400
|
+
"$('#{observed_field_id}').observe ('change', #{js_callback});\n" +
|
401
|
+
"$('#{observed_field_id}').observe ('DependentSelectFormBuilder:change', #{js_callback}); \n" +
|
402
|
+
"update_dependent_select( '#{dependent_field_id}', '#{observed_field_id}', #{js_array_name}, " +
|
403
|
+
"'#{initial_value}', #{include_blank}, #{collapse_spaces}, true);"
|
404
|
+
)
|
405
|
+
end
|
406
|
+
|
407
|
+
# generates the js script for a dependent_collection_select. See +dependent_select_js+
|
408
|
+
def dependent_select_js_for_collection(object_name, object, method, collection,
|
409
|
+
value_method, text_method, filter_method, options, html_options, extra_options, dependent_field_id)
|
410
|
+
|
411
|
+
# the array that, converted to javascript, will be assigned values_var variable,
|
412
|
+
# so it can be used for updating the select
|
413
|
+
choices_with_filter = collection.collect do |c|
|
414
|
+
[ c.send(text_method), c.send(value_method), c.send(filter_method) ]
|
415
|
+
end
|
416
|
+
|
417
|
+
dependent_select_js(object_name, object, method, choices_with_filter,
|
418
|
+
filter_method, options, html_options, extra_options, dependent_field_id)
|
419
|
+
end
|
420
|
+
|
421
|
+
# returns a collection_select html string and the id of the generated field
|
422
|
+
def dependent_collection_select_build_tag(object_name, object, method, collection, value_method,
|
423
|
+
text_method, options, html_options)
|
424
|
+
dependent_field_id, it = dependent_select_calculate_field_id_and_it(object_name,
|
425
|
+
object, method, html_options)
|
426
|
+
|
427
|
+
tag = it.to_collection_select_tag(collection, value_method, text_method, options, html_options)
|
428
|
+
|
429
|
+
return [tag, dependent_field_id]
|
430
|
+
end
|
431
|
+
|
432
|
+
# returns a select html string and the id of the generated field
|
433
|
+
def dependent_select_build_tag(object_name, object, method, choices, options = {}, html_options = {})
|
434
|
+
|
435
|
+
dependent_field_id, it = dependent_select_calculate_field_id_and_it(object_name,
|
436
|
+
object, method, html_options)
|
437
|
+
|
438
|
+
tag = it.to_select_tag(choices, options, html_options)
|
439
|
+
|
440
|
+
return [tag, dependent_field_id]
|
441
|
+
end
|
442
|
+
|
443
|
+
# Calculates the dom id of the observed field, usually concatenating object_name and filt. meth.
|
444
|
+
# For example, 'employee_province_id'. Exceptions:
|
445
|
+
# * If +extra_options+ has an item with key +:complete_filter_field+,
|
446
|
+
# it returns the value of that item
|
447
|
+
# * If +extra_options+ has an item with key +:filter_field+,
|
448
|
+
# it uses its value instead of +method+
|
449
|
+
def dependent_select_calculate_observed_field_id(object_name, object, method,
|
450
|
+
html_options, extra_options)
|
451
|
+
|
452
|
+
return extra_options[:complete_filter_field] if extra_options.has_key? :complete_filter_field
|
453
|
+
method = extra_options[:filter_field] if extra_options.has_key? :filter_field
|
454
|
+
|
455
|
+
return dependent_select_calculate_field_id(object_name, object, method, html_options)
|
456
|
+
end
|
457
|
+
|
458
|
+
# calculates the id of a generated field
|
459
|
+
def dependent_select_calculate_field_id(object_name, object, method, html_options)
|
460
|
+
field_id, it = dependent_select_calculate_field_id_and_it(object_name, object, method, html_options)
|
461
|
+
return field_id
|
462
|
+
end
|
463
|
+
|
464
|
+
# ugly hack used to obtain the generated id from a form_helper
|
465
|
+
# uses the method ActionView::Helpers::InstanceTag.add_default_name_and_id,
|
466
|
+
# ...which is a private method of an internal class of rails. filty.
|
467
|
+
def dependent_select_calculate_field_id_and_it(object_name, object, method, html_options)
|
468
|
+
it = ActionView::Helpers::InstanceTag.new(object_name, method, self, object)
|
469
|
+
html_options = html_options.stringify_keys
|
470
|
+
it.send :add_default_name_and_id, html_options #use send since add_default... is private
|
471
|
+
return [ html_options['id'], it]
|
472
|
+
end
|
473
|
+
|
474
|
+
# Returns the collection that will be used when the dependent_select is first displayed
|
475
|
+
# (before even the first update_dependent_select call is done)
|
476
|
+
# The collection is obained by taking all the elements in collection whose value
|
477
|
+
# equals to +initial_value+. For example if we are editing an employee with city_id=4,
|
478
|
+
# we should put here all the cities with id=4 (there could be more than one)
|
479
|
+
def dependent_select_initial_collection(object, method, collection, value_method)
|
480
|
+
initial_value = dependent_select_initial_value(object, method)
|
481
|
+
return collection.select { |c| c.send(value_method) == initial_value }
|
482
|
+
end
|
483
|
+
|
484
|
+
# this is +dependent_select+'s version of +dependent_select_initial_collection+
|
485
|
+
def dependent_select_initial_choices(object, method, choices_with_filter)
|
486
|
+
initial_value = dependent_select_initial_value(object, method)
|
487
|
+
return choices_with_filter.select { |c| c[1] == initial_value }
|
488
|
+
end
|
489
|
+
|
490
|
+
# The value that the dependend select will have when first rendered. It could be different from
|
491
|
+
# nil, if we are editing. Example: if we are editing an employee with city_id=4, then 4 should be
|
492
|
+
# the initial value.
|
493
|
+
def dependent_select_initial_value(object, method)
|
494
|
+
return object.send(method) || "" if object
|
495
|
+
return ""
|
496
|
+
end
|
497
|
+
|
498
|
+
|
499
|
+
end
|
500
|
+
|
501
|
+
# Helper method for form builders
|
502
|
+
module ActionView
|
503
|
+
module Helpers
|
504
|
+
class FormBuilder
|
505
|
+
def dependent_select( method, choices_with_filter, filter_method,
|
506
|
+
options = {}, html_options = {}
|
507
|
+
)
|
508
|
+
@template.dependent_select(@object_name, method, choices_with_filter,
|
509
|
+
filter_method, options.merge(:object => @object), html_options)
|
510
|
+
end
|
511
|
+
|
512
|
+
def dependent_collection_select( method, collection, value_method,
|
513
|
+
text_method, filter_method, options = {}, html_options = {}
|
514
|
+
)
|
515
|
+
@template.dependent_collection_select(@object_name, method, collection,
|
516
|
+
value_method, text_method, filter_method,
|
517
|
+
options.merge(:object => @object), html_options)
|
518
|
+
end
|
519
|
+
end
|
520
|
+
end
|
521
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module DependentSelect::IncludesHelper
|
2
|
+
# returns html necessary to load the javascript needed for dependent_select
|
3
|
+
def dependent_select_includes(options = {})
|
4
|
+
return "" if @ds_already_included
|
5
|
+
@ds_already_included=true
|
6
|
+
|
7
|
+
javascript_include_tag("dependent_select/dependent_select")
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "dependent_select/dependent_select.rb"
|
2
|
+
require "dependent_select/form_helpers.rb"
|
3
|
+
require "dependent_select/includes_helper.rb"
|
4
|
+
|
5
|
+
if Object.const_defined?(:Rails) && File.directory?(Rails.root.to_s + "/public")
|
6
|
+
ActionView::Helpers::FormHelper.send(:include, DependentSelect::FormHelpers)
|
7
|
+
ActionView::Base.send(:include, DependentSelect::FormHelpers)
|
8
|
+
ActionView::Base.send(:include, DependentSelect::IncludesHelper)
|
9
|
+
|
10
|
+
# install files
|
11
|
+
unless File.exists?(RAILS_ROOT + '/public/javascripts/dependent_select/dependent_select.js')
|
12
|
+
['/public', '/public/javascripts/dependent_select'].each do |dir|
|
13
|
+
source = File.dirname(__FILE__) + "/../#{dir}"
|
14
|
+
dest = RAILS_ROOT + dir
|
15
|
+
FileUtils.mkdir_p(dest)
|
16
|
+
FileUtils.cp(Dir.glob(source+'/*.*'), dest)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
|
2
|
+
function update_dependent_select( dependent_id, observed_id, values_array, // mandatory
|
3
|
+
initial_value, include_blank, collapse_spaces, first_run ) { // optional
|
4
|
+
|
5
|
+
// parse the optional parameters ....
|
6
|
+
initial_value = initial_value || '';
|
7
|
+
include_blank = include_blank || false;
|
8
|
+
collapse_spaces = collapse_spaces || false;
|
9
|
+
first_run = first_run || false;
|
10
|
+
|
11
|
+
// select DOM node whose options are modified
|
12
|
+
var dependent_field = $(dependent_id);
|
13
|
+
|
14
|
+
// select DOM node whose changes trigger this
|
15
|
+
var observed_field = $(observed_id);
|
16
|
+
|
17
|
+
// value chosen on observed_select, used for filtering dependent_select
|
18
|
+
var filter = observed_field.value;
|
19
|
+
|
20
|
+
// the first time the update_func is executed (on edit) the value the select has
|
21
|
+
// comes directly from the model. From them on, it will only use the client's input
|
22
|
+
var previous_value = first_run ? initial_value : dependent_field.value;
|
23
|
+
|
24
|
+
// removes all options from dependent_field
|
25
|
+
dependent_field.childElements().each( function(o) { o.remove(); } );
|
26
|
+
|
27
|
+
// adds a blank option, only if specified on options
|
28
|
+
if(include_blank) {
|
29
|
+
dependent_field.appendChild(new Element('option', {selected: !previous_value})); // it will be selected if previous_value is nil
|
30
|
+
}
|
31
|
+
|
32
|
+
// this fills the dependent select
|
33
|
+
values_array.each (function (e) {
|
34
|
+
if (e[2]==filter) { // only include options with the right filter field
|
35
|
+
var opt = new Element('option', {value: e[1]}); // create one <option> element...
|
36
|
+
if(collapse_spaces) { opt.text= e[0];} // assign the text (spaces are automatically collapsed)
|
37
|
+
else { opt.text = e[0].replace(/ /g, '\240'); } // replacing spaces with if requested
|
38
|
+
if(opt.value == previous_value) { opt.selected=true; } // mark it as selected if appropiate
|
39
|
+
dependent_field.options[dependent_field.options.length]=opt; // attach to depentent_select.. could not use "add"
|
40
|
+
}
|
41
|
+
});
|
42
|
+
|
43
|
+
// launch a custom event (Prototype doesn't allow launcthing "change") to support dependencies of dependencies
|
44
|
+
dependent_field.fire('DependentSelectFormBuilder:change');
|
45
|
+
}
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dependent_select
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.6.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Enrique Garcia Cota (egarcia)
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-08-10 00:00:00 +02:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: "dependent_select: a select that depends on other field and updates itself using js"
|
17
|
+
email: egarcia@splendeo.es
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.rdoc
|
24
|
+
files:
|
25
|
+
- init.rb
|
26
|
+
- lib/dependent_select.rb
|
27
|
+
- lib/dependent_select/dependent_select.rb
|
28
|
+
- lib/dependent_select/form_helpers.rb
|
29
|
+
- lib/dependent_select/includes_helper.rb
|
30
|
+
- public/javascripts/dependent_select/dependent_select.js
|
31
|
+
- README.rdoc
|
32
|
+
has_rdoc: true
|
33
|
+
homepage: http://github.com/splendeo/dependent_select
|
34
|
+
licenses: []
|
35
|
+
|
36
|
+
post_install_message:
|
37
|
+
rdoc_options:
|
38
|
+
- --charset=UTF-8
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: "0"
|
46
|
+
version:
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: "0"
|
52
|
+
version:
|
53
|
+
requirements: []
|
54
|
+
|
55
|
+
rubyforge_project:
|
56
|
+
rubygems_version: 1.3.5
|
57
|
+
signing_key:
|
58
|
+
specification_version: 2
|
59
|
+
summary: generates a select with some js code that updates if if another field is modified.
|
60
|
+
test_files: []
|
61
|
+
|