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