form_options_helper.rb 41.2 KB
Newer Older
D
Initial  
David Heinemeier Hansson 已提交
1 2
require 'cgi'
require 'erb'
3
require 'action_view/helpers/form_helper'
4
require 'active_support/core_ext/string/output_safety'
5
require 'active_support/core_ext/array/extract_options'
R
Rafael Mendonça França 已提交
6
require 'active_support/core_ext/array/wrap'
D
Initial  
David Heinemeier Hansson 已提交
7 8

module ActionView
9
  # = Action View Form Option Helpers
D
Initial  
David Heinemeier Hansson 已提交
10
  module Helpers
11
    # Provides a number of methods for turning different kinds of containers into a set of option tags.
12
    #
13
    # The <tt>collection_select</tt>, <tt>select</tt> and <tt>time_zone_select</tt> methods take an <tt>options</tt> parameter, a hash:
14
    #
15 16
    # * <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.
    #
A
AvnerCohen 已提交
17
    #   select("post", "category", Post::CATEGORIES, {include_blank: true})
18
    #
19
    # could become:
20 21 22 23 24 25
    #
    #   <select name="post[category]">
    #     <option></option>
    #     <option>joke</option>
    #     <option>poem</option>
    #   </select>
26
    #
27
    # Another common case is a select tag for a <tt>belongs_to</tt>-associated object.
28 29 30
    #
    # Example with @post.person_id => 2:
    #
A
AvnerCohen 已提交
31
    #   select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: 'None'})
32 33 34 35 36 37 38 39 40 41
    #
    # could become:
    #
    #   <select name="post[person_id]">
    #     <option value="">None</option>
    #     <option value="1">David</option>
    #     <option value="2" selected="selected">Sam</option>
    #     <option value="3">Tobias</option>
    #   </select>
    #
42 43
    # * <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.
    #
A
AvnerCohen 已提交
44
    #   select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {prompt: 'Select Person'})
45 46 47 48
    #
    # could become:
    #
    #   <select name="post[person_id]">
49
    #     <option value="">Select Person</option>
50 51 52 53
    #     <option value="1">David</option>
    #     <option value="2">Sam</option>
    #     <option value="3">Tobias</option>
    #   </select>
54 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.
57
    #
A
AvnerCohen 已提交
58
    #   select("album[]", "genre", %w[rap rock country], {}, { index: nil })
59
    #
60
    # becomes:
61
    #
62 63 64 65 66
    #   <select name="album[][genre]" id="album__genre">
    #     <option value="rap">rap</option>
    #     <option value="rock">rock</option>
    #     <option value="country">country</option>
    #   </select>
67 68 69
    #
    # * <tt>:disabled</tt> - can be a single value or an array of values that will be disabled options in the final output.
    #
A
AvnerCohen 已提交
70
    #   select("post", "category", Post::CATEGORIES, {disabled: 'restricted'})
71 72 73 74 75 76 77 78 79 80 81 82
    #
    # could become:
    #
    #   <select name="post[category]">
    #     <option></option>
    #     <option>joke</option>
    #     <option>poem</option>
    #     <option disabled="disabled">restricted</option>
    #   </select>
    #
    # 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.
    #
A
AvnerCohen 已提交
83
    #   collection_select(:post, :category_id, Category.all, :id, :name, {disabled: lambda{|category| category.archived? }})
84 85 86 87 88 89 90 91 92
    #
    # If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return:
    #   <select name="post[category_id]">
    #     <option value="1" disabled="disabled">2008 stuff</option>
    #     <option value="2" disabled="disabled">Christmas</option>
    #     <option value="3">Jokes</option>
    #     <option value="4">Poems</option>
    #   </select>
    #
D
Initial  
David Heinemeier Hansson 已提交
93
    module FormOptionsHelper
W
wycats 已提交
94 95
      # ERB::Util can mask some helpers like textilize. Make sure to include them.
      include TextHelper
96 97

      # Create a select tag and a series of contained option tags for the provided object and method.
98
      # The option currently held by the object will be selected, provided that the object is available.
A
Andrew Radev 已提交
99
      #
X
Xavier Noria 已提交
100 101 102 103 104 105 106
      # There are two possible formats for the +choices+ parameter, corresponding to other helpers' output:
      #
      # * A flat collection (see +options_for_select+).
      #
      # * A nested collection (see +grouped_options_for_select+).
      #
      # For example:
107
      #
A
AvnerCohen 已提交
108
      #   select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, { include_blank: true })
109
      #
X
Xavier Noria 已提交
110
      # would become:
111
      #
112
      #   <select name="post[person_id]">
113
      #     <option value=""></option>
114 115 116 117
      #     <option value="1" selected="selected">David</option>
      #     <option value="2">Sam</option>
      #     <option value="3">Tobias</option>
      #   </select>
118
      #
X
Xavier Noria 已提交
119 120
      # assuming the associated person has ID 1.
      #
121 122 123 124 125
      # This can be used to provide a default set of options in the standard way: before rendering the create form, a
      # new model instance is assigned the default options and bound to @model_name. Usually this model is not saved
      # to the database. Instead, a second model object is created when the create request is received.
      # This allows the user to submit a form page more than once with the expected results of creating multiple records.
      # In addition, this allows a single partial to be used to generate form inputs for both edit and create forms.
126
      #
A
AvnerCohen 已提交
127 128
      # By default, <tt>post.person_id</tt> is the selected option. Specify <tt>selected: value</tt> to use a different selection
      # or <tt>selected: nil</tt> to leave all options unselected. Similarly, you can specify values to be disabled in the option
129
      # tags by specifying the <tt>:disabled</tt> option. This can either be a single value or an array of values to be disabled.
130
      #
131 132 133 134 135 136 137 138 139
      # A block can be passed to +select+ to customize how the options tags will be rendered. This
      # is useful when the options tag has complex attributes.
      #
      #   select(report, "campaign_ids") do
      #     available_campaigns.each do |c|
      #       content_tag(:option, c.name, value: c.id, data: { tags: c.tags.to_json })
      #     end
      #   end
      #
140 141
      # ==== Gotcha
      #
142
      # The HTML specification says when +multiple+ parameter passed to select and all options got deselected
143 144 145 146 147
      # web browsers do not send any value to server. Unfortunately this introduces a gotcha:
      # if an +User+ model has many +roles+ and have +role_ids+ accessor, and in the form that edits roles of the user
      # the user deselects all roles from +role_ids+ multiple select box, no +role_ids+ parameter is sent. So,
      # any mass-assignment idiom like
      #
148
      #   @user.update(params[:user])
149 150 151 152 153 154
      #
      # wouldn't update roles.
      #
      # To prevent this the helper generates an auxiliary hidden field before
      # every multiple select. The hidden field has the same name as multiple select and blank value.
      #
155 156 157
      # <b>Note:</b> The client either sends only the hidden field (representing
      # the deselected multiple select box), or both fields. This means that the resulting array
      # always contains a blank string.
158
      #
159 160
      # In case if you don't want the helper to generate this hidden field you can specify
      # <tt>include_hidden: false</tt> option.
161
      #
162 163
      def select(object, method, choices = nil, options = {}, html_options = {}, &block)
        Tags::Select.new(object, method, self, choices, options, html_options, &block).render
D
Initial  
David Heinemeier Hansson 已提交
164
      end
165

166 167 168 169 170 171 172
      # Returns <tt><select></tt> and <tt><option></tt> tags for the collection of existing return values of
      # +method+ for +object+'s class. The value returned from calling +method+ on the instance +object+ will
      # be selected. If calling +method+ returns +nil+, no selection is made without including <tt>:prompt</tt>
      # or <tt>:include_blank</tt> in the +options+ hash.
      #
      # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are methods to be called on each member
      # of +collection+. The return values are used as the +value+ attribute and contents of each
173 174 175
      # <tt><option></tt> tag, respectively. They can also be any object that responds to +call+, such
      # as a +proc+, that will be called for each member of the +collection+ to
      # retrieve the value/text.
176
      #
177
      # Example object structure for use with this method:
178
      #
179 180 181
      #   class Post < ActiveRecord::Base
      #     belongs_to :author
      #   end
182
      #
183 184 185 186 187 188 189
      #   class Author < ActiveRecord::Base
      #     has_many :posts
      #     def name_with_initial
      #       "#{first_name.first}. #{last_name}"
      #     end
      #   end
      #
P
Pratik Naik 已提交
190
      # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>):
191
      #
A
AvnerCohen 已提交
192
      #   collection_select(:post, :author_id, Author.all, :id, :name_with_initial, prompt: true)
193 194 195 196 197 198 199 200
      #
      # If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return:
      #   <select name="post[author_id]">
      #     <option value="">Please select</option>
      #     <option value="1" selected="selected">D. Heinemeier Hansson</option>
      #     <option value="2">D. Thomas</option>
      #     <option value="3">M. Clark</option>
      #   </select>
D
Initial  
David Heinemeier Hansson 已提交
201
      def collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {})
202
        Tags::CollectionSelect.new(object, method, self, collection, value_method, text_method, options, html_options).render
D
Initial  
David Heinemeier Hansson 已提交
203
      end
204

205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
      # Returns <tt><select></tt>, <tt><optgroup></tt> and <tt><option></tt> tags for the collection of existing return values of
      # +method+ for +object+'s class. The value returned from calling +method+ on the instance +object+ will
      # be selected. If calling +method+ returns +nil+, no selection is made without including <tt>:prompt</tt>
      # or <tt>:include_blank</tt> in the +options+ hash.
      #
      # Parameters:
      # * +object+ - The instance of the class to be used for the select tag
      # * +method+ - The attribute of +object+ corresponding to the select tag
      # * +collection+ - An array of objects representing the <tt><optgroup></tt> tags.
      # * +group_method+ - The name of a method which, when called on a member of +collection+, returns an
      #   array of child objects representing the <tt><option></tt> tags.
      # * +group_label_method+ - The name of a method which, when called on a member of +collection+, returns a
      #   string to be used as the +label+ attribute for its <tt><optgroup></tt> tag.
      # * +option_key_method+ - The name of a method which, when called on a child object of a member of
      #   +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag.
      # * +option_value_method+ - The name of a method which, when called on a child object of a member of
      #   +collection+, returns a value to be used as the contents of its <tt><option></tt> tag.
      #
      # Example object structure for use with this method:
224
      #
225 226 227 228
      #   class Continent < ActiveRecord::Base
      #     has_many :countries
      #     # attribs: id, name
      #   end
229
      #
230 231 232 233
      #   class Country < ActiveRecord::Base
      #     belongs_to :continent
      #     # attribs: id, name, continent_id
      #   end
234
      #
235 236 237 238 239 240
      #   class City < ActiveRecord::Base
      #     belongs_to :country
      #     # attribs: id, name, country_id
      #   end
      #
      # Sample usage:
241
      #
242 243 244
      #   grouped_collection_select(:city, :country_id, @continents, :countries, :name, :id, :name)
      #
      # Possible output:
245
      #
246 247 248 249 250 251 252 253 254 255 256 257
      #   <select name="city[country_id]">
      #     <optgroup label="Africa">
      #       <option value="1">South Africa</option>
      #       <option value="3">Somalia</option>
      #     </optgroup>
      #     <optgroup label="Europe">
      #       <option value="7" selected="selected">Denmark</option>
      #       <option value="2">Ireland</option>
      #     </optgroup>
      #   </select>
      #
      def grouped_collection_select(object, method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
258
        Tags::GroupedCollectionSelect.new(object, method, self, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options).render
259 260
      end

261
      # Returns select and option tags for the given object and method, using
262 263 264 265
      # #time_zone_options_for_select to generate the list of option tags.
      #
      # In addition to the <tt>:include_blank</tt> option documented above,
      # this method also supports a <tt>:model</tt> option, which defaults
P
Pratik Naik 已提交
266 267 268
      # to ActiveSupport::TimeZone. This may be used by users to specify a
      # different time zone model object. (See +time_zone_options_for_select+
      # for more information.)
269
      #
P
Pratik Naik 已提交
270
      # You can also supply an array of ActiveSupport::TimeZone objects
271
      # as +priority_zones+, so that they will be listed above the rest of the
P
Pratik Naik 已提交
272 273
      # (long) list. (You can use ActiveSupport::TimeZone.us_zones as a convenience
      # for obtaining a list of the US time zones, or a Regexp to select the zones
274
      # of your choice)
275
      #
276
      # Finally, this method supports a <tt>:default</tt> option, which selects
P
Pratik Naik 已提交
277
      # a default ActiveSupport::TimeZone if the object's time zone is +nil+.
278
      #
A
AvnerCohen 已提交
279
      #   time_zone_select( "user", "time_zone", nil, include_blank: true)
280
      #
A
AvnerCohen 已提交
281
      #   time_zone_select( "user", "time_zone", nil, default: "Pacific Time (US & Canada)" )
282
      #
A
AvnerCohen 已提交
283
      #   time_zone_select( "user", 'time_zone', ActiveSupport::TimeZone.us_zones, default: "Pacific Time (US & Canada)")
284
      #
P
Pratik Naik 已提交
285
      #   time_zone_select( "user", 'time_zone', [ ActiveSupport::TimeZone['Alaska'], ActiveSupport::TimeZone['Hawaii'] ])
286
      #
287 288
      #   time_zone_select( "user", 'time_zone', /Australia/)
      #
A
AvnerCohen 已提交
289
      #   time_zone_select( "user", "time_zone", ActiveSupport::TimeZone.all.sort, model: ActiveSupport::TimeZone)
290
      def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {})
291
        Tags::TimeZoneSelect.new(object, method, self, priority_zones, options, html_options).render
292 293
      end

294
      # Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container
D
Initial  
David Heinemeier Hansson 已提交
295 296
      # where the elements respond to first and last (such as a two-element array), the "lasts" serve as option values and
      # the "firsts" as option text. Hashes are turned into this form automatically, so the keys become "firsts" and values
297
      # become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. +selected+
298
      # may also be an array of values to be selected when using a multiple select.
D
Initial  
David Heinemeier Hansson 已提交
299 300
      #
      #   options_for_select([["Dollar", "$"], ["Kroner", "DKK"]])
301 302
      #   # => <option value="$">Dollar</option>
      #   # => <option value="DKK">Kroner</option>
D
Initial  
David Heinemeier Hansson 已提交
303
      #
304
      #   options_for_select([ "VISA", "MasterCard" ], "MasterCard")
305 306
      #   # => <option>VISA</option>
      #   # => <option selected="selected">MasterCard</option>
D
Initial  
David Heinemeier Hansson 已提交
307 308
      #
      #   options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40")
309 310
      #   # => <option value="$20">Basic</option>
      #   # => <option value="$40" selected="selected">Plus</option>
D
Initial  
David Heinemeier Hansson 已提交
311
      #
312
      #   options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"])
313 314 315
      #   # => <option selected="selected">VISA</option>
      #   # => <option>MasterCard</option>
      #   # => <option selected="selected">Discover</option>
316
      #
317 318
      # You can optionally provide html attributes as the last element of the array.
      #
A
AvnerCohen 已提交
319
      #   options_for_select([ "Denmark", ["USA", {class: 'bold'}], "Sweden" ], ["USA", "Sweden"])
320 321 322
      #   # => <option value="Denmark">Denmark</option>
      #   # => <option value="USA" class="bold" selected="selected">USA</option>
      #   # => <option value="Sweden" selected="selected">Sweden</option>
323
      #
A
AvnerCohen 已提交
324
      #   options_for_select([["Dollar", "$", {class: "bold"}], ["Kroner", "DKK", {onclick: "alert('HI');"}]])
325 326
      #   # => <option value="$" class="bold">Dollar</option>
      #   # => <option value="DKK" onclick="alert('HI');">Kroner</option>
327
      #
328 329 330
      # If you wish to specify disabled option tags, set +selected+ to be a hash, with <tt>:disabled</tt> being either a value
      # or array of values to be disabled. In this case, you can use <tt>:selected</tt> to specify selected option tags.
      #
A
AvnerCohen 已提交
331
      #   options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], disabled: "Super Platinum")
332 333 334 335
      #   # => <option value="Free">Free</option>
      #   # => <option value="Basic">Basic</option>
      #   # => <option value="Advanced">Advanced</option>
      #   # => <option value="Super Platinum" disabled="disabled">Super Platinum</option>
336
      #
A
AvnerCohen 已提交
337
      #   options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], disabled: ["Advanced", "Super Platinum"])
338 339 340 341
      #   # => <option value="Free">Free</option>
      #   # => <option value="Basic">Basic</option>
      #   # => <option value="Advanced" disabled="disabled">Advanced</option>
      #   # => <option value="Super Platinum" disabled="disabled">Super Platinum</option>
342
      #
A
AvnerCohen 已提交
343
      #   options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], selected: "Free", disabled: "Super Platinum")
344 345 346 347
      #   # => <option value="Free" selected="selected">Free</option>
      #   # => <option value="Basic">Basic</option>
      #   # => <option value="Advanced">Advanced</option>
      #   # => <option value="Super Platinum" disabled="disabled">Super Platinum</option>
348
      #
349
      # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
D
Initial  
David Heinemeier Hansson 已提交
350
      def options_for_select(container, selected = nil)
351 352
        return container if String === container

353 354
        selected, disabled = extract_selected_and_disabled(selected).map do |r|
          Array(r).map { |item| item.to_s }
355
        end
356

357
        container.map do |element|
358
          html_attributes = option_html_attributes(element)
359
          text, value = option_text_and_value(element).map { |item| item.to_s }
V
Vasiliy Ermolovich 已提交
360

361 362
          html_attributes[:selected] ||= option_value_selected?(value, selected)
          html_attributes[:disabled] ||= disabled && option_value_selected?(value, disabled)
V
Vasiliy Ermolovich 已提交
363 364
          html_attributes[:value] = value

365
          content_tag_string(:option, text, html_attributes)
366
        end.join("\n").html_safe
D
Initial  
David Heinemeier Hansson 已提交
367 368
      end

369
      # Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning
D
Initial  
David Heinemeier Hansson 已提交
370
      # the result of a call to the +value_method+ as the option value and the +text_method+ as the option text.
371
      #
P
Pratik Naik 已提交
372
      #   options_from_collection_for_select(@people, 'id', 'name')
373
      #   # => <option value="#{person.id}">#{person.name}</option>
D
Initial  
David Heinemeier Hansson 已提交
374
      #
P
Pratik Naik 已提交
375
      # This is more often than not used inside a #select_tag like this example:
376
      #
P
Pratik Naik 已提交
377
      #   select_tag 'person', options_from_collection_for_select(@people, 'id', 'name')
378
      #
379 380 381 382 383 384 385 386 387
      # If +selected+ is specified as a value or array of values, the element(s) returning a match on +value_method+
      # will be selected option tag(s).
      #
      # If +selected+ is specified as a Proc, those members of the collection that return true for the anonymous
      # function are the selected values.
      #
      # +selected+ can also be a hash, specifying both <tt>:selected</tt> and/or <tt>:disabled</tt> values as required.
      #
      # Be sure to specify the same class as the +value_method+ when specifying selected or disabled options.
P
Pratik Naik 已提交
388 389 390 391 392
      # Failure to do this will produce undesired results. Example:
      #   options_from_collection_for_select(@people, 'id', 'name', '1')
      # Will not select a person with the id of 1 because 1 (an Integer) is not the same as '1' (a string)
      #   options_from_collection_for_select(@people, 'id', 'name', 1)
      # should produce the desired results.
393 394
      def options_from_collection_for_select(collection, value_method, text_method, selected = nil)
        options = collection.map do |element|
395
          [value_for_collection(element, text_method), value_for_collection(element, value_method), option_html_attributes(element)]
396
        end
397
        selected, disabled = extract_selected_and_disabled(selected)
398
        select_deselect = {
399 400
          selected: extract_values_from_collection(collection, value_method, selected),
          disabled: extract_values_from_collection(collection, value_method, disabled)
401
        }
402 403

        options_for_select(options, select_deselect)
D
Initial  
David Heinemeier Hansson 已提交
404 405
      end

P
Pratik Naik 已提交
406
      # Returns a string of <tt><option></tt> tags, like <tt>options_from_collection_for_select</tt>, but
407 408 409
      # groups them by <tt><optgroup></tt> tags based on the object relationships of the arguments.
      #
      # Parameters:
P
Pratik Naik 已提交
410 411 412 413 414 415 416 417 418 419 420
      # * +collection+ - An array of objects representing the <tt><optgroup></tt> tags.
      # * +group_method+ - The name of a method which, when called on a member of +collection+, returns an
      #   array of child objects representing the <tt><option></tt> tags.
      # * group_label_method+ - The name of a method which, when called on a member of +collection+, returns a
      #   string to be used as the +label+ attribute for its <tt><optgroup></tt> tag.
      # * +option_key_method+ - The name of a method which, when called on a child object of a member of
      #   +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag.
      # * +option_value_method+ - The name of a method which, when called on a child object of a member of
      #   +collection+, returns a value to be used as the contents of its <tt><option></tt> tag.
      # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags,
      #   which will have the +selected+ attribute set. Corresponds to the return value of one of the calls
421 422
      #   to +option_key_method+. If +nil+, no selection is made. Can also be a hash if disabled values are
      #   to be specified.
423 424
      #
      # Example object structure for use with this method:
425
      #
426 427 428 429
      #   class Continent < ActiveRecord::Base
      #     has_many :countries
      #     # attribs: id, name
      #   end
430
      #
431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454
      #   class Country < ActiveRecord::Base
      #     belongs_to :continent
      #     # attribs: id, name, continent_id
      #   end
      #
      # Sample usage:
      #   option_groups_from_collection_for_select(@continents, :countries, :name, :id, :name, 3)
      #
      # Possible output:
      #   <optgroup label="Africa">
      #     <option value="1">Egypt</option>
      #     <option value="4">Rwanda</option>
      #     ...
      #   </optgroup>
      #   <optgroup label="Asia">
      #     <option value="3" selected="selected">China</option>
      #     <option value="12">India</option>
      #     <option value="5">Japan</option>
      #     ...
      #   </optgroup>
      #
      # <b>Note:</b> Only the <tt><optgroup></tt> and <tt><option></tt> tags are returned, so you still have to
      # wrap the output in an appropriate <tt><select></tt> tag.
      def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil)
A
Aaron Patterson 已提交
455
        collection.map do |group|
456 457 458
          option_tags = options_from_collection_for_select(
            group.send(group_method), option_key_method, option_value_method, selected_key)

459
          content_tag(:optgroup, option_tags, label: group.send(group_label_method))
A
Aaron Patterson 已提交
460
        end.join.html_safe
461 462
      end

463 464 465 466
      # Returns a string of <tt><option></tt> tags, like <tt>options_for_select</tt>, but
      # wraps them with <tt><optgroup></tt> tags.
      #
      # Parameters:
467
      # * +grouped_options+ - Accepts a nested array or hash of strings. The first value serves as the
468 469 470 471 472
      #   <tt><optgroup></tt> label while the second value must be an array of options. The second value can be a
      #   nested array of text-value pairs. See <tt>options_for_select</tt> for more info.
      #    Ex. ["North America",[["United States","US"],["Canada","CA"]]]
      # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags,
      #   which will have the +selected+ attribute set. Note: It is possible for this value to match multiple options
473
      #   as you might have the same option in multiple groups. Each will then get <tt>selected="selected"</tt>.
474 475 476
      #
      # Options:
      # * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this
477
      #   prepends an option with a generic prompt - "Please select" - or the given prompt string.
478
      # * <tt>:divider</tt> - the divider for the options groups.
479 480 481 482 483 484 485 486 487 488
      #
      #   grouped_options = [
      #    ['North America',
      #      [['United States','US'],'Canada']],
      #    ['Europe',
      #      ['Denmark','Germany','France']]
      #   ]
      #   grouped_options_for_select(grouped_options)
      #
      #   grouped_options = {
489 490
      #     'North America' => [['United States','US'], 'Canada'],
      #     'Europe' => ['Denmark','Germany','France']
491 492 493 494
      #   }
      #   grouped_options_for_select(grouped_options)
      #
      # Possible output:
495 496 497 498
      #   <optgroup label="North America">
      #     <option value="US">United States</option>
      #     <option value="Canada">Canada</option>
      #   </optgroup>
499 500 501 502 503 504
      #   <optgroup label="Europe">
      #     <option value="Denmark">Denmark</option>
      #     <option value="Germany">Germany</option>
      #     <option value="France">France</option>
      #   </optgroup>
      #
505
      #   grouped_options = [
506 507
      #     [['United States','US'], 'Canada'],
      #     ['Denmark','Germany','France']
508
      #   ]
509
      #   grouped_options_for_select(grouped_options, nil, divider: '---------')
510 511 512
      #
      # Possible output:
      #   <optgroup label="---------">
513 514 515 516
      #     <option value="US">United States</option>
      #     <option value="Canada">Canada</option>
      #   </optgroup>
      #   <optgroup label="---------">
517 518 519 520 521
      #     <option value="Denmark">Denmark</option>
      #     <option value="Germany">Germany</option>
      #     <option value="France">France</option>
      #   </optgroup>
      #
522 523
      # <b>Note:</b> Only the <tt><optgroup></tt> and <tt><option></tt> tags are returned, so you still have to
      # wrap the output in an appropriate <tt><select></tt> tag.
524
      def grouped_options_for_select(grouped_options, selected_key = nil, options = {})
525 526
        prompt  = options[:prompt]
        divider = options[:divider]
527

528
        body = "".html_safe
529 530

        if prompt
531
          body.safe_concat content_tag(:option, prompt_text(prompt), value: "")
532
        end
533

534
        grouped_options.each do |container|
535 536
          html_attributes = option_html_attributes(container)

537
          if divider
538
            label = divider
539 540 541
          else
            label, container = container
          end
542

543
          html_attributes = { label: label }.merge!(html_attributes)
544
          body.safe_concat content_tag(:optgroup, options_for_select(container, selected_key), html_attributes)
545 546
        end

547
        body
548 549
      end

550
      # Returns a string of option tags for pretty much any time zone in the
P
Pratik Naik 已提交
551 552 553 554 555 556
      # world. Supply a ActiveSupport::TimeZone name as +selected+ to have it
      # marked as the selected option tag. You can also supply an array of
      # ActiveSupport::TimeZone objects as +priority_zones+, so that they will
      # be listed above the rest of the (long) list. (You can use
      # ActiveSupport::TimeZone.us_zones as a convenience for obtaining a list
      # of the US time zones, or a Regexp to select the zones of your choice)
557
      #
558
      # The +selected+ parameter must be either +nil+, or a string that names
P
Pratik Naik 已提交
559
      # a ActiveSupport::TimeZone.
560
      #
P
Pratik Naik 已提交
561 562 563 564
      # By default, +model+ is the ActiveSupport::TimeZone constant (which can
      # be obtained in Active Record as a value object). The only requirement
      # is that the +model+ parameter be an object that responds to +all+, and
      # returns an array of objects that represent time zones.
565 566 567
      #
      # NOTE: Only the option tags are returned, you have to wrap this call in
      # a regular HTML select tag.
568
      def time_zone_options_for_select(selected = nil, priority_zones = nil, model = ::ActiveSupport::TimeZone)
569
        zone_options = "".html_safe
570

571
        zones = model.all
572 573
        convert_zones = lambda { |list| list.map { |z| [ z.to_s, z.name ] } }

574
        if priority_zones
575
          if priority_zones.is_a?(Regexp)
576
            priority_zones = zones.select { |z| z =~ priority_zones }
577
          end
578

579
          zone_options.safe_concat options_for_select(convert_zones[priority_zones], selected)
580
          zone_options.safe_concat content_tag(:option, '-------------', value: '', disabled: true)
581 582
          zone_options.safe_concat "\n"

583
          zones = zones - priority_zones
584 585
        end

586
        zone_options.safe_concat options_for_select(convert_zones[zones], selected)
587
      end
D
Initial  
David Heinemeier Hansson 已提交
588

589 590 591 592 593 594 595 596 597 598 599
      # Returns radio button tags for the collection of existing return values
      # of +method+ for +object+'s class. The value returned from calling
      # +method+ on the instance +object+ will be selected. If calling +method+
      # returns +nil+, no selection is made.
      #
      # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are
      # methods to be called on each member of +collection+. The return values
      # are used as the +value+ attribute and contents of each radio button tag,
      # respectively. They can also be any object that responds to +call+, such
      # as a +proc+, that will be called for each member of the +collection+ to
      # retrieve the value/text.
600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616
      #
      # Example object structure for use with this method:
      #   class Post < ActiveRecord::Base
      #     belongs_to :author
      #   end
      #   class Author < ActiveRecord::Base
      #     has_many :posts
      #     def name_with_initial
      #       "#{first_name.first}. #{last_name}"
      #     end
      #   end
      #
      # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>):
      #   collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial)
      #
      # If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return:
      #   <input id="post_author_id_1" name="post[author_id]" type="radio" value="1" checked="checked" />
617
      #   <label for="post_author_id_1">D. Heinemeier Hansson</label>
618
      #   <input id="post_author_id_2" name="post[author_id]" type="radio" value="2" />
619
      #   <label for="post_author_id_2">D. Thomas</label>
620
      #   <input id="post_author_id_3" name="post[author_id]" type="radio" value="3" />
621
      #   <label for="post_author_id_3">M. Clark</label>
622 623 624 625 626 627 628 629 630 631 632 633 634 635 636
      #
      # It is also possible to customize the way the elements will be shown by
      # giving a block to the method:
      #   collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
      #     b.label { b.radio_button }
      #   end
      #
      # The argument passed to the block is a special kind of builder for this
      # collection, which has the ability to generate the label and radio button
      # for the current item in the collection, with proper text and value.
      # Using it, you can change the label and radio button display order or
      # even use the label as wrapper, as in the example above.
      #
      # The builder methods <tt>label</tt> and <tt>radio_button</tt> also accept
      # extra html options:
637
      #   collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
A
AvnerCohen 已提交
638
      #     b.label(class: "radio_button") { b.radio_button(class: "radio_button") }
639
      #   end
640
      #
641 642 643
      # There are also three special methods available: <tt>object</tt>, <tt>text</tt> and
      # <tt>value</tt>, which are the current item being rendered, its text and value methods,
      # respectively. You can use them like this:
644
      #   collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
645 646
      #      b.label(:"data-value" => b.value) { b.radio_button + b.text }
      #   end
647 648 649 650
      def collection_radio_buttons(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block)
        Tags::CollectionRadioButtons.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block)
      end

651 652 653 654 655 656 657 658 659 660 661
      # Returns check box tags for the collection of existing return values of
      # +method+ for +object+'s class. The value returned from calling +method+
      # on the instance +object+ will be selected. If calling +method+ returns
      # +nil+, no selection is made.
      #
      # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are
      # methods to be called on each member of +collection+. The return values
      # are used as the +value+ attribute and contents of each check box tag,
      # respectively. They can also be any object that responds to +call+, such
      # as a +proc+, that will be called for each member of the +collection+ to
      # retrieve the value/text.
662 663 664
      #
      # Example object structure for use with this method:
      #   class Post < ActiveRecord::Base
665
      #     has_and_belongs_to_many :authors
666 667
      #   end
      #   class Author < ActiveRecord::Base
668
      #     has_and_belongs_to_many :posts
669 670 671 672 673 674 675 676 677 678
      #     def name_with_initial
      #       "#{first_name.first}. #{last_name}"
      #     end
      #   end
      #
      # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>):
      #   collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial)
      #
      # If <tt>@post.author_ids</tt> is already <tt>[1]</tt>, this would return:
      #   <input id="post_author_ids_1" name="post[author_ids][]" type="checkbox" value="1" checked="checked" />
679
      #   <label for="post_author_ids_1">D. Heinemeier Hansson</label>
680
      #   <input id="post_author_ids_2" name="post[author_ids][]" type="checkbox" value="2" />
681
      #   <label for="post_author_ids_2">D. Thomas</label>
682
      #   <input id="post_author_ids_3" name="post[author_ids][]" type="checkbox" value="3" />
683
      #   <label for="post_author_ids_3">M. Clark</label>
684
      #   <input name="post[author_ids][]" type="hidden" value="" />
685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700
      #
      # It is also possible to customize the way the elements will be shown by
      # giving a block to the method:
      #   collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b|
      #     b.label { b.check_box }
      #   end
      #
      # The argument passed to the block is a special kind of builder for this
      # collection, which has the ability to generate the label and check box
      # for the current item in the collection, with proper text and value.
      # Using it, you can change the label and check box display order or even
      # use the label as wrapper, as in the example above.
      #
      # The builder methods <tt>label</tt> and <tt>check_box</tt> also accept
      # extra html options:
      #   collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b|
A
AvnerCohen 已提交
701
      #     b.label(class: "check_box") { b.check_box(class: "check_box") }
702
      #   end
703
      #
704 705 706
      # There are also three special methods available: <tt>object</tt>, <tt>text</tt> and
      # <tt>value</tt>, which are the current item being rendered, its text and value methods,
      # respectively. You can use them like this:
707 708 709
      #   collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b|
      #      b.label(:"data-value" => b.value) { b.check_box + b.text }
      #   end
710 711 712 713
      def collection_check_boxes(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block)
        Tags::CollectionCheckBoxes.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block)
      end

D
Initial  
David Heinemeier Hansson 已提交
714
      private
715
        def option_html_attributes(element)
716
          if Array === element
717
            element.select { |e| Hash === e }.reduce({}, :merge!)
718 719 720
          else
            {}
          end
721 722
        end

723 724
        def option_text_and_value(option)
          # Options are [text, value] pairs or strings used for both.
725 726
          if !option.is_a?(String) && option.respond_to?(:first) && option.respond_to?(:last)
            option = option.reject { |e| Hash === e } if Array === option
727 728 729 730 731 732 733
            [option.first, option.last]
          else
            [option, option]
          end
        end

        def option_value_selected?(value, selected)
734
          Array(selected).include? value
735
        end
736 737

        def extract_selected_and_disabled(selected)
738
          if selected.is_a?(Proc)
739
            [selected, nil]
740
          else
741
            selected = Array.wrap(selected)
742
            options = selected.extract_options!.symbolize_keys
743 744
            selected_items = options.fetch(:selected, selected)
            [selected_items, options[:disabled]]
745 746 747 748 749 750 751 752 753 754 755 756
          end
        end

        def extract_values_from_collection(collection, value_method, selected)
          if selected.is_a?(Proc)
            collection.map do |element|
              element.send(value_method) if selected.call(element)
            end.compact
          else
            selected
          end
        end
757 758 759 760

        def value_for_collection(item, value)
          value.respond_to?(:call) ? value.call(item) : item.send(value)
        end
761 762

        def prompt_text(prompt)
763
          prompt.kind_of?(String) ? prompt : I18n.translate('helpers.select.prompt', default: 'Please select')
764
        end
D
Initial  
David Heinemeier Hansson 已提交
765 766
    end

767
    class FormBuilder
768
      # Wraps ActionView::Helpers::FormOptionsHelper#select for form builders:
769
      #
770
      #   <%= form_for @post do |f| %>
771
      #     <%= f.select :person_id, Person.all.collect { |p| [ p.name, p.id ] }, include_blank: true %>
772 773
      #     <%= f.submit %>
      #   <% end %>
774
      #
775
      # Please refer to the documentation of the base helper for details.
776 777
      def select(method, choices = nil, options = {}, html_options = {}, &block)
        @template.select(@object_name, method, choices, objectify_options(options), @default_options.merge(html_options), &block)
778 779
      end

780
      # Wraps ActionView::Helpers::FormOptionsHelper#collection_select for form builders:
781
      #
782 783 784 785
      #   <%= form_for @post do |f| %>
      #     <%= f.collection_select :person_id, Author.all, :id, :name_with_initial, prompt: true %>
      #     <%= f.submit %>
      #   <% end %>
786
      #
787
      # Please refer to the documentation of the base helper for details.
788
      def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
789
        @template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options))
790 791
      end

792
      # Wraps ActionView::Helpers::FormOptionsHelper#grouped_collection_select for form builders:
793
      #
794
      #   <%= form_for @city do |f| %>
795
      #     <%= f.grouped_collection_select :country_id, @continents, :countries, :name, :id, :name %>
796 797
      #     <%= f.submit %>
      #   <% end %>
798
      #
799
      # Please refer to the documentation of the base helper for details.
800 801 802 803
      def grouped_collection_select(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
        @template.grouped_collection_select(@object_name, method, collection, group_method, group_label_method, option_key_method, option_value_method, objectify_options(options), @default_options.merge(html_options))
      end

804
      # Wraps ActionView::Helpers::FormOptionsHelper#time_zone_select for form builders:
805
      #
806 807 808 809
      #   <%= form_for @user do |f| %>
      #     <%= f.time_zone_select :time_zone, nil, include_blank: true %>
      #     <%= f.submit %>
      #   <% end %>
810
      #
811
      # Please refer to the documentation of the base helper for details.
812
      def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
813
        @template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_options.merge(html_options))
814
      end
815

816
      # Wraps ActionView::Helpers::FormOptionsHelper#collection_check_boxes for form builders:
817
      #
818 819 820 821
      #   <%= form_for @post do |f| %>
      #     <%= f.collection_check_boxes :author_ids, Author.all, :id, :name_with_initial %>
      #     <%= f.submit %>
      #   <% end %>
822
      #
823
      # Please refer to the documentation of the base helper for details.
824 825
      def collection_check_boxes(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
        @template.collection_check_boxes(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options), &block)
826 827
      end

828
      # Wraps ActionView::Helpers::FormOptionsHelper#collection_radio_buttons for form builders:
829
      #
830 831 832 833
      #   <%= form_for @post do |f| %>
      #     <%= f.collection_radio_buttons :author_id, Author.all, :id, :name_with_initial %>
      #     <%= f.submit %>
      #   <% end %>
834
      #
835
      # Please refer to the documentation of the base helper for details.
836 837
      def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
        @template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options), &block)
838
      end
839
    end
D
Initial  
David Heinemeier Hansson 已提交
840
  end
841
end