6b385ef77dff920f9c72ad5e6a0e084f0671c2ab
3 require 'action_view/helpers/form_helper'
7 # Provides a number of methods for turning different kinds of containers into a set of option tags.
9 # The <tt>collection_select</tt>, <tt>select</tt> and <tt>time_zone_select</tt> methods take an <tt>options</tt> parameter, a hash:
11 # * <tt>:include_blank</tt> - set to true or a prompt string if the first option element of the select element is a blank. Useful if there is not a default value required for the select element.
15 # select("post", "category", Post::CATEGORIES, {:include_blank => true})
19 # <select name="post[category]">
21 # <option>joke</option>
22 # <option>poem</option>
25 # Another common case is a select tag for an <tt>belongs_to</tt>-associated object.
27 # Example with @post.person_id => 2:
29 # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {:include_blank => 'None'})
33 # <select name="post[person_id]">
34 # <option value="">None</option>
35 # <option value="1">David</option>
36 # <option value="2" selected="selected">Sam</option>
37 # <option value="3">Tobias</option>
40 # * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this prepends an option with a generic prompt -- "Please select" -- or the given prompt string.
44 # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {:prompt => 'Select Person'})
48 # <select name="post[person_id]">
49 # <option value="">Select Person</option>
50 # <option value="1">David</option>
51 # <option value="2">Sam</option>
52 # <option value="3">Tobias</option>
55 # Like the other form helpers, +select+ can accept an <tt>:index</tt> option to manually set the ID used in the resulting output. Unlike other helpers, +select+ expects this
56 # option to be in the +html_options+ parameter.
60 # select("album[]", "genre", %w[rap rock country], {}, { :index => nil })
64 # <select name="album[][genre]" id="album__genre">
65 # <option value="rap">rap</option>
66 # <option value="rock">rock</option>
67 # <option value="country">country</option>
70 # * <tt>:disabled</tt> - can be a single value or an array of values that will be disabled options in the final output.
74 # select("post", "category", Post::CATEGORIES, {:disabled => 'restricted'})
78 # <select name="post[category]">
80 # <option>joke</option>
81 # <option>poem</option>
82 # <option disabled="disabled">restricted</option>
85 # When used with the <tt>collection_select</tt> helper, <tt>:disabled</tt> can also be a Proc that identifies those options that should be disabled.
89 # collection_select(:post, :category_id, Category.all, :id, :name, {:disabled => lambda{|category| category.archived? }})
91 # If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return:
92 # <select name="post[category_id]">
93 # <option value="1" disabled="disabled">2008 stuff</option>
94 # <option value="2" disabled="disabled">Christmas</option>
95 # <option value="3">Jokes</option>
96 # <option value="4">Poems</option>
99 module FormOptionsHelper
102 # Create a select tag and a series of contained option tags for the provided object and method.
103 # The option currently held by the object will be selected, provided that the object is available.
104 # See options_for_select for the required format of the choices parameter.
106 # Example with @post.person_id => 1:
107 # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, { :include_blank => true })
111 # <select name="post[person_id]">
112 # <option value=""></option>
113 # <option value="1" selected="selected">David</option>
114 # <option value="2">Sam</option>
115 # <option value="3">Tobias</option>
118 # This can be used to provide a default set of options in the standard way: before rendering the create form, a
119 # new model instance is assigned the default options and bound to @model_name. Usually this model is not saved
120 # to the database. Instead, a second model object is created when the create request is received.
121 # This allows the user to submit a form page more than once with the expected results of creating multiple records.
122 # In addition, this allows a single partial to be used to generate form inputs for both edit and create forms.
124 # By default, <tt>post.person_id</tt> is the selected option. Specify <tt>:selected => value</tt> to use a different selection
125 # or <tt>:selected => nil</tt> to leave all options unselected. Similarly, you can specify values to be disabled in the option
126 # tags by specifying the <tt>:disabled</tt> option. This can either be a single value or an array of values to be disabled.
127 def select(object
, method
, choices
, options
= {}, html_options
= {})
128 InstanceTag
.new(object
, method
, self, options
.delete(:object)).to_select_tag(choices
, options
, html_options
)
131 # Returns <tt><select></tt> and <tt><option></tt> tags for the collection of existing return values of
132 # +method+ for +object+'s class. The value returned from calling +method+ on the instance +object+ will
133 # be selected. If calling +method+ returns +nil+, no selection is made without including <tt>:prompt</tt>
134 # or <tt>:include_blank</tt> in the +options+ hash.
136 # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are methods to be called on each member
137 # of +collection+. The return values are used as the +value+ attribute and contents of each
138 # <tt><option></tt> tag, respectively.
140 # Example object structure for use with this method:
141 # class Post < ActiveRecord::Base
144 # class Author < ActiveRecord::Base
146 # def name_with_initial
147 # "#{first_name.first}. #{last_name}"
151 # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>):
152 # collection_select(:post, :author_id, Author.all, :id, :name_with_initial, {:prompt => true})
154 # If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return:
155 # <select name="post[author_id]">
156 # <option value="">Please select</option>
157 # <option value="1" selected="selected">D. Heinemeier Hansson</option>
158 # <option value="2">D. Thomas</option>
159 # <option value="3">M. Clark</option>
161 def collection_select(object
, method
, collection
, value_method
, text_method
, options
= {}, html_options
= {})
162 InstanceTag
.new(object
, method
, self, options
.delete(:object)).to_collection_select_tag(collection
, value_method
, text_method
, options
, html_options
)
165 # Return select and option tags for the given object and method, using
166 # #time_zone_options_for_select to generate the list of option tags.
168 # In addition to the <tt>:include_blank</tt> option documented above,
169 # this method also supports a <tt>:model</tt> option, which defaults
170 # to TimeZone. This may be used by users to specify a different time
171 # zone model object. (See +time_zone_options_for_select+ for more
174 # You can also supply an array of TimeZone objects
175 # as +priority_zones+, so that they will be listed above the rest of the
176 # (long) list. (You can use TimeZone.us_zones as a convenience for
177 # obtaining a list of the US time zones, or a Regexp to select the zones
180 # Finally, this method supports a <tt>:default</tt> option, which selects
181 # a default TimeZone if the object's time zone is +nil+.
184 # time_zone_select( "user", "time_zone", nil, :include_blank => true)
186 # time_zone_select( "user", "time_zone", nil, :default => "Pacific Time (US & Canada)" )
188 # time_zone_select( "user", 'time_zone', TimeZone.us_zones, :default => "Pacific Time (US & Canada)")
190 # time_zone_select( "user", 'time_zone', [ TimeZone['Alaska'], TimeZone['Hawaii'] ])
192 # time_zone_select( "user", 'time_zone', /Australia/)
194 # time_zone_select( "user", "time_zone", TZInfo::Timezone.all.sort, :model => TZInfo::Timezone)
195 def time_zone_select(object
, method
, priority_zones
= nil, options
= {}, html_options
= {})
196 InstanceTag
.new(object
, method
, self, options
.delete(:object)).to_time_zone_select_tag(priority_zones
, options
, html_options
)
199 # Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container
200 # where the elements respond to first and last (such as a two-element array), the "lasts" serve as option values and
201 # the "firsts" as option text. Hashes are turned into this form automatically, so the keys become "firsts" and values
202 # become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. +selected+
203 # may also be an array of values to be selected when using a multiple select.
205 # Examples (call, result):
206 # options_for_select([["Dollar", "$"], ["Kroner", "DKK"]])
207 # <option value="$">Dollar</option>\n<option value="DKK">Kroner</option>
209 # options_for_select([ "VISA", "MasterCard" ], "MasterCard")
210 # <option>VISA</option>\n<option selected="selected">MasterCard</option>
212 # options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40")
213 # <option value="$20">Basic</option>\n<option value="$40" selected="selected">Plus</option>
215 # options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"])
216 # <option selected="selected">VISA</option>\n<option>MasterCard</option>\n<option selected="selected">Discover</option>
218 # If you wish to specify disabled option tags, set +selected+ to be a hash, with <tt>:disabled</tt> being either a value
219 # or array of values to be disabled. In this case, you can use <tt>:selected</tt> to specify selected option tags.
222 # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :disabled => "Super Platinum")
223 # <option value="Free">Free</option>\n<option value="Basic">Basic</option>\n<option value="Advanced">Advanced</option>\n<option value="Super Platinum" disabled="disabled">Super Platinum</option>
225 # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :disabled => ["Advanced", "Super Platinum"])
226 # <option value="Free">Free</option>\n<option value="Basic">Basic</option>\n<option value="Advanced" disabled="disabled">Advanced</option>\n<option value="Super Platinum" disabled="disabled">Super Platinum</option>
228 # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :selected => "Free", :disabled => "Super Platinum")
229 # <option value="Free" selected="selected">Free</option>\n<option value="Basic">Basic</option>\n<option value="Advanced">Advanced</option>\n<option value="Super Platinum" disabled="disabled">Super Platinum</option>
231 # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
232 def options_for_select(container
, selected
= nil)
233 container
= container
.to_a
if Hash
=== container
234 selected
, disabled
= extract_selected_and_disabled(selected
)
236 options_for_select
= container
.inject([]) do |options
, element
|
237 text
, value
= option_text_and_value(element
)
238 selected_attribute
= ' selected="selected"' if option_value_selected
?(value
, selected
)
239 disabled_attribute
= ' disabled="disabled"' if disabled
&& option_value_selected
?(value
, disabled
)
240 options
<< %(<option value="#{html_escape(value.to_s)}"#{selected_attribute}#{disabled_attribute}>#{html_escape(text.to_s)}</option>)
243 options_for_select.join("\n")
246 # Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning the
247 # the result of a call to the +value_method+ as the option value and the +text_method+ as the option text.
249 # options_from_collection_for_select(@people, 'id', 'name')
250 # This will output the same HTML as if you did this:
251 # <option value="#{person.id}">#{person.name}</option>
253 # This is more often than not used inside a #select_tag like this example:
254 # select_tag 'person', options_from_collection_for_select(@people, 'id', 'name')
256 # If +selected+ is specified as a value or array of values, the element(s) returning a match on +value_method+
257 # will be selected option tag(s).
259 # If +selected+ is specified as a Proc, those members of the collection that return true for the anonymous
260 # function are the selected values.
262 # +selected+ can also be a hash, specifying both <tt>:selected</tt> and/or <tt>:disabled</tt> values as required.
264 # Be sure to specify the same class as the +value_method+ when specifying selected or disabled options.
265 # Failure to do this will produce undesired results. Example:
266 # options_from_collection_for_select(@people, 'id', 'name', '1')
267 # Will not select a person with the id of 1 because 1 (an Integer) is not the same as '1' (a string)
268 # options_from_collection_for_select(@people, 'id', 'name', 1)
269 # should produce the desired results.
270 def options_from_collection_for_select(collection, value_method, text_method, selected = nil)
271 options = collection.map do |element|
272 [element.send(text_method), element.send(value_method)]
274 selected, disabled = extract_selected_and_disabled(selected)
276 select_deselect[:selected] = extract_values_from_collection(collection, value_method, selected)
277 select_deselect[:disabled] = extract_values_from_collection(collection, value_method, disabled)
279 options_for_select(options, select_deselect)
282 # Returns a string of <tt><option></tt> tags, like <tt>options_from_collection_for_select</tt>, but
283 # groups them by <tt><optgroup></tt> tags based on the object relationships of the arguments.
286 # * +collection+ - An array of objects representing the <tt><optgroup></tt> tags.
287 # * +group_method+ - The name of a method which, when called on a member of +collection+, returns an
288 # array of child objects representing the <tt><option></tt> tags.
289 # * group_label_method+ - The name of a method which, when called on a member of +collection+, returns a
290 # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag.
291 # * +option_key_method+ - The name of a method which, when called on a child object of a member of
292 # +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag.
293 # * +option_value_method+ - The name of a method which, when called on a child object of a member of
294 # +collection+, returns a value to be used as the contents of its <tt><option></tt> tag.
295 # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags,
296 # which will have the +selected+ attribute set. Corresponds to the return value of one of the calls
297 # to +option_key_method+. If +nil+, no selection is made. Can also be a hash if disabled values are
300 # Example object structure for use with this method:
301 # class Continent < ActiveRecord::Base
302 # has_many :countries
303 # # attribs: id, name
305 # class Country < ActiveRecord::Base
306 # belongs_to :continent
307 # # attribs: id, name, continent_id
311 # option_groups_from_collection_for_select(@continents, :countries, :name, :id, :name, 3)
314 # <optgroup label="Africa">
315 # <option value="1">Egypt</option>
316 # <option value="4">Rwanda</option>
319 # <optgroup label="Asia">
320 # <option value="3" selected="selected">China</option>
321 # <option value="12">India</option>
322 # <option value="5">Japan</option>
326 # <b>Note:</b> Only the <tt><optgroup></tt> and <tt><option></tt> tags are returned, so you still have to
327 # wrap the output in an appropriate <tt><select></tt> tag.
328 def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil)
329 collection.inject("") do |options_for_select, group|
330 group_label_string = eval("group.#{group_label_method}")
331 options_for_select += "<optgroup label=\"#{html_escape(group_label_string)}\">"
332 options_for_select += options_from_collection_for_select(eval("group.#{group_method}"), option_key_method, option_value_method, selected_key)
333 options_for_select += '</optgroup>'
337 # Returns a string of <tt><option></tt> tags, like <tt>options_for_select</tt>, but
338 # wraps them with <tt><optgroup></tt> tags.
341 # * +grouped_options+ - Accepts a nested array or hash of strings. The first value serves as the
342 # <tt><optgroup></tt> label while the second value must be an array of options. The second value can be a
343 # nested array of text-value pairs. See <tt>options_for_select</tt> for more info.
344 # Ex. ["North America",[["United States","US"],["Canada","CA"]]]
345 # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags,
346 # which will have the +selected+ attribute set. Note: It is possible for this value to match multiple options
347 # as you might have the same option in multiple groups. Each will then get <tt>selected="selected"</tt>.
348 # * +prompt+ - set to true or a prompt string. When the select element doesn’t have a value yet, this
349 # prepends an option with a generic prompt — "Please select" — or the given prompt string.
351 # Sample usage (Array):
352 # grouped_options = [
354 # [['United States','US'],'Canada']],
356 # ['Denmark','Germany','France']]
358 # grouped_options_for_select(grouped_options)
360 # Sample usage (Hash):
361 # grouped_options = {
362 # 'North America' => [['United States','US], 'Canada'],
363 # 'Europe' => ['Denmark','Germany','France']
365 # grouped_options_for_select(grouped_options)
368 # <optgroup label="Europe">
369 # <option value="Denmark">Denmark</option>
370 # <option value="Germany">Germany</option>
371 # <option value="France">France</option>
373 # <optgroup label="North America">
374 # <option value="US">United States</option>
375 # <option value="Canada">Canada</option>
378 # <b>Note:</b> Only the <tt><optgroup></tt> and <tt><option></tt> tags are returned, so you still have to
379 # wrap the output in an appropriate <tt><select></tt> tag.
380 def grouped_options_for_select(grouped_options, selected_key = nil, prompt = nil)
382 body << content_tag(:option, prompt, :value => "") if prompt
384 grouped_options = grouped_options.sort if grouped_options.is_a?(Hash)
386 grouped_options.each do |group|
387 body << content_tag(:optgroup, options_for_select(group[1], selected_key), :label => group[0])
393 # Returns a string of option tags for pretty much any time zone in the
394 # world. Supply a TimeZone name as +selected+ to have it marked as the
395 # selected option tag. You can also supply an array of TimeZone objects
396 # as +priority_zones+, so that they will be listed above the rest of the
397 # (long) list. (You can use TimeZone.us_zones as a convenience for
398 # obtaining a list of the US time zones, or a Regexp to select the zones
401 # The +selected+ parameter must be either +nil+, or a string that names
404 # By default, +model+ is the TimeZone constant (which can be obtained
405 # in Active Record as a value object). The only requirement is that the
406 # +model+ parameter be an object that responds to +all+, and returns
407 # an array of objects that represent time zones.
409 # NOTE: Only the option tags are returned, you have to wrap this call in
410 # a regular HTML select tag.
411 def time_zone_options_for_select(selected = nil, priority_zones = nil, model = ::ActiveSupport::TimeZone)
415 convert_zones = lambda { |list| list.map { |z| [ z.to_s, z.name ] } }
418 if priority_zones.is_a?(Regexp)
419 priority_zones = model.all.find_all {|z| z =~ priority_zones}
421 zone_options += options_for_select(convert_zones[priority_zones], selected)
422 zone_options += "<option value=\"\" disabled=\"disabled\">-------------</option>\n"
424 zones = zones.reject { |z| priority_zones.include?( z ) }
427 zone_options += options_for_select(convert_zones[zones], selected)
432 def option_text_and_value(option)
433 # Options are [text, value] pairs or strings used for both.
434 if !option.is_a?(String) and option.respond_to?(:first) and option.respond_to?(:last)
435 [option.first, option.last]
441 def option_value_selected?(value, selected)
442 if selected.respond_to?(:include?) && !selected.is_a?(String)
443 selected.include? value
449 def extract_selected_and_disabled(selected)
450 if selected.is_a?(Hash)
451 [selected[:selected], selected[:disabled]]
457 def extract_values_from_collection(collection, value_method, selected)
458 if selected.is_a?(Proc)
459 collection.map do |element|
460 element.send(value_method) if selected.call(element)
468 class InstanceTag #:nodoc:
469 include FormOptionsHelper
471 def to_select_tag(choices, options, html_options)
472 html_options = html_options.stringify_keys
473 add_default_name_and_id(html_options)
474 value = value(object)
475 selected_value = options.has_key?(:selected) ? options[:selected] : value
476 disabled_value = options.has_key?(:disabled) ? options[:disabled] : nil
477 content_tag("select", add_options(options_for_select(choices, :selected => selected_value, :disabled => disabled_value), options, selected_value), html_options)
480 def to_collection_select_tag(collection, value_method, text_method, options, html_options)
481 html_options = html_options.stringify_keys
482 add_default_name_and_id(html_options)
483 value = value(object)
484 disabled_value = options.has_key?(:disabled) ? options[:disabled] : nil
485 selected_value = options.has_key?(:selected) ? options[:selected] : value
487 "select", add_options(options_from_collection_for_select(collection, value_method, text_method, :selected => selected_value, :disabled => disabled_value), options, value), html_options
491 def to_time_zone_select_tag(priority_zones, options, html_options)
492 html_options = html_options.stringify_keys
493 add_default_name_and_id(html_options)
494 value = value(object)
495 content_tag("select",
497 time_zone_options_for_select(value || options[:default], priority_zones, options[:model] || ActiveSupport::TimeZone),
504 def add_options(option_tags, options, value = nil)
505 if options[:include_blank]
506 option_tags = "<option value=\"\">#{options[:include_blank] if options[:include_blank].kind_of?(String)}</option>\n" + option_tags
508 if value.blank? && options[:prompt]
509 ("<option value=\"\">#{options[:prompt].kind_of?(String) ? options[:prompt] : 'Please select'}</option>\n") + option_tags
517 def select(method, choices, options = {}, html_options = {})
518 @template.select(@object_name, method, choices, objectify_options(options), @default_options.merge(html_options))
521 def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
522 @template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options))
525 def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
526 @template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_options.merge(html_options))