require 'cgi'
require 'erb'
require 'action_view/helpers/form_helper'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/string/output_safety'
module ActionView
# = Action View Form Option Helpers
module Helpers
# Provides a number of methods for turning different kinds of containers into a set of option tags.
# == Options
# The collection_select, select and time_zone_select methods take an options parameter, a hash:
#
# * :include_blank - 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.
#
# For example,
#
# select("post", "category", Post::CATEGORIES, {:include_blank => true})
#
# could become:
#
#
#
# Another common case is a select tag for an belongs_to-associated object.
#
# Example with @post.person_id => 2:
#
# select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {:include_blank => 'None'})
#
# could become:
#
#
#
# * :prompt - 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.
#
# Example:
#
# select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {:prompt => 'Select Person'})
#
# could become:
#
#
#
# Like the other form helpers, +select+ can accept an :index option to manually set the ID used in the resulting output. Unlike other helpers, +select+ expects this
# option to be in the +html_options+ parameter.
#
# Example:
#
# select("album[]", "genre", %w[rap rock country], {}, { :index => nil })
#
# becomes:
#
#
#
# * :disabled - can be a single value or an array of values that will be disabled options in the final output.
#
# Example:
#
# select("post", "category", Post::CATEGORIES, {:disabled => 'restricted'})
#
# could become:
#
#
#
# When used with the collection_select helper, :disabled can also be a Proc that identifies those options that should be disabled.
#
# Example:
#
# collection_select(:post, :category_id, Category.all, :id, :name, {:disabled => lambda{|category| category.archived? }})
#
# If the categories "2008 stuff" and "Christmas" return true when the method archived? is called, this would return:
#
#
module FormOptionsHelper
# ERB::Util can mask some helpers like textilize. Make sure to include them.
include TextHelper
# Create a select tag and a series of contained option tags for the provided object and method.
# The option currently held by the object will be selected, provided that the object is available.
# See options_for_select for the required format of the choices parameter.
#
# Example with @post.person_id => 1:
# select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, { :include_blank => true })
#
# could become:
#
#
#
# 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.
#
# By default, post.person_id is the selected option. Specify :selected => value to use a different selection
# or :selected => nil to leave all options unselected. Similarly, you can specify values to be disabled in the option
# tags by specifying the :disabled option. This can either be a single value or an array of values to be disabled.
#
# ==== Gotcha
#
# The HTML specification says when +multiple+ parameter passed to select and all options got deselected
# 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
#
# @user.update_attributes(params[:user])
#
# 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.
#
# This way, the client either sends only the hidden field (representing
# the deselected multiple select box), or both fields. Since the HTML specification
# says key/value pairs have to be sent in the same order they appear in the
# form, and parameters extraction gets the last occurrence of any repeated
# key in the query string, that works for ordinary forms.
#
def select(object, method, choices, options = {}, html_options = {})
InstanceTag.new(object, method, self, options.delete(:object)).to_select_tag(choices, options, html_options)
end
# Returns and 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 :prompt
# or :include_blank in the +options+ hash.
#
# The :value_method and :text_method parameters are methods to be called on each member
# of +collection+. The return values are used as the +value+ attribute and contents of each
# tag, respectively.
#
# 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, @post):
# collection_select(:post, :author_id, Author.all, :id, :name_with_initial, :prompt => true)
#
# If @post.author_id is already 1, this would return:
#
def collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {})
InstanceTag.new(object, method, self, options.delete(:object)).to_collection_select_tag(collection, value_method, text_method, options, html_options)
end
# Returns , and 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 :prompt
# or :include_blank 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 tags.
# * +group_method+ - The name of a method which, when called on a member of +collection+, returns an
# array of child objects representing the 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 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 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 tag.
#
# Example object structure for use with this method:
# class Continent < ActiveRecord::Base
# has_many :countries
# # attribs: id, name
# end
# class Country < ActiveRecord::Base
# belongs_to :continent
# # attribs: id, name, continent_id
# end
# class City < ActiveRecord::Base
# belongs_to :country
# # attribs: id, name, country_id
# end
#
# Sample usage:
# grouped_collection_select(:city, :country_id, @continents, :countries, :name, :id, :name)
#
# Possible output:
#
#
def grouped_collection_select(object, method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
InstanceTag.new(object, method, self, options.delete(:object)).to_grouped_collection_select_tag(collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options)
end
# Return select and option tags for the given object and method, using
# #time_zone_options_for_select to generate the list of option tags.
#
# In addition to the :include_blank option documented above,
# this method also supports a :model option, which defaults
# 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.)
#
# 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)
#
# Finally, this method supports a :default option, which selects
# a default ActiveSupport::TimeZone if the object's time zone is +nil+.
#
# Examples:
# time_zone_select( "user", "time_zone", nil, :include_blank => true)
#
# time_zone_select( "user", "time_zone", nil, :default => "Pacific Time (US & Canada)" )
#
# time_zone_select( "user", 'time_zone', ActiveSupport::TimeZone.us_zones, :default => "Pacific Time (US & Canada)")
#
# time_zone_select( "user", 'time_zone', [ ActiveSupport::TimeZone['Alaska'], ActiveSupport::TimeZone['Hawaii'] ])
#
# time_zone_select( "user", 'time_zone', /Australia/)
#
# time_zone_select( "user", "time_zone", ActiveSupport::TimeZone.all.sort, :model => ActiveSupport::TimeZone)
def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {})
InstanceTag.new(object, method, self, options.delete(:object)).to_time_zone_select_tag(priority_zones, options, html_options)
end
# Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container
# 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
# become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. +selected+
# may also be an array of values to be selected when using a multiple select.
#
# Examples (call, result):
# options_for_select([["Dollar", "$"], ["Kroner", "DKK"]])
# \n
#
# options_for_select([ "VISA", "MasterCard" ], "MasterCard")
# \n
#
# options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40")
# \n
#
# options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"])
# \n\n
#
# You can optionally provide html attributes as the last element of the array.
#
# Examples:
# options_for_select([ "Denmark", ["USA", {:class => 'bold'}], "Sweden" ], ["USA", "Sweden"])
# \n\n
#
# options_for_select([["Dollar", "$", {:class => "bold"}], ["Kroner", "DKK", {:onclick => "alert('HI');"}]])
# \n
#
# If you wish to specify disabled option tags, set +selected+ to be a hash, with :disabled being either a value
# or array of values to be disabled. In this case, you can use :selected to specify selected option tags.
#
# Examples:
# options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :disabled => "Super Platinum")
# \n\n\n
#
# options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :disabled => ["Advanced", "Super Platinum"])
# \n\n\n
#
# options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :selected => "Free", :disabled => "Super Platinum")
# \n\n\n
#
# NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
def options_for_select(container, selected = nil)
return container if String === container
selected, disabled = extract_selected_and_disabled(selected).map do | r |
Array.wrap(r).map(&:to_s)
end
container.map do |element|
html_attributes = option_html_attributes(element)
text, value = option_text_and_value(element).map(&:to_s)
selected_attribute = ' selected="selected"' if option_value_selected?(value, selected)
disabled_attribute = ' disabled="disabled"' if disabled && option_value_selected?(value, disabled)
%()
end.join("\n").html_safe
end
# Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning the
# the result of a call to the +value_method+ as the option value and the +text_method+ as the option text.
# Example:
# options_from_collection_for_select(@people, 'id', 'name')
# This will output the same HTML as if you did this:
#
#
# This is more often than not used inside a #select_tag like this example:
# select_tag 'person', options_from_collection_for_select(@people, 'id', 'name')
#
# 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 :selected and/or :disabled values as required.
#
# Be sure to specify the same class as the +value_method+ when specifying selected or disabled options.
# 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.
def options_from_collection_for_select(collection, value_method, text_method, selected = nil)
options = collection.map do |element|
[element.send(text_method), element.send(value_method)]
end
selected, disabled = extract_selected_and_disabled(selected)
select_deselect = {}
select_deselect[:selected] = extract_values_from_collection(collection, value_method, selected)
select_deselect[:disabled] = extract_values_from_collection(collection, value_method, disabled)
options_for_select(options, select_deselect)
end
# Returns a string of tags, like options_from_collection_for_select, but
# groups them by tags based on the object relationships of the arguments.
#
# Parameters:
# * +collection+ - An array of objects representing the tags.
# * +group_method+ - The name of a method which, when called on a member of +collection+, returns an
# array of child objects representing the 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 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 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 tag.
# * +selected_key+ - A value equal to the +value+ attribute for one of the tags,
# which will have the +selected+ attribute set. Corresponds to the return value of one of the calls
# to +option_key_method+. If +nil+, no selection is made. Can also be a hash if disabled values are
# to be specified.
#
# Example object structure for use with this method:
# class Continent < ActiveRecord::Base
# has_many :countries
# # attribs: id, name
# end
# 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:
#
#
#
# Note: Only the and tags are returned, so you still have to
# wrap the output in an appropriate tag.
def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil)
collection.map do |group|
group_label_string = eval("group.#{group_label_method}")
"'
end.join.html_safe
end
# Returns a string of tags, like options_for_select, but
# wraps them with tags.
#
# Parameters:
# * +grouped_options+ - Accepts a nested array or hash of strings. The first value serves as the
# label while the second value must be an array of options. The second value can be a
# nested array of text-value pairs. See options_for_select for more info.
# Ex. ["North America",[["United States","US"],["Canada","CA"]]]
# * +selected_key+ - A value equal to the +value+ attribute for one of the tags,
# which will have the +selected+ attribute set. Note: It is possible for this value to match multiple options
# as you might have the same option in multiple groups. Each will then get selected="selected".
# * +prompt+ - 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.
#
# Sample usage (Array):
# grouped_options = [
# ['North America',
# [['United States','US'],'Canada']],
# ['Europe',
# ['Denmark','Germany','France']]
# ]
# grouped_options_for_select(grouped_options)
#
# Sample usage (Hash):
# grouped_options = {
# 'North America' => [['United States','US'], 'Canada'],
# 'Europe' => ['Denmark','Germany','France']
# }
# grouped_options_for_select(grouped_options)
#
# Possible output:
#
#
#
# Note: Only the and tags are returned, so you still have to
# wrap the output in an appropriate tag.
def grouped_options_for_select(grouped_options, selected_key = nil, prompt = nil)
body = ''
body << content_tag(:option, prompt, { :value => "" }, true) if prompt
grouped_options = grouped_options.sort if grouped_options.is_a?(Hash)
grouped_options.each do |group|
body << content_tag(:optgroup, options_for_select(group[1], selected_key), :label => group[0])
end
body.html_safe
end
# Returns a string of option tags for pretty much any time zone in the
# 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)
#
# The +selected+ parameter must be either +nil+, or a string that names
# a ActiveSupport::TimeZone.
#
# 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.
#
# NOTE: Only the option tags are returned, you have to wrap this call in
# a regular HTML select tag.
def time_zone_options_for_select(selected = nil, priority_zones = nil, model = ::ActiveSupport::TimeZone)
zone_options = ""
zones = model.all
convert_zones = lambda { |list| list.map { |z| [ z.to_s, z.name ] } }
if priority_zones
if priority_zones.is_a?(Regexp)
priority_zones = model.all.find_all {|z| z =~ priority_zones}
end
zone_options += options_for_select(convert_zones[priority_zones], selected)
zone_options += "\n"
zones = zones.reject { |z| priority_zones.include?( z ) }
end
zone_options += options_for_select(convert_zones[zones], selected)
zone_options.html_safe
end
private
def option_html_attributes(element)
return "" unless Array === element
html_attributes = []
element.select { |e| Hash === e }.reduce({}, :merge).each do |k, v|
html_attributes << " #{k}=\"#{ERB::Util.html_escape(v.to_s)}\""
end
html_attributes.join
end
def option_text_and_value(option)
# Options are [text, value] pairs or strings used for both.
case
when Array === option
option = option.reject { |e| Hash === e }
[option.first, option.last]
when !option.is_a?(String) && option.respond_to?(:first) && option.respond_to?(:last)
[option.first, option.last]
else
[option, option]
end
end
def option_value_selected?(value, selected)
if selected.respond_to?(:include?) && !selected.is_a?(String)
selected.include? value
else
value == selected
end
end
def extract_selected_and_disabled(selected)
if selected.is_a?(Proc)
[ selected, nil ]
else
selected = Array.wrap(selected)
options = selected.extract_options!.symbolize_keys
[ options.include?(:selected) ? options[:selected] : selected, options[:disabled] ]
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
end
class InstanceTag #:nodoc:
include FormOptionsHelper
def to_select_tag(choices, options, html_options)
selected_value = options.has_key?(:selected) ? options[:selected] : value(object)
select_content_tag(options_for_select(choices, :selected => selected_value, :disabled => options[:disabled]), options, html_options)
end
def to_collection_select_tag(collection, value_method, text_method, options, html_options)
selected_value = options.has_key?(:selected) ? options[:selected] : value(object)
select_content_tag(
options_from_collection_for_select(collection, value_method, text_method, :selected => selected_value, :disabled => options[:disabled]), options, html_options
)
end
def to_grouped_collection_select_tag(collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options)
select_content_tag(
option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, value(object)), options, html_options
)
end
def to_time_zone_select_tag(priority_zones, options, html_options)
select_content_tag(
time_zone_options_for_select(value(object) || options[:default], priority_zones, options[:model] || ActiveSupport::TimeZone), options, html_options
)
end
private
def add_options(option_tags, options, value = nil)
if options[:include_blank]
option_tags = "\n" + option_tags
end
if value.blank? && options[:prompt]
prompt = options[:prompt].kind_of?(String) ? options[:prompt] : I18n.translate('helpers.select.prompt', :default => 'Please select')
option_tags = "\n" + option_tags
end
option_tags.html_safe
end
def select_content_tag(option_tags, options, html_options)
html_options = html_options.stringify_keys
add_default_name_and_id(html_options)
select = content_tag("select", add_options(option_tags, options, value(object)), html_options)
if html_options["multiple"]
tag("input", :disabled => html_options["disabled"], :name => html_options["name"], :type => "hidden", :value => "") + select
else
select
end
end
end
class FormBuilder
def select(method, choices, options = {}, html_options = {})
@template.select(@object_name, method, choices, objectify_options(options), @default_options.merge(html_options))
end
def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
@template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options))
end
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
def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
@template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_options.merge(html_options))
end
end
end
end