diff options
Diffstat (limited to 'actionview/lib/action_view/helpers/tags')
12 files changed, 197 insertions, 93 deletions
diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb index 8607da301c..d57f26ba4f 100644 --- a/actionview/lib/action_view/helpers/tags/base.rb +++ b/actionview/lib/action_view/helpers/tags/base.rb @@ -14,7 +14,7 @@ module ActionView @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 + @auto_index = Regexp.last_match ? retrieve_autoindex(Regexp.last_match.pre_match) : nil end # This is what child classes implement. @@ -25,19 +25,26 @@ module ActionView private def value(object) - object.send @method_name if object + object.public_send @method_name if object end def value_before_type_cast(object) unless object.nil? method_before_type_cast = @method_name + "_before_type_cast" - object.respond_to?(method_before_type_cast) ? - object.send(method_before_type_cast) : + if value_came_from_user?(object) && object.respond_to?(method_before_type_cast) + object.public_send(method_before_type_cast) + else value(object) + end end end + def value_came_from_user?(object) + method_name = "#{@method_name}_came_from_user?" + !object.respond_to?(method_name) || object.public_send(method_name) + end + def retrieve_object(object) if object object @@ -72,35 +79,30 @@ module ActionView end def add_default_name_and_id(options) - if options.has_key?("index") - options["name"] ||= options.fetch("name"){ tag_name_with_index(options["index"], options["multiple"]) } - options["id"] = options.fetch("id"){ tag_id_with_index(options["index"]) } - options.delete("index") - elsif defined?(@auto_index) - options["name"] ||= options.fetch("name"){ tag_name_with_index(@auto_index, options["multiple"]) } - options["id"] = options.fetch("id"){ tag_id_with_index(@auto_index) } - else - options["name"] ||= options.fetch("name"){ tag_name(options["multiple"]) } - options["id"] = options.fetch("id"){ tag_id } + index = name_and_id_index(options) + options["name"] = options.fetch("name"){ tag_name(options["multiple"], index) } + options["id"] = options.fetch("id"){ tag_id(index) } + if namespace = options.delete("namespace") + options['id'] = options['id'] ? "#{namespace}_#{options['id']}" : namespace end - - options["id"] = [options.delete('namespace'), options["id"]].compact.join("_").presence - end - - def tag_name(multiple = false) - "#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}" - end - - def tag_name_with_index(index, multiple = false) - "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}" end - def tag_id - "#{sanitized_object_name}_#{sanitized_method_name}" + def tag_name(multiple = false, index = nil) + # a little duplication to construct less strings + if index + "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}" + else + "#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}" + end end - def tag_id_with_index(index) - "#{sanitized_object_name}_#{index}_#{sanitized_method_name}" + def tag_id(index = nil) + # a little duplication to construct less strings + if index + "#{sanitized_object_name}_#{index}_#{sanitized_method_name}" + else + "#{sanitized_object_name}_#{sanitized_method_name}" + end end def sanitized_object_name @@ -118,7 +120,12 @@ module ActionView def select_content_tag(option_tags, options, html_options) html_options = html_options.stringify_keys add_default_name_and_id(html_options) - options[:include_blank] ||= true unless options[:prompt] || select_not_required?(html_options) + + if placeholder_required?(html_options) + raise ArgumentError, "include_blank cannot be false for a required field." if options[:include_blank] == false + options[:include_blank] ||= true unless options[:prompt] + end + value = options.fetch(:selected) { value(object) } select = content_tag("select", add_options(option_tags, options, value), html_options) @@ -129,8 +136,9 @@ module ActionView end end - def select_not_required?(html_options) - !html_options["required"] || html_options["multiple"] || html_options["size"].to_i > 1 + def placeholder_required?(html_options) + # See https://html.spec.whatwg.org/multipage/forms.html#attr-select-required + html_options["required"] && !html_options["multiple"] && html_options.fetch("size", 1).to_i == 1 end def add_options(option_tags, options, value = nil) @@ -142,6 +150,10 @@ module ActionView end option_tags end + + def name_and_id_index(options) + options.key?("index") ? options.delete("index") || "" : @auto_index + end end end end diff --git a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb index 6242a2a085..3256d44e18 100644 --- a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb +++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb @@ -9,29 +9,13 @@ module ActionView class CheckBoxBuilder < Builder # :nodoc: def check_box(extra_html_options={}) html_options = extra_html_options.merge(@input_html_options) + html_options[:multiple] = true @template_object.check_box(@object_name, @method_name, html_options, @value, nil) end end def render(&block) - rendered_collection = render_collection do |item, value, text, default_html_options| - default_html_options[:multiple] = true - builder = instantiate_builder(CheckBoxBuilder, item, value, text, default_html_options) - - if block_given? - @template_object.capture(builder, &block) - else - render_component(builder) - end - end - - # Append a hidden field to make sure something will be sent back to the - # server if all check boxes are unchecked. - if @options.fetch(:include_hidden, true) - rendered_collection + hidden_field - else - rendered_collection - end + render_collection_for(CheckBoxBuilder, &block) end private @@ -39,18 +23,6 @@ module ActionView def render_component(builder) builder.check_box + builder.label end - - def hidden_field - hidden_name = @html_options[:name] - - hidden_name ||= if @options.has_key?(:index) - "#{tag_name_with_index(@options[:index])}[]" - else - "#{tag_name}[]" - end - - @template_object.hidden_field_tag(hidden_name, "", id: nil) - end end end end diff --git a/actionview/lib/action_view/helpers/tags/collection_helpers.rb b/actionview/lib/action_view/helpers/tags/collection_helpers.rb index 8050638363..b87b4281d6 100644 --- a/actionview/lib/action_view/helpers/tags/collection_helpers.rb +++ b/actionview/lib/action_view/helpers/tags/collection_helpers.rb @@ -19,6 +19,8 @@ module ActionView def label(label_html_options={}, &block) html_options = @input_html_options.slice(:index, :namespace).merge(label_html_options) + html_options[:for] ||= @input_html_options[:id] if @input_html_options[:id] + @template_object.label(@object_name, @sanitized_attribute_name, @text, html_options, &block) end end @@ -79,6 +81,32 @@ module ActionView yield item, value, text, default_html_options.merge(additional_html_options) end.join.html_safe end + + def render_collection_for(builder_class, &block) #:nodoc: + options = @options.stringify_keys + rendered_collection = render_collection do |item, value, text, default_html_options| + builder = instantiate_builder(builder_class, item, value, text, default_html_options) + + if block_given? + @template_object.capture(builder, &block) + else + render_component(builder) + end + end + + # Append a hidden field to make sure something will be sent back to the + # server if all radio buttons are unchecked. + if options.fetch('include_hidden', true) + rendered_collection + hidden_field + else + rendered_collection + end + end + + def hidden_field #:nodoc: + hidden_name = @html_options[:name] || "#{tag_name(false, @options[:index])}[]" + @template_object.hidden_field_tag(hidden_name, "", id: nil) + end end end end diff --git a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb index 20be34c1f2..21aaf122f8 100644 --- a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb +++ b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb @@ -14,15 +14,7 @@ module ActionView end def render(&block) - render_collection do |item, value, text, default_html_options| - builder = instantiate_builder(RadioButtonBuilder, item, value, text, default_html_options) - - if block_given? - @template_object.capture(builder, &block) - else - render_component(builder) - end - end + render_collection_for(RadioButtonBuilder, &block) end private diff --git a/actionview/lib/action_view/helpers/tags/file_field.rb b/actionview/lib/action_view/helpers/tags/file_field.rb index 476b820d84..e6a1d9c62d 100644 --- a/actionview/lib/action_view/helpers/tags/file_field.rb +++ b/actionview/lib/action_view/helpers/tags/file_field.rb @@ -2,6 +2,21 @@ module ActionView module Helpers module Tags # :nodoc: class FileField < TextField # :nodoc: + + def render + options = @options.stringify_keys + + if options.fetch("include_hidden", true) + add_default_name_and_id(options) + options[:type] = "file" + tag("input", name: options["name"], type: "hidden", value: "") + tag("input", options) + else + options.delete("include_hidden") + @options = options + + super + end + end end end end diff --git a/actionview/lib/action_view/helpers/tags/label.rb b/actionview/lib/action_view/helpers/tags/label.rb index a5bcaf8153..b31d5fda66 100644 --- a/actionview/lib/action_view/helpers/tags/label.rb +++ b/actionview/lib/action_view/helpers/tags/label.rb @@ -2,6 +2,29 @@ module ActionView module Helpers module Tags # :nodoc: class Label < Base # :nodoc: + class LabelBuilder # :nodoc: + attr_reader :object + + def initialize(template_object, object_name, method_name, object, tag_value) + @template_object = template_object + @object_name = object_name + @method_name = method_name + @object = object + @tag_value = tag_value + end + + def translation + method_and_value = @tag_value.present? ? "#{@method_name}.#{@tag_value}" : @method_name + + content ||= Translator + .new(object, @object_name, method_and_value, scope: "helpers.label") + .translate + content ||= @method_name.humanize + + content + end + end + def initialize(object_name, method_name, template_object, content_or_options = nil, options = nil) options ||= {} @@ -32,33 +55,24 @@ module ActionView options.delete("namespace") options["for"] = name_and_id["id"] unless options.key?("for") - if block_given? - content = @template_object.capture(&block) - else - method_and_value = tag_value.present? ? "#{@method_name}.#{tag_value}" : @method_name - content = if @content.blank? - @object_name.gsub!(/\[(.*)_attributes\]\[\d+\]/, '.\1') - - 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 + builder = LabelBuilder.new(@template_object, @object_name, @method_name, @object, tag_value) - content ||= if object && object.class.respond_to?(:human_attribute_name) - object.class.human_attribute_name(method_and_value) - end - - content ||= @method_name.humanize + content = if block_given? + @template_object.capture(builder, &block) + elsif @content.present? + @content.to_s + else + render_component(builder) end label_tag(name_and_id["id"], content, options) end + + private + + def render_component(builder) + builder.translation + end end end end diff --git a/actionview/lib/action_view/helpers/tags/placeholderable.rb b/actionview/lib/action_view/helpers/tags/placeholderable.rb new file mode 100644 index 0000000000..cf7b117614 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/placeholderable.rb @@ -0,0 +1,22 @@ +module ActionView + module Helpers + module Tags # :nodoc: + module Placeholderable # :nodoc: + def initialize(*) + super + + if tag_value = @options[:placeholder] + placeholder = tag_value if tag_value.is_a?(String) + method_and_value = tag_value.is_a?(TrueClass) ? @method_name : "#{@method_name}.#{tag_value}" + + placeholder ||= Tags::Translator + .new(object, @object_name, method_and_value, scope: "helpers.placeholder") + .translate + placeholder ||= @method_name.humanize + @options[:placeholder] = placeholder + end + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/search_field.rb b/actionview/lib/action_view/helpers/tags/search_field.rb index c09e2f1be7..a848aeabfa 100644 --- a/actionview/lib/action_view/helpers/tags/search_field.rb +++ b/actionview/lib/action_view/helpers/tags/search_field.rb @@ -16,6 +16,7 @@ module ActionView options["incremental"] = true unless options.has_key?("incremental") end + @options = options super end end diff --git a/actionview/lib/action_view/helpers/tags/select.rb b/actionview/lib/action_view/helpers/tags/select.rb index 00881d9978..180900cc8d 100644 --- a/actionview/lib/action_view/helpers/tags/select.rb +++ b/actionview/lib/action_view/helpers/tags/select.rb @@ -3,7 +3,7 @@ module ActionView module Tags # :nodoc: class Select < Base # :nodoc: def initialize(object_name, method_name, template_object, choices, options, html_options) - @choices = block_given? ? template_object.capture { yield } : choices + @choices = block_given? ? template_object.capture { yield || "" } : choices @choices = @choices.to_a if @choices.is_a?(Range) @html_options = html_options diff --git a/actionview/lib/action_view/helpers/tags/text_area.rb b/actionview/lib/action_view/helpers/tags/text_area.rb index 9ee83ee7c2..69038c1498 100644 --- a/actionview/lib/action_view/helpers/tags/text_area.rb +++ b/actionview/lib/action_view/helpers/tags/text_area.rb @@ -1,7 +1,11 @@ +require 'action_view/helpers/tags/placeholderable' + module ActionView module Helpers module Tags # :nodoc: class TextArea < Base # :nodoc: + include Placeholderable + def render options = @options.stringify_keys add_default_name_and_id(options) diff --git a/actionview/lib/action_view/helpers/tags/text_field.rb b/actionview/lib/action_view/helpers/tags/text_field.rb index e0b80d81c2..5c576a20ca 100644 --- a/actionview/lib/action_view/helpers/tags/text_field.rb +++ b/actionview/lib/action_view/helpers/tags/text_field.rb @@ -1,7 +1,11 @@ +require 'action_view/helpers/tags/placeholderable' + module ActionView module Helpers module Tags # :nodoc: class TextField < Base # :nodoc: + include Placeholderable + def render options = @options.stringify_keys options["size"] = options["maxlength"] unless options.key?("size") diff --git a/actionview/lib/action_view/helpers/tags/translator.rb b/actionview/lib/action_view/helpers/tags/translator.rb new file mode 100644 index 0000000000..8b6655481d --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/translator.rb @@ -0,0 +1,40 @@ +module ActionView + module Helpers + module Tags # :nodoc: + class Translator # :nodoc: + def initialize(object, object_name, method_and_value, scope:) + @object_name = object_name.gsub(/\[(.*)_attributes\]\[\d+\]/, '.\1') + @method_and_value = method_and_value + @scope = scope + @model = object.respond_to?(:to_model) ? object.to_model : nil + end + + def translate + translated_attribute = I18n.t("#{object_name}.#{method_and_value}", default: i18n_default, scope: scope).presence + translated_attribute || human_attribute_name + end + + protected + + attr_reader :object_name, :method_and_value, :scope, :model + + private + + def i18n_default + if model + key = model.model_name.i18n_key + ["#{key}.#{method_and_value}".to_sym, ""] + else + "" + end + end + + def human_attribute_name + if model && model.class.respond_to?(:human_attribute_name) + model.class.human_attribute_name(method_and_value) + end + end + end + end + end +end |