Merge pull request #4488 from rafaelfranca/av-refactor

ActionView::Helpers::FormHelper refactoring
This commit is contained in:
Xavier Noria 2012-01-17 14:08:09 -08:00
commit 57aaaa6197
29 changed files with 688 additions and 436 deletions

@ -16,9 +16,7 @@ def object
end
end
%w(content_tag to_date_select_tag to_datetime_select_tag to_time_select_tag).each do |meth|
module_eval "def #{meth}(*) error_wrapping(super) end", __FILE__, __LINE__
end
module_eval "def content_tag(*) error_wrapping(super) end", __FILE__, __LINE__
def tag(type, options, *)
tag_generate_errors?(options) ? error_wrapping(super) : super

@ -213,7 +213,7 @@ def time_ago_in_words(from_time, include_seconds = false)
# Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that
# all month choices are valid.
def date_select(object_name, method, options = {}, html_options = {})
InstanceTag.new(object_name, method, self, options.delete(:object)).to_date_select_tag(options, html_options)
Tags::DateSelect.new(object_name, method, self, options, html_options).render
end
# Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a
@ -251,7 +251,7 @@ def date_select(object_name, method, options = {}, html_options = {})
# Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that
# all month choices are valid.
def time_select(object_name, method, options = {}, html_options = {})
InstanceTag.new(object_name, method, self, options.delete(:object)).to_time_select_tag(options, html_options)
Tags::TimeSelect.new(object_name, method, self, options, html_options).render
end
# Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a
@ -287,7 +287,7 @@ def time_select(object_name, method, options = {}, html_options = {})
#
# The selects are prepared for multi-parameter assignment to an Active Record object.
def datetime_select(object_name, method, options = {}, html_options = {})
InstanceTag.new(object_name, method, self, options.delete(:object)).to_datetime_select_tag(options, html_options)
Tags::DatetimeSelect.new(object_name, method, self, options, html_options).render
end
# Returns a set of html select-tags (one for year, month, day, hour, minute, and second) pre-selected with the
@ -974,66 +974,6 @@ def separator(type)
end
end
module DateHelperInstanceTag
def to_date_select_tag(options = {}, html_options = {})
datetime_selector(options, html_options).select_date.html_safe
end
def to_time_select_tag(options = {}, html_options = {})
datetime_selector(options, html_options).select_time.html_safe
end
def to_datetime_select_tag(options = {}, html_options = {})
datetime_selector(options, html_options).select_datetime.html_safe
end
private
def datetime_selector(options, html_options)
datetime = value(object) || default_datetime(options)
@auto_index ||= nil
options = options.dup
options[:field_name] = @method_name
options[:include_position] = true
options[:prefix] ||= @object_name
options[:index] = @auto_index if @auto_index && !options.has_key?(:index)
DateTimeSelector.new(datetime, options, html_options)
end
def default_datetime(options)
return if options[:include_blank] || options[:prompt]
case options[:default]
when nil
Time.current
when Date, Time
options[:default]
else
default = options[:default].dup
# Rename :minute and :second to :min and :sec
default[:min] ||= default[:minute]
default[:sec] ||= default[:second]
time = Time.current
[:year, :month, :day, :hour, :min, :sec].each do |key|
default[key] ||= time.send(key)
end
Time.utc_time(
default[:year], default[:month], default[:day],
default[:hour], default[:min], default[:sec]
)
end
end
end
class InstanceTag #:nodoc:
include DateHelperInstanceTag
end
class FormBuilder
def date_select(method, options = {}, html_options = {})
@template.date_select(@object_name, method, objectify_options(options), html_options)

@ -3,6 +3,7 @@
require 'action_view/helpers/tag_helper'
require 'action_view/helpers/form_tag_helper'
require 'action_view/helpers/active_model_helper'
require 'action_view/helpers/tags'
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/object/blank'
@ -654,16 +655,7 @@ def fields_for(record_name, record_object = nil, options = {}, &block)
# 'Accept <a href="/terms">Terms</a>.'.html_safe
# end
def label(object_name, method, content_or_options = nil, options = nil, &block)
content_is_options = content_or_options.is_a?(Hash)
if content_is_options || block_given?
options = content_or_options if content_is_options
text = nil
else
text = content_or_options
end
options ||= {}
InstanceTag.new(object_name, method, self, options.delete(:object)).to_label_tag(text, options, &block)
Tags::Label.new(object_name, method, self, content_or_options, options).render(&block)
end
# Returns an input tag of the "text" type tailored for accessing a specified attribute (identified by +method+) on an object
@ -685,7 +677,7 @@ def label(object_name, method, content_or_options = nil, options = nil, &block)
# # => <input type="text" id="snippet_code" name="snippet[code]" size="20" value="#{@snippet.code}" class="code_input" />
#
def text_field(object_name, method, options = {})
InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("text", options)
Tags::TextField.new(object_name, method, self, options).render
end
# Returns an input tag of the "password" type tailored for accessing a specified attribute (identified by +method+) on an object
@ -707,7 +699,7 @@ def text_field(object_name, method, options = {})
# # => <input type="password" id="account_pin" name="account[pin]" size="20" class="form_input" />
#
def password_field(object_name, method, options = {})
InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("password", { :value => nil }.merge!(options))
Tags::PasswordField.new(object_name, method, self, options).render
end
# Returns a hidden input tag tailored for accessing a specified attribute (identified by +method+) on an object
@ -725,7 +717,7 @@ def password_field(object_name, method, options = {})
# hidden_field(:user, :token)
# # => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" />
def hidden_field(object_name, method, options = {})
InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("hidden", options)
Tags::HiddenField.new(object_name, method, self, options).render
end
# Returns a file upload input tag tailored for accessing a specified attribute (identified by +method+) on an object
@ -746,7 +738,7 @@ def hidden_field(object_name, method, options = {})
# # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" />
#
def file_field(object_name, method, options = {})
InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("file", options.update({:size => nil}))
Tags::FileField.new(object_name, method, self, options).render
end
# Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+)
@ -774,7 +766,7 @@ def file_field(object_name, method, options = {})
# # #{@entry.body}
# # </textarea>
def text_area(object_name, method, options = {})
InstanceTag.new(object_name, method, self, options.delete(:object)).to_text_area_tag(options)
Tags::TextArea.new(object_name, method, self, options).render
end
# Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object
@ -836,7 +828,7 @@ def text_area(object_name, method, options = {})
# # <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" />
#
def check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0")
InstanceTag.new(object_name, method, self, options.delete(:object)).to_check_box_tag(options, checked_value, unchecked_value)
Tags::CheckBox.new(object_name, method, self, checked_value, unchecked_value, options).render
end
# Returns a radio button tag for accessing a specified attribute (identified by +method+) on an object
@ -858,7 +850,7 @@ def check_box(object_name, method, options = {}, checked_value = "1", unchecked_
# # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" />
# # <input type="radio" id="user_receive_newsletter_no" name="user[receive_newsletter]" value="no" checked="checked" />
def radio_button(object_name, method, tag_value, options = {})
InstanceTag.new(object_name, method, self, options.delete(:object)).to_radio_button_tag(tag_value, options)
Tags::RadioButton.new(object_name, method, self, tag_value, options).render
end
# Returns an input of type "search" for accessing a specified attribute (identified by +method+) on an object
@ -884,20 +876,7 @@ def radio_button(object_name, method, tag_value, options = {})
# # => <input autosave="com.example.www" id="user_name" incremental="true" name="user[name]" onsearch="true" results="10" size="30" type="search" />
#
def search_field(object_name, method, options = {})
options = options.stringify_keys
if options["autosave"]
if options["autosave"] == true
options["autosave"] = request.host.split(".").reverse.join(".")
end
options["results"] ||= 10
end
if options["onsearch"]
options["incremental"] = true unless options.has_key?("incremental")
end
InstanceTag.new(object_name, method, self, options.delete("object")).to_input_field_tag("search", options)
Tags::SearchField.new(object_name, method, self, options).render
end
# Returns a text_field of type "tel".
@ -906,7 +885,7 @@ def search_field(object_name, method, options = {})
# # => <input id="user_phone" name="user[phone]" size="30" type="tel" />
#
def telephone_field(object_name, method, options = {})
InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("tel", options)
Tags::TelField.new(object_name, method, self, options).render
end
alias phone_field telephone_field
@ -916,7 +895,7 @@ def telephone_field(object_name, method, options = {})
# # => <input id="user_homepage" size="30" name="user[homepage]" type="url" />
#
def url_field(object_name, method, options = {})
InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("url", options)
Tags::UrlField.new(object_name, method, self, options).render
end
# Returns a text_field of type "email".
@ -925,7 +904,7 @@ def url_field(object_name, method, options = {})
# # => <input id="user_address" size="30" name="user[address]" type="email" />
#
def email_field(object_name, method, options = {})
InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("email", options)
Tags::EmailField.new(object_name, method, self, options).render
end
# Returns an input tag of type "number".
@ -933,7 +912,7 @@ def email_field(object_name, method, options = {})
# ==== Options
# * Accepts same options as number_field_tag
def number_field(object_name, method, options = {})
InstanceTag.new(object_name, method, self, options.delete(:object)).to_number_field_tag("number", options)
Tags::NumberField.new(object_name, method, self, options).render
end
# Returns an input tag of type "range".
@ -941,7 +920,7 @@ def number_field(object_name, method, options = {})
# ==== Options
# * Accepts same options as range_field_tag
def range_field(object_name, method, options = {})
InstanceTag.new(object_name, method, self, options.delete(:object)).to_number_field_tag("range", options)
Tags::RangeField.new(object_name, method, self, options).render
end
private
@ -961,272 +940,6 @@ def instantiate_builder(record_name, record_object, options, &block)
end
end
class InstanceTag
include Helpers::ActiveModelInstanceTag, Helpers::TagHelper, Helpers::FormTagHelper
attr_reader :object, :method_name, :object_name
DEFAULT_FIELD_OPTIONS = { "size" => 30 }
DEFAULT_RADIO_OPTIONS = { }
DEFAULT_TEXT_AREA_OPTIONS = { "cols" => 40, "rows" => 20 }
def initialize(object_name, method_name, template_object, object = nil)
@object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
@template_object = template_object
@object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]")
@object = retrieve_object(object)
@auto_index = retrieve_autoindex(Regexp.last_match.pre_match) if Regexp.last_match
end
def to_label_tag(text = nil, options = {}, &block)
options = options.stringify_keys
tag_value = options.delete("value")
name_and_id = options.dup
if name_and_id["for"]
name_and_id["id"] = name_and_id["for"]
else
name_and_id.delete("id")
end
add_default_name_and_id_for_value(tag_value, name_and_id)
options.delete("index")
options.delete("namespace")
options["for"] ||= name_and_id["id"]
if block_given?
@template_object.label_tag(name_and_id["id"], options, &block)
else
content = if text.blank?
object_name.gsub!(/\[(.*)_attributes\]\[\d\]/, '.\1')
method_and_value = tag_value.present? ? "#{method_name}.#{tag_value}" : method_name
if object.respond_to?(:to_model)
key = object.class.model_name.i18n_key
i18n_default = ["#{key}.#{method_and_value}".to_sym, ""]
end
i18n_default ||= ""
I18n.t("#{object_name}.#{method_and_value}", :default => i18n_default, :scope => "helpers.label").presence
else
text.to_s
end
content ||= if object && object.class.respond_to?(:human_attribute_name)
object.class.human_attribute_name(method_name)
end
content ||= method_name.humanize
label_tag(name_and_id["id"], content, options)
end
end
def to_input_field_tag(field_type, options = {})
options = options.stringify_keys
options["size"] = options["maxlength"] || DEFAULT_FIELD_OPTIONS["size"] unless options.key?("size")
options = DEFAULT_FIELD_OPTIONS.merge(options)
if field_type == "hidden"
options.delete("size")
end
options["type"] ||= field_type
options["value"] = options.fetch("value"){ value_before_type_cast(object) } unless field_type == "file"
options["value"] &&= ERB::Util.html_escape(options["value"])
add_default_name_and_id(options)
tag("input", options)
end
def to_number_field_tag(field_type, options = {})
options = options.stringify_keys
options['size'] ||= nil
if range = options.delete("in") || options.delete("within")
options.update("min" => range.min, "max" => range.max)
end
to_input_field_tag(field_type, options)
end
def to_radio_button_tag(tag_value, options = {})
options = DEFAULT_RADIO_OPTIONS.merge(options.stringify_keys)
options["type"] = "radio"
options["value"] = tag_value
if options.has_key?("checked")
cv = options.delete "checked"
checked = cv == true || cv == "checked"
else
checked = self.class.radio_button_checked?(value(object), tag_value)
end
options["checked"] = "checked" if checked
add_default_name_and_id_for_value(tag_value, options)
tag("input", options)
end
def to_text_area_tag(options = {})
options = DEFAULT_TEXT_AREA_OPTIONS.merge(options.stringify_keys)
add_default_name_and_id(options)
if size = options.delete("size")
options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
end
content_tag("textarea", ERB::Util.html_escape(options.delete('value') || value_before_type_cast(object)), options)
end
def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
options = options.stringify_keys
options["type"] = "checkbox"
options["value"] = checked_value
if options.has_key?("checked")
cv = options.delete "checked"
checked = cv == true || cv == "checked"
else
checked = self.class.check_box_checked?(value(object), checked_value)
end
options["checked"] = "checked" if checked
if options["multiple"]
add_default_name_and_id_for_value(checked_value, options)
options.delete("multiple")
else
add_default_name_and_id(options)
end
hidden = unchecked_value ? tag("input", "name" => options["name"], "type" => "hidden", "value" => unchecked_value, "disabled" => options["disabled"]) : ""
checkbox = tag("input", options)
hidden + checkbox
end
def to_boolean_select_tag(options = {})
options = options.stringify_keys
add_default_name_and_id(options)
value = value(object)
tag_text = "<select"
tag_text << tag_options(options)
tag_text << "><option value=\"false\""
tag_text << " selected" if value == false
tag_text << ">False</option><option value=\"true\""
tag_text << " selected" if value
tag_text << ">True</option></select>"
end
def to_content_tag(tag_name, options = {})
content_tag(tag_name, value(object), options)
end
def retrieve_object(object)
if object
object
elsif @template_object.instance_variable_defined?("@#{@object_name}")
@template_object.instance_variable_get("@#{@object_name}")
end
rescue NameError
# As @object_name may contain the nested syntax (item[subobject]) we need to fallback to nil.
nil
end
def retrieve_autoindex(pre_match)
object = self.object || @template_object.instance_variable_get("@#{pre_match}")
if object && object.respond_to?(:to_param)
object.to_param
else
raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
end
end
def value(object)
self.class.value(object, @method_name)
end
def value_before_type_cast(object)
self.class.value_before_type_cast(object, @method_name)
end
class << self
def value(object, method_name)
object.send method_name if object
end
def value_before_type_cast(object, method_name)
unless object.nil?
object.respond_to?(method_name + "_before_type_cast") ?
object.send(method_name + "_before_type_cast") :
object.send(method_name)
end
end
def check_box_checked?(value, checked_value)
case value
when TrueClass, FalseClass
value
when NilClass
false
when Integer
value != 0
when String
value == checked_value
when Array
value.include?(checked_value)
else
value.to_i != 0
end
end
def radio_button_checked?(value, checked_value)
value.to_s == checked_value.to_s
end
end
private
def add_default_name_and_id_for_value(tag_value, options)
unless tag_value.nil?
pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase
specified_id = options["id"]
add_default_name_and_id(options)
options["id"] += "_#{pretty_tag_value}" if specified_id.blank? && options["id"].present?
else
add_default_name_and_id(options)
end
end
def add_default_name_and_id(options)
if options.has_key?("index")
options["name"] ||= tag_name_with_index(options["index"])
options["id"] = options.fetch("id"){ tag_id_with_index(options["index"]) }
options.delete("index")
elsif defined?(@auto_index)
options["name"] ||= tag_name_with_index(@auto_index)
options["id"] = options.fetch("id"){ tag_id_with_index(@auto_index) }
else
options["name"] ||= tag_name + (options['multiple'] ? '[]' : '')
options["id"] = options.fetch("id"){ tag_id }
end
options["id"] = [options.delete('namespace'), options["id"]].compact.join("_").presence
end
def tag_name
"#{@object_name}[#{sanitized_method_name}]"
end
def tag_name_with_index(index)
"#{@object_name}[#{index}][#{sanitized_method_name}]"
end
def tag_id
"#{sanitized_object_name}_#{sanitized_method_name}"
end
def tag_id_with_index(index)
"#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
end
def sanitized_object_name
@sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
end
def sanitized_method_name
@sanitized_method_name ||= @method_name.sub(/\?$/,"")
end
end
class FormBuilder
# The methods which wrap a form helper call.
class_attribute :field_helpers

@ -154,7 +154,7 @@ module FormOptionsHelper
# 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)
Tags::Select.new(object, method, self, choices, options, html_options).render
end
# Returns <tt><select></tt> and <tt><option></tt> tags for the collection of existing return values of
@ -188,10 +188,9 @@ def select(object, method, choices, options = {}, html_options = {})
# <option value="3">M. Clark</option>
# </select>
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)
Tags::CollectionSelect.new(object, method, self, collection, value_method, text_method, options, html_options).render
end
# 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>
@ -240,7 +239,7 @@ def collection_select(object, method, collection, value_method, text_method, opt
# </select>
#
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)
Tags::GroupedCollectionSelect.new(object, method, self, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options).render
end
# Return select and option tags for the given object and method, using
@ -274,7 +273,7 @@ def grouped_collection_select(object, method, collection, group_method, group_la
#
# 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)
Tags::TimeZoneSelect.new(object, method, self, priority_zones, options, html_options).render
end
# Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container
@ -573,69 +572,6 @@ def extract_values_from_collection(collection, value_method, selected)
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)
# Grouped choices look like this:
#
# [nil, []]
# { nil => [] }
#
if !choices.empty? && choices.first.respond_to?(:last) && Array === choices.first.last
option_tags = grouped_options_for_select(choices, :selected => selected_value, :disabled => options[:disabled])
else
option_tags = options_for_select(choices, :selected => selected_value, :disabled => options[:disabled])
end
select_content_tag(option_tags, 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 = "<option value=\"\">#{ERB::Util.html_escape(options[:include_blank]) if options[:include_blank].kind_of?(String)}</option>\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 = "<option value=\"\">#{ERB::Util.html_escape(prompt)}</option>\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))

@ -0,0 +1,28 @@
module ActionView
module Helpers
module Tags
autoload :Base, 'action_view/helpers/tags/base'
autoload :Label, 'action_view/helpers/tags/label'
autoload :TextField, 'action_view/helpers/tags/text_field'
autoload :PasswordField, 'action_view/helpers/tags/password_field'
autoload :HiddenField, 'action_view/helpers/tags/hidden_field'
autoload :FileField, 'action_view/helpers/tags/file_field'
autoload :SearchField, 'action_view/helpers/tags/search_field'
autoload :TelField, 'action_view/helpers/tags/tel_field'
autoload :UrlField, 'action_view/helpers/tags/url_field'
autoload :EmailField, 'action_view/helpers/tags/email_field'
autoload :NumberField, 'action_view/helpers/tags/number_field'
autoload :RangeField, 'action_view/helpers/tags/range_field'
autoload :TextArea, 'action_view/helpers/tags/text_area'
autoload :CheckBox, 'action_view/helpers/tags/check_box'
autoload :RadioButton, 'action_view/helpers/tags/radio_button'
autoload :Select, 'action_view/helpers/tags/select'
autoload :CollectionSelect, 'action_view/helpers/tags/collection_select'
autoload :GroupedCollectionSelect, 'action_view/helpers/tags/grouped_collection_select'
autoload :TimeZoneSelect, 'action_view/helpers/tags/time_zone_select'
autoload :DateSelect, 'action_view/helpers/tags/date_select'
autoload :TimeSelect, 'action_view/helpers/tags/time_select'
autoload :DatetimeSelect, 'action_view/helpers/tags/datetime_select'
end
end
end

@ -0,0 +1,134 @@
module ActionView
module Helpers
module Tags
class Base #:nodoc:
include Helpers::ActiveModelInstanceTag, Helpers::TagHelper, Helpers::FormTagHelper
include FormOptionsHelper
DEFAULT_FIELD_OPTIONS = { "size" => 30 }
attr_reader :object
def initialize(object_name, method_name, template_object, options = {})
@object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
@template_object = template_object
@object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]")
@object = retrieve_object(options.delete(:object))
@options = options
@auto_index = retrieve_autoindex(Regexp.last_match.pre_match) if Regexp.last_match
end
def render(&block)
raise "Abstract Method called"
end
private
def value(object)
object.send @method_name if object
end
def value_before_type_cast(object)
unless object.nil?
object.respond_to?(@method_name + "_before_type_cast") ?
object.send(@method_name + "_before_type_cast") :
object.send(@method_name)
end
end
def retrieve_object(object)
if object
object
elsif @template_object.instance_variable_defined?("@#{@object_name}")
@template_object.instance_variable_get("@#{@object_name}")
end
rescue NameError
# As @object_name may contain the nested syntax (item[subobject]) we need to fallback to nil.
nil
end
def retrieve_autoindex(pre_match)
object = self.object || @template_object.instance_variable_get("@#{pre_match}")
if object && object.respond_to?(:to_param)
object.to_param
else
raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
end
end
def add_default_name_and_id_for_value(tag_value, options)
unless tag_value.nil?
pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase
specified_id = options["id"]
add_default_name_and_id(options)
options["id"] += "_#{pretty_tag_value}" if specified_id.blank? && options["id"].present?
else
add_default_name_and_id(options)
end
end
def add_default_name_and_id(options)
if options.has_key?("index")
options["name"] ||= tag_name_with_index(options["index"])
options["id"] = options.fetch("id"){ tag_id_with_index(options["index"]) }
options.delete("index")
elsif defined?(@auto_index)
options["name"] ||= tag_name_with_index(@auto_index)
options["id"] = options.fetch("id"){ tag_id_with_index(@auto_index) }
else
options["name"] ||= tag_name + (options['multiple'] ? '[]' : '')
options["id"] = options.fetch("id"){ tag_id }
end
options["id"] = [options.delete('namespace'), options["id"]].compact.join("_").presence
end
def tag_name
"#{@object_name}[#{sanitized_method_name}]"
end
def tag_name_with_index(index)
"#{@object_name}[#{index}][#{sanitized_method_name}]"
end
def tag_id
"#{sanitized_object_name}_#{sanitized_method_name}"
end
def tag_id_with_index(index)
"#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
end
def sanitized_object_name
@sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
end
def sanitized_method_name
@sanitized_method_name ||= @method_name.sub(/\?$/,"")
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
def add_options(option_tags, options, value = nil)
if options[:include_blank]
option_tags = "<option value=\"\">#{ERB::Util.html_escape(options[:include_blank]) if options[:include_blank].kind_of?(String)}</option>\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 = "<option value=\"\">#{ERB::Util.html_escape(prompt)}</option>\n" + option_tags
end
option_tags.html_safe
end
end
end
end
end

@ -0,0 +1,54 @@
require 'action_view/helpers/tags/checkable'
module ActionView
module Helpers
module Tags
class CheckBox < Base #:nodoc:
include Checkable
def initialize(object_name, method_name, template_object, checked_value, unchecked_value, options)
@checked_value = checked_value
@unchecked_value = unchecked_value
super(object_name, method_name, template_object, options)
end
def render
options = @options.stringify_keys
options["type"] = "checkbox"
options["value"] = @checked_value
options["checked"] = "checked" if input_checked?(object, options)
if options["multiple"]
add_default_name_and_id_for_value(@checked_value, options)
options.delete("multiple")
else
add_default_name_and_id(options)
end
hidden = @unchecked_value ? tag("input", "name" => options["name"], "type" => "hidden", "value" => @unchecked_value, "disabled" => options["disabled"]) : ""
checkbox = tag("input", options)
hidden + checkbox
end
private
def checked?(value)
case value
when TrueClass, FalseClass
value
when NilClass
false
when Integer
value != 0
when String
value == @checked_value
when Array
value.include?(@checked_value)
else
value.to_i != 0
end
end
end
end
end
end

@ -0,0 +1,16 @@
module ActionView
module Helpers
module Tags
module Checkable
def input_checked?(object, options)
if options.has_key?("checked")
checked = options.delete "checked"
checked == true || checked == "checked"
else
checked?(value(object))
end
end
end
end
end
end

@ -0,0 +1,23 @@
module ActionView
module Helpers
module Tags
class CollectionSelect < Base #:nodoc:
def initialize(object_name, method_name, template_object, collection, value_method, text_method, options, html_options)
@collection = collection
@value_method = value_method
@text_method = text_method
@html_options = html_options
super(object_name, method_name, template_object, options)
end
def render
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
end
end
end
end

@ -0,0 +1,64 @@
module ActionView
module Helpers
module Tags
class DateSelect < Base #:nodoc:
def initialize(object_name, method_name, template_object, options, html_options)
@html_options = html_options
super(object_name, method_name, template_object, options)
end
def render
error_wrapping(datetime_selector(@options, @html_options).send("select_#{select_type}").html_safe)
end
private
def select_type
self.class.name.split("::").last.sub("Select", "").downcase
end
def datetime_selector(options, html_options)
datetime = value(object) || default_datetime(options)
@auto_index ||= nil
options = options.dup
options[:field_name] = @method_name
options[:include_position] = true
options[:prefix] ||= @object_name
options[:index] = @auto_index if @auto_index && !options.has_key?(:index)
DateTimeSelector.new(datetime, options, html_options)
end
def default_datetime(options)
return if options[:include_blank] || options[:prompt]
case options[:default]
when nil
Time.current
when Date, Time
options[:default]
else
default = options[:default].dup
# Rename :minute and :second to :min and :sec
default[:min] ||= default[:minute]
default[:sec] ||= default[:second]
time = Time.current
[:year, :month, :day, :hour, :min, :sec].each do |key|
default[key] ||= time.send(key)
end
Time.utc_time(
default[:year], default[:month], default[:day],
default[:hour], default[:min], default[:sec]
)
end
end
end
end
end
end

@ -0,0 +1,8 @@
module ActionView
module Helpers
module Tags
class DatetimeSelect < DateSelect #:nodoc:
end
end
end
end

@ -0,0 +1,8 @@
module ActionView
module Helpers
module Tags
class EmailField < TextField #:nodoc:
end
end
end
end

@ -0,0 +1,12 @@
module ActionView
module Helpers
module Tags
class FileField < TextField #:nodoc:
def render
@options.update(:size => nil)
super
end
end
end
end
end

@ -0,0 +1,24 @@
module ActionView
module Helpers
module Tags
class GroupedCollectionSelect < Base #:nodoc:
def initialize(object_name, method_name, template_object, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options)
@collection = collection
@group_method = group_method
@group_label_method = group_label_method
@option_key_method = option_key_method
@option_value_method = option_value_method
@html_options = html_options
super(object_name, method_name, template_object, options)
end
def render
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
end
end
end
end

@ -0,0 +1,12 @@
module ActionView
module Helpers
module Tags
class HiddenField < TextField #:nodoc:
def render
@options.update(:size => nil)
super
end
end
end
end
end

@ -0,0 +1,65 @@
module ActionView
module Helpers
module Tags
class Label < Base #:nodoc:
def initialize(object_name, method_name, template_object, content_or_options = nil, options = nil)
content_is_options = content_or_options.is_a?(Hash)
if content_is_options
options = content_or_options
@content = nil
else
@content = content_or_options
end
options ||= {}
super(object_name, method_name, template_object, options)
end
def render(&block)
options = @options.stringify_keys
tag_value = options.delete("value")
name_and_id = options.dup
if name_and_id["for"]
name_and_id["id"] = name_and_id["for"]
else
name_and_id.delete("id")
end
add_default_name_and_id_for_value(tag_value, name_and_id)
options.delete("index")
options.delete("namespace")
options["for"] ||= name_and_id["id"]
if block_given?
@template_object.label_tag(name_and_id["id"], options, &block)
else
content = if @content.blank?
@object_name.gsub!(/\[(.*)_attributes\]\[\d\]/, '.\1')
method_and_value = tag_value.present? ? "#{@method_name}.#{tag_value}" : @method_name
if object.respond_to?(:to_model)
key = object.class.model_name.i18n_key
i18n_default = ["#{key}.#{method_and_value}".to_sym, ""]
end
i18n_default ||= ""
I18n.t("#{@object_name}.#{method_and_value}", :default => i18n_default, :scope => "helpers.label").presence
else
@content.to_s
end
content ||= if object && object.class.respond_to?(:human_attribute_name)
object.class.human_attribute_name(@method_name)
end
content ||= @method_name.humanize
label_tag(name_and_id["id"], content, options)
end
end
end
end
end
end

@ -0,0 +1,19 @@
module ActionView
module Helpers
module Tags
class NumberField < TextField #:nodoc:
def render
options = @options.stringify_keys
options['size'] ||= nil
if range = options.delete("in") || options.delete("within")
options.update("min" => range.min, "max" => range.max)
end
@options = options
super
end
end
end
end
end

@ -0,0 +1,12 @@
module ActionView
module Helpers
module Tags
class PasswordField < TextField #:nodoc:
def render
@options = {:value => nil}.merge!(@options)
super
end
end
end
end
end

@ -0,0 +1,31 @@
require 'action_view/helpers/tags/checkable'
module ActionView
module Helpers
module Tags
class RadioButton < Base #:nodoc:
include Checkable
def initialize(object_name, method_name, template_object, tag_value, options)
@tag_value = tag_value
super(object_name, method_name, template_object, options)
end
def render
options = @options.stringify_keys
options["type"] = "radio"
options["value"] = @tag_value
options["checked"] = "checked" if input_checked?(object, options)
add_default_name_and_id_for_value(@tag_value, options)
tag("input", options)
end
private
def checked?(value)
value.to_s == @tag_value.to_s
end
end
end
end
end

@ -0,0 +1,8 @@
module ActionView
module Helpers
module Tags
class RangeField < NumberField #:nodoc:
end
end
end
end

@ -0,0 +1,24 @@
module ActionView
module Helpers
module Tags
class SearchField < TextField #:nodoc:
def render
options = @options.stringify_keys
if options["autosave"]
if options["autosave"] == true
options["autosave"] = request.host.split(".").reverse.join(".")
end
options["results"] ||= 10
end
if options["onsearch"]
options["incremental"] = true unless options.has_key?("incremental")
end
super
end
end
end
end
end

@ -0,0 +1,31 @@
module ActionView
module Helpers
module Tags
class Select < Base #:nodoc:
def initialize(object_name, method_name, template_object, choices, options, html_options)
@choices = choices
@html_options = html_options
super(object_name, method_name, template_object, options)
end
def render
selected_value = @options.has_key?(:selected) ? @options[:selected] : value(@object)
# Grouped choices look like this:
#
# [nil, []]
# { nil => [] }
#
if !@choices.empty? && @choices.first.respond_to?(:last) && Array === @choices.first.last
option_tags = grouped_options_for_select(@choices, :selected => selected_value, :disabled => @options[:disabled])
else
option_tags = options_for_select(@choices, :selected => selected_value, :disabled => @options[:disabled])
end
select_content_tag(option_tags, @options, @html_options)
end
end
end
end
end

@ -0,0 +1,8 @@
module ActionView
module Helpers
module Tags
class TelField < TextField #:nodoc:
end
end
end
end

@ -0,0 +1,20 @@
module ActionView
module Helpers
module Tags
class TextArea < Base #:nodoc:
DEFAULT_TEXT_AREA_OPTIONS = { "cols" => 40, "rows" => 20 }
def render
options = DEFAULT_TEXT_AREA_OPTIONS.merge(@options.stringify_keys)
add_default_name_and_id(options)
if size = options.delete("size")
options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
end
content_tag("textarea", ERB::Util.html_escape(options.delete('value') || value_before_type_cast(object)), options)
end
end
end
end
end

@ -0,0 +1,24 @@
module ActionView
module Helpers
module Tags
class TextField < Base #:nodoc:
def render
options = @options.stringify_keys
options["size"] = options["maxlength"] || DEFAULT_FIELD_OPTIONS["size"] unless options.key?("size")
options = DEFAULT_FIELD_OPTIONS.merge(options)
options["type"] ||= field_type
options["value"] = options.fetch("value"){ value_before_type_cast(object) } unless field_type == "file"
options["value"] &&= ERB::Util.html_escape(options["value"])
add_default_name_and_id(options)
tag("input", options)
end
private
def field_type
@field_type ||= self.class.name.split("::").last.sub("Field", "").downcase
end
end
end
end
end

@ -0,0 +1,8 @@
module ActionView
module Helpers
module Tags
class TimeSelect < DateSelect #:nodoc:
end
end
end
end

@ -0,0 +1,20 @@
module ActionView
module Helpers
module Tags
class TimeZoneSelect < Base #:nodoc:
def initialize(object_name, method_name, template_object, priority_zones, options, html_options)
@priority_zones = priority_zones
@html_options = html_options
super(object_name, method_name, template_object, options)
end
def render
select_content_tag(
time_zone_options_for_select(value(@object) || @options[:default], @priority_zones, @options[:model] || ActiveSupport::TimeZone), @options, @html_options
)
end
end
end
end
end

@ -0,0 +1,8 @@
module ActionView
module Helpers
module Tags
class UrlField < TextField #:nodoc:
end
end
end
end

@ -232,6 +232,10 @@ def test_label_with_block
assert_dom_equal('<label for="post_title">The title, please:</label>', label(:post, :title) { "The title, please:" })
end
def test_label_with_block_and_options
assert_dom_equal('<label for="my_for">The title, please:</label>', label(:post, :title, "for" => "my_for") { "The title, please:" })
end
def test_label_with_block_in_erb
assert_equal "<label for=\"post_message\">\n Message\n <input id=\"post_message\" name=\"post[message]\" size=\"30\" type=\"text\" />\n</label>", view.render("test/label_with_block")
end