diff --git a/actionpack/lib/action_view/helpers/active_model_helper.rb b/actionpack/lib/action_view/helpers/active_model_helper.rb index 56f15604a6..973135e2ea 100644 --- a/actionpack/lib/action_view/helpers/active_model_helper.rb +++ b/actionpack/lib/action_view/helpers/active_model_helper.rb @@ -16,9 +16,7 @@ module ActionView 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 diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index 2806348337..f5077b034a 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -213,7 +213,7 @@ module ActionView # 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 @@ module ActionView # 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 @@ module ActionView # # 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 @@ module ActionView 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) diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index ca2eb1ac10..10277599a0 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -3,6 +3,7 @@ require 'action_view/helpers/date_helper' 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 @@ module ActionView # 'Accept Terms.'.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 @@ module ActionView # # => # 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 @@ module ActionView # # => # 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 @@ module ActionView # hidden_field(: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 @@ module ActionView # # => # 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 @@ module ActionView # # #{@entry.body} # # 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 @@ module ActionView # # # 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 @@ module ActionView # # => # # 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 @@ module ActionView # # => # 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 @@ module ActionView # # => # 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 @@ module ActionView # # => # 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 @@ module ActionView # # => # 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 @@ module ActionView # ==== 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 @@ module ActionView # ==== 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 @@ module ActionView 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 = "" - 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 diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index ba9ff1d5aa..e323350608 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -154,7 +154,7 @@ module ActionView # 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 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 # 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 @@ module ActionView # # 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 @@ module ActionView 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 = "\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)) diff --git a/actionpack/lib/action_view/helpers/tags.rb b/actionpack/lib/action_view/helpers/tags.rb new file mode 100644 index 0000000000..89b3efda5f --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags.rb @@ -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 diff --git a/actionpack/lib/action_view/helpers/tags/base.rb b/actionpack/lib/action_view/helpers/tags/base.rb new file mode 100644 index 0000000000..24956beb9c --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/base.rb @@ -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 = "\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 + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/check_box.rb b/actionpack/lib/action_view/helpers/tags/check_box.rb new file mode 100644 index 0000000000..b3bd6eb2ad --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/check_box.rb @@ -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 diff --git a/actionpack/lib/action_view/helpers/tags/checkable.rb b/actionpack/lib/action_view/helpers/tags/checkable.rb new file mode 100644 index 0000000000..b97c0c68d7 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/checkable.rb @@ -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 diff --git a/actionpack/lib/action_view/helpers/tags/collection_select.rb b/actionpack/lib/action_view/helpers/tags/collection_select.rb new file mode 100644 index 0000000000..f84140d8d0 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/collection_select.rb @@ -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 diff --git a/actionpack/lib/action_view/helpers/tags/date_select.rb b/actionpack/lib/action_view/helpers/tags/date_select.rb new file mode 100644 index 0000000000..5912598ca1 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/date_select.rb @@ -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 diff --git a/actionpack/lib/action_view/helpers/tags/datetime_select.rb b/actionpack/lib/action_view/helpers/tags/datetime_select.rb new file mode 100644 index 0000000000..a32c840bce --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/datetime_select.rb @@ -0,0 +1,8 @@ +module ActionView + module Helpers + module Tags + class DatetimeSelect < DateSelect #:nodoc: + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/email_field.rb b/actionpack/lib/action_view/helpers/tags/email_field.rb new file mode 100644 index 0000000000..45cde507d7 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/email_field.rb @@ -0,0 +1,8 @@ +module ActionView + module Helpers + module Tags + class EmailField < TextField #:nodoc: + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/file_field.rb b/actionpack/lib/action_view/helpers/tags/file_field.rb new file mode 100644 index 0000000000..56442e1c14 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/file_field.rb @@ -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 diff --git a/actionpack/lib/action_view/helpers/tags/grouped_collection_select.rb b/actionpack/lib/action_view/helpers/tags/grouped_collection_select.rb new file mode 100644 index 0000000000..507466a57a --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/grouped_collection_select.rb @@ -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 diff --git a/actionpack/lib/action_view/helpers/tags/hidden_field.rb b/actionpack/lib/action_view/helpers/tags/hidden_field.rb new file mode 100644 index 0000000000..ea86596e0b --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/hidden_field.rb @@ -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 diff --git a/actionpack/lib/action_view/helpers/tags/label.rb b/actionpack/lib/action_view/helpers/tags/label.rb new file mode 100644 index 0000000000..74ac92ee18 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/label.rb @@ -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 diff --git a/actionpack/lib/action_view/helpers/tags/number_field.rb b/actionpack/lib/action_view/helpers/tags/number_field.rb new file mode 100644 index 0000000000..e89fdbec46 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/number_field.rb @@ -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 diff --git a/actionpack/lib/action_view/helpers/tags/password_field.rb b/actionpack/lib/action_view/helpers/tags/password_field.rb new file mode 100644 index 0000000000..6e7a4d3c36 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/password_field.rb @@ -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 diff --git a/actionpack/lib/action_view/helpers/tags/radio_button.rb b/actionpack/lib/action_view/helpers/tags/radio_button.rb new file mode 100644 index 0000000000..8a0421f061 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/radio_button.rb @@ -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 diff --git a/actionpack/lib/action_view/helpers/tags/range_field.rb b/actionpack/lib/action_view/helpers/tags/range_field.rb new file mode 100644 index 0000000000..47db4680e7 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/range_field.rb @@ -0,0 +1,8 @@ +module ActionView + module Helpers + module Tags + class RangeField < NumberField #:nodoc: + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/search_field.rb b/actionpack/lib/action_view/helpers/tags/search_field.rb new file mode 100644 index 0000000000..818fd4b887 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/search_field.rb @@ -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 diff --git a/actionpack/lib/action_view/helpers/tags/select.rb b/actionpack/lib/action_view/helpers/tags/select.rb new file mode 100644 index 0000000000..71fd4d04b7 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/select.rb @@ -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 diff --git a/actionpack/lib/action_view/helpers/tags/tel_field.rb b/actionpack/lib/action_view/helpers/tags/tel_field.rb new file mode 100644 index 0000000000..87c1f6b6b6 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/tel_field.rb @@ -0,0 +1,8 @@ +module ActionView + module Helpers + module Tags + class TelField < TextField #:nodoc: + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/text_area.rb b/actionpack/lib/action_view/helpers/tags/text_area.rb new file mode 100644 index 0000000000..a7db8eb437 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/text_area.rb @@ -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 diff --git a/actionpack/lib/action_view/helpers/tags/text_field.rb b/actionpack/lib/action_view/helpers/tags/text_field.rb new file mode 100644 index 0000000000..0f81726eb4 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/text_field.rb @@ -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 diff --git a/actionpack/lib/action_view/helpers/tags/time_select.rb b/actionpack/lib/action_view/helpers/tags/time_select.rb new file mode 100644 index 0000000000..9e97deb706 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/time_select.rb @@ -0,0 +1,8 @@ +module ActionView + module Helpers + module Tags + class TimeSelect < DateSelect #:nodoc: + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/time_zone_select.rb b/actionpack/lib/action_view/helpers/tags/time_zone_select.rb new file mode 100644 index 0000000000..0a176157c3 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/time_zone_select.rb @@ -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 diff --git a/actionpack/lib/action_view/helpers/tags/url_field.rb b/actionpack/lib/action_view/helpers/tags/url_field.rb new file mode 100644 index 0000000000..1ffdfe0b3c --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/url_field.rb @@ -0,0 +1,8 @@ +module ActionView + module Helpers + module Tags + class UrlField < TextField #:nodoc: + end + end + end +end diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index 82e001732d..5be6a2e4e5 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -232,6 +232,10 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal('', label(:post, :title) { "The title, please:" }) end + def test_label_with_block_and_options + assert_dom_equal('', label(:post, :title, "for" => "my_for") { "The title, please:" }) + end + def test_label_with_block_in_erb assert_equal "", view.render("test/label_with_block") end