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