dependent_select 0.6.5

Sign up to get free protection for your applications and to get access to all the features.
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 &nbsp; 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 &nbsp; 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 &nbsp; 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
+