splendeo-dependent_select 0.5.1

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,181 @@
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
+ ==Quick example: Store with country and province
10
+
11
+ On your layout:
12
+ <%= javascript_include_tag :defaults %>
13
+ <%= dependent_select_includes %>
14
+
15
+ On your new/edit views:
16
+
17
+ <% form_for :store do |f| %>
18
+ Country:<br/>
19
+ <% f.collection_select :country_id, @countries, :id, :name, :include_blanks => true %> <br />
20
+ Province:<br/>
21
+ <% f.collection_select :province_id, @provinces, :id, :name, :country_id, :include_blanks => true %> <br />
22
+ City:<br/>
23
+ <% f.collection_select :city_id, @cities, :id, :name, :province_id, :include_blanks => true %> <br />
24
+ <% end %>
25
+
26
+ In order for this to work properly, the Store model must have methods for +country_id+ and +store_id+.
27
+ The best way I've found for implementing this is by using +delegate+. So the model would be:
28
+
29
+ class Store < ActiveRecord::Base
30
+ belongs_to :city, :include => [{:province => :country}] #useful to preload these
31
+ delegate :country, :country_id, :country_id=, :to =>:city
32
+ delegate :province, :province_id, :province_id=, :to =>:city
33
+ end
34
+
35
+ Notice that I've delegated the country to the city - so the city should probably have another +delegate+ line:
36
+
37
+ class City < ActiveRecord::Base
38
+ belongs_to :province, :include => [:country] #again, useful but not needed
39
+ delegate :country, :country_id, :country_id=, :to =>:province
40
+ end
41
+
42
+ Finally, the controller might look like this:
43
+
44
+ class StoresController < ApplicationController
45
+ before_filter :fill_selects :only => [:new, :edit, :update, :create]
46
+
47
+ {...} # Standard scaffold-generated methods
48
+
49
+ protected
50
+ def fill_selects
51
+ @countries = Country.find(:all, :order => 'name ASC')
52
+ @provinces = Province.find(:all, :order => 'name ASC') # all provinces for all countries
53
+ @cities = City.find(:all, :order => 'name ASC') # all cities for all provinces
54
+ end
55
+ end
56
+
57
+ This will generate a regular +collection_select+ for the country and a +dependent_collection_select+
58
+ for province. The later will be a regular +collection_select+ followed by a js +<script>+ tag that:
59
+ * Will create an array with all the provinces. (+var array=[province1, province2...];+)
60
+ * Will place a listeners on the +country_id+ +select+ in order to update the provinces select if
61
+ the countries select is modified
62
+ * Fill up the provinces select with appropiate values.
63
+
64
+ *Note that this will not work if you haven't followed the installation procedure - see below*
65
+
66
+ There's a more complex example at the end of this document.
67
+
68
+ ==Installation
69
+
70
+ ===As a gem
71
+ Copy this on config/environment.erb, inside the gems section
72
+ config.gem "splendeo-dependent_select", :lib => 'dependent_select', :source => "http://gems.github.com"
73
+ Then execute
74
+ rake gems:install
75
+
76
+ 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.
77
+
78
+ ===As a plug-in
79
+ I actually haven't tried this, sorry I don't know how to do it.
80
+
81
+ ==Re-installation
82
+ ===As a gem
83
+ Several steps are needed:
84
+ * It is recommended that you uninstall the gem before installing a new version.
85
+ * You must remove this file: public/javascripts/dependent_select/dependent_select.js
86
+ * And then install the new version
87
+
88
+ In other words:
89
+ sudo gem uninstall splendeo-dependent_select
90
+ rm public/javascripts/dependent_select/dependent_select.js
91
+ rake gems:install
92
+
93
+ ===As a plug-in
94
+ I haven't looked into that yet.
95
+
96
+ ==No AJAX? I can't sent the client all my cities!
97
+
98
+ No AJAX for now, sorry. Just plain old javascript.
99
+
100
+ However, it might interest you that you'll be generating this:
101
+
102
+ <script>
103
+ var whatever = [['opt1',1,1],['opt2',2,1],['opt3',3,1]...];
104
+ </script>
105
+
106
+ Instead of this :
107
+
108
+ <option value='1'>opt1</option>
109
+ <option value='2'>opt2</option>
110
+ <option value='3'>opt3</option>
111
+
112
+ In our tests, generating information for 8000 cities took arond 20k - the size of a small image.
113
+
114
+ Make the test and then decide. It will not take you more than 10 minutes.
115
+
116
+ ==Complex example: Employee with home city and work city
117
+
118
+ On this case we have an employee model with 2 relationships with cities. So the employee model
119
+ might look like the one below. Notice that the +delegates+ get a bit more complicated.
120
+
121
+ class Employee < ActiveRecord::Base
122
+
123
+ belongs_to :home_city, :class_name => "City", :include => [{:province => :country}]
124
+ delegate :country, :country_id, :country_id=, :to =>:home_city,
125
+ :allow_nil => true, :prefix => :home
126
+ delegate :province, :province_id, :province_id=, :to =>:home_city,
127
+ :allow_nil => true, :prefix => :home
128
+
129
+ belongs_to :work_city, :class_name => "City", :include => [{:province => :country}]
130
+ delegate :country, :country_id, :country_id=, :to =>:work_city,
131
+ :allow_nil => true, :prefix => :work
132
+ delegate :province, :province_id, :province_id=, :to =>:work_city,
133
+ :allow_nil => true, :prefix => :home
134
+
135
+ end
136
+
137
+ On your layout:
138
+ <%= javascript_include_tag :defaults %>
139
+ <%= dependent_select_includes %>
140
+
141
+ On your new/edit views, the "filter" for provinces isn't +:country_id+ any more, but +:home_country_id+ or
142
+ +:work_country_id+. The same happens with the cities and the provinces. You have to tell the selects
143
+ where to find the right filter fields, using the +filter_field+ option.
144
+
145
+ <% form_for :employee do |f| %>
146
+ Home Country:<br/>
147
+ <% f.collection_select :home_country_id, @countries, :id, :name, :include_blanks => true %> <br />
148
+ Home Province:<br/>
149
+ <% f.dependent_collection_select :home_province_id, @provinces, :id, :name, :country_id,
150
+ :filter_field => :home_country_id,
151
+ :include_blanks => true %> <br />
152
+ Home City:<br/>
153
+ <% f.dependent_collection_select :home_city_id, @cities, :id, :name, :city_id,
154
+ :filter_field => :home_province_id,
155
+ :include_blanks => true %> <br />
156
+ Work Country:<br/>
157
+ <% f.collection_select :work_country_id, @countries, :id, :name, :include_blanks => true %> <br />
158
+ Work Province:<br/>
159
+ <% f.dependent_collection_select :work_province_id, @provinces, :id, :name, :country_id,
160
+ :filter_field => :work_country_id,
161
+ :include_blanks => true %> <br />
162
+ Work City:<br/>
163
+ <% f.dependent_collection_select :work_city_id, @cities, :id, :name, :city_id,
164
+ :filter_field => :work_province_id,
165
+ :include_blanks => true %> <br />
166
+ <% end %>
167
+
168
+ On your controller:
169
+
170
+ class EmployeesController < ApplicationController
171
+ before_filter :fill_selects :only => [:new, :edit, :update, :create]
172
+
173
+ {...} # Standard scaffold-generated methods
174
+
175
+ protected
176
+ def fill_selects
177
+ @countries = Country.find(:all, :order => 'name ASC')
178
+ @provinces = Province.find(:all, :order => 'name ASC') # all provinces for all countries
179
+ @cities = City.find(:all, :order => 'name ASC') # all cities for all provinces
180
+ end
181
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/lib/dependent_select.rb"
@@ -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,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_blanks => false
9
+ # )
10
+ def self.default_options
11
+ @default_options ||= { :collapse_blanks => true }
12
+ end
13
+
14
+ # By default, blank 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_blanks=(value)
25
+ default_options[:collapse_blanks] = value
26
+ end
27
+
28
+ end
@@ -0,0 +1,412 @@
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+, two 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_blanks 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
+ # === :filter_field
48
+ # The javascript employed for updating the dependent select needs a field for getting
49
+ # the "filter value". The default behaviour is calculating this field name by using the
50
+ # +:object_name+ and +:filter_value+ parameters. For example, on this case:
51
+ #
52
+ # <%= dependent_collection_select(:sale, :province_id, @provinces, :id, :name, :country_id) %>
53
+ #
54
+ # +:object_name+ is +:sale+, and +:filter_id+ is +:country_id+, so the javascript will look
55
+ # for a field called +'sale_country_id'+ on the html.
56
+ #
57
+ # It is possible to override this default behaviour by specifying a +:filter_field+ option.
58
+ # For example, in this case:
59
+ #
60
+ # <%= dependent_collection_select(:sale, :province_id, @provinces, :id, :name,
61
+ # :country_id, {:filter_field => :state_id})
62
+ # %>
63
+ #
64
+ # This will make the javascript to look for a field called +'sale_state_id+
65
+ # Notice that the chain 'sale_' is still appended to the field name. It is possible to override this
66
+ # by using the +:complete_filter_field+ option istead of this one.
67
+ #
68
+ # === :complete_filter_field
69
+ # Works the same way as :filter_field, except that it uses its value directly, instead
70
+ # of appending the :object_name at all. For example:
71
+ #
72
+ # <%= dependent_collection_select (:sale, :province_id, @provinces, :id, :name,
73
+ # :country_id, {:complete_filter_field => :the_province})
74
+ # %>
75
+ #
76
+ # This will make the javascript to look for a field called +'the_province'+ on the
77
+ # page, and use its value for filtering.
78
+ #
79
+ # == Examples
80
+ #
81
+ # === Example 1: A store on a City
82
+ # In a form for creating a Store, the three selects used for Store, Province and City.
83
+ #
84
+ # views/Stores/new and views/Stores/edit:
85
+ #
86
+ # <p>
87
+ # Country:
88
+ # <%= collection_select :store, :country_id, @countries, :id, :name %>
89
+ # </p><p>
90
+ # Province:
91
+ # <%= dependent_collection_select :store, :province_id, @provinces, :id, :name, :country_id %>
92
+ # </p><p>
93
+ # City:
94
+ # <%= dependent_collection_select :store, :city_id, @cities, :id, :name, :province_id %>
95
+ # </p>
96
+ #
97
+ # Notes:
98
+ # * The first helper is rail's regular +collection_select+, since countries don't
99
+ # "depend" on anything on this example.
100
+ # * You only need a +city_id+ on the +stores+ table (+belongs_to :city+).
101
+ #
102
+ # You need to define methods on your model for obtaining a +province_id+ and
103
+ # +country_id+. One of the possible ways is using rails' +delegate+ keyword. See
104
+ # example below (and note the +:include+ clause)
105
+ #
106
+ # class Store < ActiveRecord::Base
107
+ # belongs_to :city, :include => [{:province => :country}] #:include not necessary, but nice
108
+ # delegate :country, :country_id, :country_id=, :to =>:city, :allow_nil => true
109
+ # delegate :province, :province_id, :province_id=, :to =>:city, :allow_nil => true
110
+ # end
111
+ #
112
+ # This delegates the +province_id+ and +country_id+ methods to the +:city+ object_name.
113
+ # So a City must be able to handle country-related stuff too. Again, using +delegate+, you can
114
+ # do:
115
+ #
116
+ # class City < ActiveRecord::Base
117
+ # belongs_to :province, :include => [:country]
118
+ # delegate :country, :country_id, :country_id=, :to =>:province, :allow_nil => true
119
+ # end
120
+ #
121
+ # Note that I've also delegated +province+, +province=+, +country+ and +country=+ .
122
+ # This is so I'm able to do things like +puts store.country.name+.
123
+ # This is convenient but not needed.
124
+ #
125
+ # === Example 2: Using html_options
126
+ # Imagine that you want your selects to be of an specific css class, for formatting.
127
+ # You can accomplish this by using the +html_options+ parameter
128
+ #
129
+ # <p>
130
+ # Country:
131
+ # <%= collection_select :store, :country_id, @countries, :id, :name,
132
+ # {:include_blanks => true}, {:class=>'monospaced'}
133
+ # %>
134
+ # </p><p>
135
+ # Province:
136
+ # <%= dependent_collection_select :store, :province_id, @provinces, :id, :name,
137
+ # :country_id, {:include_blanks => true}, {:class=>'brightYellow'}
138
+ # %>
139
+ # </p><p>
140
+ # City:
141
+ # <%= dependent_collection_select :store, :city_id, @cities, :id, :name,
142
+ # :province_id, {:include_blanks => true}, {:class=>'navyBlue'}
143
+ # %>
144
+ # </p>
145
+ #
146
+ # Notice the use of +{}+ for the +:options+ parameter. If we wanted to include +html_options+
147
+ # but not options, we would have had to leave empty brackets.
148
+ #
149
+ # === Example 3: Multiple relations and +:filter_field+
150
+ # Imagine that you want your stores to have two cities instead of one; One for
151
+ # importing and another one for exporting. Let's call them +:import_city+ and
152
+ # +:export_city+:
153
+ #
154
+ # Models/Store.rb:
155
+ #
156
+ # class Store < ActiveRecord::Base
157
+ #
158
+ # belongs_to :import_city, :class_name => "City", :include => [{:province => :country}]
159
+ # delegate :country, :country_id, :country_id=, :to =>:import_city,
160
+ # :allow_nil => true, :prefix => :import
161
+ # delegate :province, :province_id, :province_id=, :to =>:import_city,
162
+ # :allow_nil => true, :prefix => :import
163
+ #
164
+ # belongs_to :export_city, :class_name => "City", :include => [{:province => :country}]
165
+ # delegate :country, :country_id, :country_id=, :to =>:export_city,
166
+ # :allow_nil => true, :prefix => :export
167
+ # delegate :province, :province_id, :province_id=, :to =>:export_city,
168
+ # :allow_nil => true, :prefix => :import
169
+ # end
170
+ #
171
+ # In this case, the store doesn't have a +:city_id+, +:country_id+ or +:province_id+. Instead, it has
172
+ # +:export_city_id+, +:export_country_id+ and +:export_province_id+, and the same for import.
173
+ #
174
+ # We'll have to use the +:filter_field+ option in order to use the right field for
175
+ # updating the selects.
176
+ #
177
+ # views/Stores/new and views/Stores/edit:
178
+ #
179
+ # <p>
180
+ # Import Country:
181
+ # <%= collection_select :store, :import_country_id, @countries, :id, :name %>
182
+ # </p><p>
183
+ # Import Province:
184
+ # <%= dependent_collection_select :store, :import_province_id, @provinces, :id, :name,
185
+ # :country_id, :filter_field => :import_country_id
186
+ # %>
187
+ # </p><p>
188
+ # Import City:
189
+ # <%= dependent_collection_select :store, :import_city_id, @cities, :id, :name,
190
+ # :province_id, :filter_field => :import_province_id %>
191
+ # </p><p>
192
+ # Export Country:
193
+ # <%= collection_collection_select :store, :export_country_id, @countries, :id, :name %>
194
+ # </p><p>
195
+ # Export Province:
196
+ # <%= dependent_collection_select :store, :export_province_id, @provinces, :id, :name,
197
+ # :country_id, :filter_field => :export_country_id
198
+ # %>
199
+ # </p><p>
200
+ # Export City:
201
+ # <%= dependent_select :store, :export_city_id, @cities, :id, :name,
202
+ # :province_id, :filter_field => :export_province_id %>
203
+ # </p>
204
+ #
205
+ def dependent_collection_select(object_name, method, collection, value_method,
206
+ text_method, filter_method, options = {}, html_options = {}
207
+ )
208
+ object, options, extra_options = dependent_select_process_options(options)
209
+
210
+ initial_collection = dependent_select_initial_collection(object,
211
+ method, collection, value_method)
212
+
213
+ tag = collection_select(object_name, method, initial_collection, value_method,
214
+ text_method, options, html_options)
215
+
216
+ script = dependent_select_js_for_collection(object_name, object, method,
217
+ collection, value_method, text_method, filter_method, options, extra_options)
218
+
219
+ return "#{tag}\n#{script}"
220
+ end
221
+
222
+
223
+ # Similar to +select+ form helper, but generates javascript for filtering the
224
+ # results depending on the value on another field.
225
+ # Consider using +dependent_collection_select+ instead of this one, it will probably
226
+ # help you more. And I'll be updating that one more often.
227
+ # == Parameters
228
+ # +object_name+:: The name of the object being modified by this select.
229
+ # Example: +:employee+
230
+ # +method+:: The name of the method on the object cadded "object_name" that this
231
+ # will affect. Example: +:city_id+
232
+ # +choices_with_filter+:: The structure is +[[opt1_txt, opt1_value, opt1_filter],
233
+ # [opt2_txt, opt2_value, opt2_filter] ... ]+.
234
+ # +filter_method:: The method being used for filtering. For example,
235
+ # +:province_id+ will filter cities by province.
236
+ # Important notice: this parameter also sets the DOM field
237
+ # id that should be used for getting the filter value.
238
+ # In other words, setting this to :province_id and the +object_name+ to
239
+ # :employee will mean that somewhere on your form there will be a
240
+ # field called "employee_province_id", used for filtering.
241
+ # +options+ and +html_options+:: See +dependent_collection_select+.
242
+ # == Examples
243
+ #
244
+ # === Example 1: Types of animals
245
+ # Create an animal on a children-oriented app, where groups and subgroups
246
+ # are predefined constants.
247
+ #
248
+ # models/animal.rb
249
+ #
250
+ # class Animal < ActiveRecord::Base
251
+ # GROUPS = [['Invertebrates', 1], ['Vertebrates', 2]]
252
+ # SUBGROUPS = [
253
+ # ['Protozoa', 1, 1], ['Echinoderms',2,1], ['Annelids',3,1], ['Mollusks',4,1],
254
+ # ['Arthropods',5,1], ['Crustaceans',6,1], ['Arachnids',7,1], ['Insects',8,1],
255
+ # ['Fish',9,2], ['Amphibians',10,2], ['Reptiles',11,2], ['Birds',12,2],
256
+ # ['Mammals',13,2], ['Marsupials',14,2], ['Primates',15,2], ['Rodents',16,2],
257
+ # ['Cetaceans',17,2], ['Seals, Seal Lions and Walrus',18,2]
258
+ # ]
259
+ # end
260
+ #
261
+ # new/edit animal html.erb
262
+ #
263
+ # <p>
264
+ # Group: <%= select :animal, :group_id, Animal::GROUPS %>
265
+ # </p><p>
266
+ # Subgroup: <%= select :animal, :subgroup_id, :group, Animal::SUBGROUPS %>
267
+ # </p>
268
+ #
269
+ def dependent_select(object_name, method, choices_with_filter, filter_method,
270
+ options = {}, html_options = {})
271
+
272
+ object, options, extra_options = dependent_select_process_options(options)
273
+
274
+ initial_choices = dependent_select_initial_choices(object, method, choices_with_filter)
275
+
276
+ tag = select(object_name, method, initial_choices, options, html_options)
277
+
278
+ script = dependent_select_js(object_name, method, choices_with_filter,
279
+ filter_method, options, extra_options)
280
+
281
+ return "#{tag}\n#{script}"
282
+ end
283
+
284
+ private
285
+ # extracts any options passed into calendar date select, appropriating them to either the Javascript call or the html tag.
286
+ def dependent_select_process_options(options)
287
+ options, extra_options = DependentSelect.default_options.merge(options), {}
288
+ for key in [:collapse_spaces, :filter_field, :complete_filter_field]
289
+ extra_options[key] = options.delete(key) if options.has_key?(key)
290
+ end
291
+
292
+ object = options.delete(:object) || instance_variable_get("@#{object}")
293
+
294
+ [object, options, extra_options]
295
+ end
296
+
297
+ # generates the javascript that will follow the dependent select
298
+ # contains:
299
+ # * An array with all the possible options (structure [text, value, filter])
300
+ # * An observer that detects changes on the "observed" field and triggers an update
301
+ # * An extra observer for custon events. These events are raised by the dependent_select itself.
302
+ # * An first call to update_dependent_select, that sets up the initial stuff
303
+ def dependent_select_js(object_name, object, method, choices_with_filter,
304
+ filter_method, options, extra_options)
305
+
306
+ # the js variable that will hold the array with option values, texts and filters
307
+ js_array_name = "ds_#{object_name}_#{method}_array"
308
+
309
+ dependent_id = dependent_select_calculate_id(object_name, method)
310
+ observed_id = dependent_select_calculate_observed_field_id(object_name, filter_method, extra_options)
311
+ initial_value = dependent_select_initial_value(object, method)
312
+ include_blank = options[:include_blank] || false
313
+ collapse_spaces = extra_options[:collapse_spaces] || false
314
+
315
+ js_callback =
316
+ "function(e) { update_dependent_select( '#{dependent_id}', '#{observed_id}', #{js_array_name}, " +
317
+ "'#{initial_value}', #{include_blank}, #{collapse_spaces}, false); }"
318
+
319
+ javascript_tag( "#{js_array_name} = #{choices_with_filter.to_json};\n" +
320
+ "$('#{observed_id}').observe ('change', #{js_callback});\n" +
321
+ "$('#{observed_id}').observe ('DependentSelectFormBuilder:change', #{js_callback}); \n" +
322
+ "update_dependent_select( '#{dependent_id}', '#{observed_id}', #{js_array_name}, " +
323
+ "'#{initial_value}', #{include_blank}, #{collapse_spaces}, true);"
324
+ )
325
+ end
326
+
327
+ # generates the js script for a dependent_collection_select. See +dependent_select_js+
328
+ def dependent_select_js_for_collection(object_name, object, method, collection,
329
+ value_method, text_method, filter_method, options, extra_options)
330
+
331
+ # the array that, converted to javascript, will be assigned values_var variable,
332
+ # so it can be used for updating the select
333
+ choices_with_filter = collection.collect do |c|
334
+ [ c.send(text_method), c.send(value_method), c.send(filter_method) ]
335
+ end
336
+
337
+ dependent_select_js(object_name, object, method, choices_with_filter,
338
+ filter_method, options, extra_options)
339
+ end
340
+
341
+ # Calculates the dom id of the observed field, usually concatenating object_name and filt. meth.
342
+ # For example, 'employee_province_id'. Exceptions:
343
+ # * If +extra_options+ has an item with key +:complete_filter_field+,
344
+ # it returns the value of that item
345
+ # * If +extra_options+ has an item with key +:filter_field+,
346
+ # it uses its value instead of +method+
347
+ def dependent_select_calculate_observed_field_id(object_name, filter_method, extra_options)
348
+ if extra_options.has_key? :complete_filter_field
349
+ return extra_options[:complete_filter_field]
350
+ elsif extra_options.has_key? :filter_field
351
+ method = extra_options[:filter_field]
352
+ end
353
+
354
+ dependent_select_calculate_id(object_name, filter_method)
355
+ end
356
+
357
+ # calculates one id. Usually it just concatenates object_method, ie 'employee_city_id'
358
+ def dependent_select_calculate_id(object_name, method)
359
+ sanitized_object_name = object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
360
+ sanitized_method_name = method.to_s.sub(/\?$/,"")
361
+
362
+ return "#{sanitized_object_name}_#{sanitized_method_name}"
363
+ end
364
+
365
+ # Returns the collection that will be used when the dependent_select is first displayed
366
+ # (before even the first update_dependent_select call is done)
367
+ # The collection is obained by taking all the elements in collection whose value
368
+ # equals to +initial_value+. For example if we are editing an employee with city_id=4,
369
+ # we should put here all the cities with id=4 (there could be more than one)
370
+ def dependent_select_initial_collection(object, method, collection, value_method)
371
+ initial_value = dependent_select_initial_value(object, method)
372
+ return collection.select { |c| c.send(value_method) == initial_value }
373
+ end
374
+
375
+ # this is +dependent_select+'s version of +dependent_select_initial_collection+
376
+ def dependent_select_initial_choices(object, method, choices_with_filter)
377
+ initial_value = dependent_select_initial_value(object, method)
378
+ return choices_with_filter.select { |c| c[1] == initial_value }
379
+ end
380
+
381
+ # The value that the dependend select will have when first rendered. It could be different from
382
+ # nil, if we are editing. Example: if we are editing an employee with city_id=4, then 4 should be
383
+ # the initial value.
384
+ def dependent_select_initial_value(object, method)
385
+ return object.send(method) || "" if object
386
+ return ""
387
+ end
388
+
389
+
390
+ end
391
+
392
+ # Helper method for form builders
393
+ module ActionView
394
+ module Helpers
395
+ class FormBuilder
396
+ def dependent_select( method, choices_with_filter, filter_method,
397
+ options = {}, html_options = {}
398
+ )
399
+ @template.dependent_select(@object_name, method, choices_with_filter,
400
+ filter_method, options.merge(:object => @object), html_options)
401
+ end
402
+
403
+ def dependent_collection_select( method, collection, value_method,
404
+ text_method, filter_method, options = {}, html_options = {}
405
+ )
406
+ @template.dependent_collection_select(@object_name, method, collection,
407
+ value_method, text_method, filter_method,
408
+ options.merge(:object => @object), html_options)
409
+ end
410
+ end
411
+ end
412
+ 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,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
+ opt.text= e[0]; // add text,
37
+ if(collapse_spaces) { 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,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: splendeo-dependent_select
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.1
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 -07: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
+ post_install_message:
36
+ rdoc_options:
37
+ - --charset=UTF-8
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: "0"
45
+ version:
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: "0"
51
+ version:
52
+ requirements: []
53
+
54
+ rubyforge_project:
55
+ rubygems_version: 1.3.5
56
+ signing_key:
57
+ specification_version: 2
58
+ summary: generates a select with some js code that updates if if another field is modified.
59
+ test_files: []
60
+