diff --git a/lib/bootstrap_form/form_builder.rb b/lib/bootstrap_form/form_builder.rb index bcc70c66d..c7f74053a 100644 --- a/lib/bootstrap_form/form_builder.rb +++ b/lib/bootstrap_form/form_builder.rb @@ -38,9 +38,7 @@ def initialize(object_name, object, template, options) define_method(with_method_name) do |name, options = {}| form_group_builder(name, options) do - prepend_and_append_input(options) do - send(without_method_name, name, options) - end + send(without_method_name, name, options) end end @@ -52,6 +50,7 @@ def initialize(object_name, object, template, options) without_method_name = "#{method_name}_without_bootstrap" define_method(with_method_name) do |name, options = {}, html_options = {}| + prevent_prepend_and_append!(options) form_group_builder(name, options, html_options) do content_tag(:div, send(without_method_name, name, options, html_options), class: control_specific_class(method_name)) end @@ -61,6 +60,7 @@ def initialize(object_name, object, template, options) end def file_field_with_bootstrap(name, options = {}) + prevent_prepend_and_append!(options) options = options.reverse_merge(control_class: 'form-control-file') form_group_builder(name, options) do file_field_without_bootstrap(name, options) @@ -71,15 +71,14 @@ def file_field_with_bootstrap(name, options = {}) def select_with_bootstrap(method, choices = nil, options = {}, html_options = {}, &block) form_group_builder(method, options, html_options) do - prepend_and_append_input(options) do - select_without_bootstrap(method, choices, options, html_options, &block) - end + select_without_bootstrap(method, choices, options, html_options, &block) end end bootstrap_method_alias :select def collection_select_with_bootstrap(method, collection, value_method, text_method, options = {}, html_options = {}) + prevent_prepend_and_append!(options) form_group_builder(method, options, html_options) do collection_select_without_bootstrap(method, collection, value_method, text_method, options, html_options) end @@ -88,6 +87,7 @@ def collection_select_with_bootstrap(method, collection, value_method, text_meth bootstrap_method_alias :collection_select def grouped_collection_select_with_bootstrap(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {}) + prevent_prepend_and_append!(options) form_group_builder(method, options, html_options) do grouped_collection_select_without_bootstrap(method, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options) end @@ -96,6 +96,7 @@ def grouped_collection_select_with_bootstrap(method, collection, group_method, g bootstrap_method_alias :grouped_collection_select def time_zone_select_with_bootstrap(method, priority_zones = nil, options = {}, html_options = {}) + prevent_prepend_and_append!(options) form_group_builder(method, options, html_options) do time_zone_select_without_bootstrap(method, priority_zones, options, html_options) end @@ -104,7 +105,32 @@ def time_zone_select_with_bootstrap(method, priority_zones = nil, options = {}, bootstrap_method_alias :time_zone_select def check_box_with_bootstrap(name, options = {}, checked_value = "1", unchecked_value = "0", &block) + prevent_prepend_and_append!(options) options = options.symbolize_keys! + + wrapped_check_box(custom: options[:custom], disabled: options[:disabled], inline: layout_inline?(options[:inline])) do + unwrapped_check_box(name, options, checked_value, unchecked_value, &block) + end + end + + bootstrap_method_alias :check_box + + private + def wrapped_check_box(custom: false, disabled: false, inline: false) + if custom + wrapper_classes = ["custom-control", "custom-checkbox"] + wrapper_classes.append("custom-control-inline") if inline + else + wrapper_classes = ["form-check"] + wrapper_classes << "form-check-inline" if inline + end + wrapper_class = wrapper_classes.compact.join(" ") + content_tag(:div, class: wrapper_class) do + yield + end + end + + def unwrapped_check_box(name, options = {}, checked_value = "1", unchecked_value = "0", &block) check_box_options = options.except(:label, :label_class, :help, :inline, :custom, :hide_label, :skip_label) check_box_classes = [check_box_options[:class]] check_box_classes << "position-static" if options[:skip_label] || options[:hide_label] @@ -126,45 +152,56 @@ def check_box_with_bootstrap(name, options = {}, checked_value = "1", unchecked_ # https://github.com/rails/rails/blob/c57e7239a8b82957bcb07534cb7c1a3dcef71864/actionview/lib/action_view/helpers/tags/base.rb#L116-L118 if options[:multiple] label_name = - "#{name}_#{checked_value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase}" + "#{name}_#{checked_value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase}" end label_classes = [options[:label_class]] label_classes << hide_class if options[:hide_label] - if options[:custom] - div_class = ["custom-control", "custom-checkbox"] - div_class.append("custom-control-inline") if options[:inline] + if options[:skip_label] + checkbox_html + elsif options[:custom] label_class = label_classes.prepend("custom-control-label").compact.join(" ") - content_tag(:div, class: div_class.compact.join(" ")) do - if options[:skip_label] - checkbox_html - else - # TODO: Notice we don't seem to pass the ID into the custom control. - checkbox_html.concat(label(label_name, label_description, class: label_class)) - end - end + # TODO: Notice we don't seem to pass the ID into the custom control. + checkbox_html.concat(label(label_name, label_description, class: label_class)) else - wrapper_class = "form-check" - wrapper_class += " form-check-inline" if options[:inline] label_class = label_classes.prepend("form-check-label").compact.join(" ") - content_tag(:div, class: wrapper_class) do - if options[:skip_label] - checkbox_html - else - checkbox_html - .concat(label(label_name, - label_description, - { class: label_class }.merge(options[:id].present? ? { for: options[:id] } : {}))) - end - end + checkbox_html + .concat(label(label_name, + label_description, + { class: label_class }.merge(options[:id].present? ? { for: options[:id] } : {}))) end end - - bootstrap_method_alias :check_box + public def radio_button_with_bootstrap(name, value, *args) + prevent_prepend_and_append!(options) options = args.extract_options!.symbolize_keys! + + wrapped_radio(custom: options[:custom], disabled: options[:disabled], inline: layout_inline?(options[:inline])) do + unwrapped_radio(name, value, options, *args) + end + end + + bootstrap_method_alias :radio_button + + private + def wrapped_radio(custom: false, disabled: false, inline: false) + if custom + wrapper_classes = ["custom-control", "custom-radio"] + wrapper_classes.append("custom-control-inline") if inline + else + wrapper_classes = ["form-check"] + wrapper_classes.append("form-check-inline") if inline + wrapper_classes.append("disabled") if disabled + end + wrapper_class = wrapper_classes.compact.join(" ") + content_tag(:div, class: wrapper_class) do + yield + end + end + + def unwrapped_radio(name, value, options, *args) radio_options = options.except(:label, :label_class, :help, :inline, :custom, :hide_label, :skip_label) radio_classes = [options[:class]] radio_classes << "position-static" if options[:skip_label] || options[:hide_label] @@ -176,52 +213,68 @@ def radio_button_with_bootstrap(name, value, *args) args << radio_options radio_html = radio_button_without_bootstrap(name, value, *args) - disabled_class = " disabled" if options[:disabled] - label_classes = [options[:label_class]] + label_classes = [options[:label_class]] label_classes << hide_class if options[:hide_label] - if options[:custom] - div_class = ["custom-control", "custom-radio"] - div_class.append("custom-control-inline") if options[:inline] + if options[:skip_label] + radio_html + elsif options[:custom] label_class = label_classes.prepend("custom-control-label").compact.join(" ") - content_tag(:div, class: div_class.compact.join(" ")) do - if options[:skip_label] - radio_html - else - # TODO: Notice we don't seem to pass the ID into the custom control. - radio_html.concat(label(name, options[:label], value: value, class: label_class)) - end - end + # TODO: Notice we don't seem to pass the ID into the custom control. + radio_html.concat(label(name, options[:label], value: value, class: label_class)) else - wrapper_class = "form-check" - wrapper_class += " form-check-inline" if options[:inline] label_class = label_classes.prepend("form-check-label").compact.join(" ") - content_tag(:div, class: "#{wrapper_class}#{disabled_class}") do - if options[:skip_label] - radio_html - else - radio_html - .concat(label(name, options[:label], { value: value, class: label_class }.merge(options[:id].present? ? { for: options[:id] } : {}))) - end - end + radio_html + .concat(label(name, options[:label], { value: value, class: label_class }.merge(options[:id].present? ? { for: options[:id] } : {}))) end end - - bootstrap_method_alias :radio_button - - def collection_check_boxes_with_bootstrap(*args) - html = inputs_collection(*args) do |name, value, options| - options[:multiple] = true - check_box(name, options, value, nil) + public + + def collection_check_boxes_with_bootstrap(outer_name, collection, outer_value, text, outer_options = {}) + prevent_prepend_and_append!(outer_options) + # This next line is because the options get munged in the legacy code. + help = outer_options[:help] + # Use begin..ensure so block returns the html and resets the in...collection flag. + begin + # The following is an ugly way to prevent `form_group` from outputting + # the error on the wrapper, when Bootstrap 4 wants it to be in the last + # element. + self.in_radio_checkbox_collection = true + html = inputs_collection(outer_name, collection, outer_value, text, outer_options) do |name, value, options, i| + options[:multiple] = true + wrapped_check_box(custom: options[:custom], disabled: options[:disabled], inline: layout_inline?(options[:inline])) do + check_box_html = unwrapped_check_box(name, options, value, nil) + check_box_html.concat(generate_help(name, help)) if i == collection.size - 1 + check_box_html + end + end + ensure + self.in_radio_checkbox_collection = false end - hidden_field(args.first,{value: "", multiple: true}).concat(html) + hidden_field(outer_name, value: "", multiple: true).concat(html) end bootstrap_method_alias :collection_check_boxes - def collection_radio_buttons_with_bootstrap(*args) - inputs_collection(*args) do |name, value, options| - radio_button(name, value, options) + def collection_radio_buttons_with_bootstrap(outer_name, collection, outer_value, text, outer_options = {}) + prevent_prepend_and_append!(outer_options) + # This next line is because the options get munged in the legacy code. + help = outer_options[:help] + # Use begin..ensure so block returns the html and resets the in...collection flag. + begin + # The following is an ugly way to prevent `form_group` from outputting + # the error on the wrapper, when Bootstrap 4 wants it to be in the last + # element. + self.in_radio_checkbox_collection = true + inputs_collection(outer_name, collection, outer_value, text, outer_options) do |name, value, options, i| + wrapped_radio(custom: options[:custom], disabled: options[:disabled], inline: layout_inline?(options[:inline])) do + radio_html = unwrapped_radio(name, value, options) + radio_html.concat(generate_help(name, help)) if i == collection.size - 1 + radio_html + end + end + ensure + self.in_radio_checkbox_collection = false end end @@ -235,10 +288,9 @@ def form_group(*args, &block) options[:class] << " row" if get_group_layout(options[:layout]) == :horizontal options[:class] << " #{feedback_class}" if options[:icon] - content_tag(:div, options.except(:id, :label, :help, :icon, :label_col, :control_col, :layout)) do + content_tag(:div, options.except(:append, :id, :label, :help, :icon, :input_group_class, :label_col, :control_col, :layout, :prepend)) do label = generate_label(options[:id], name, options[:label], options[:label_col], options[:layout]) if options[:label] - control = capture(&block).to_s - control.concat(generate_help(name, options[:help]).to_s) + control = prepend_and_append_input(name, options, &block).to_s if get_group_layout(options[:layout]) == :horizontal control_class = options[:control_col] || control_col @@ -274,8 +326,28 @@ def fields_for_with_bootstrap(record_name, record_object = nil, fields_options = private - def horizontal? - layout == :horizontal + def layout_default?(field_layout = nil) + [:default, nil].include? layout_in_effect(field_layout) + end + + def layout_horizontal?(field_layout = nil) + layout_in_effect(field_layout) == :horizontal + end + + def layout_inline?(field_layout = nil) + layout_in_effect(field_layout) == :inline + end + + def field_inline_override?(field_layout = nil) + field_layout == :inline && layout != :inline + end + + # true and false should only come from check_box and radio_button, + # and those don't have a :horizontal layout + def layout_in_effect(field_layout) + field_layout = :inline if field_layout == true + field_layout = :default if field_layout == false + field_layout || layout end def get_group_layout(group_layout) @@ -294,6 +366,14 @@ def default_control_col "col-sm-10" end + def in_radio_checkbox_collection? + @in_radio_checkbox_collection ||= false + end + + def in_radio_checkbox_collection=(state) + @in_radio_checkbox_collection = state + end + def hide_class "sr-only" # still accessible for screen readers end @@ -366,6 +446,10 @@ def form_group_builder(method, options, html_options = nil) class: wrapper_class } + form_group_options[:append] = options.delete(:append) if options[:append] + form_group_options[:prepend] = options.delete(:prepend) if options[:prepend] + form_group_options[:input_group_class] = options.delete(:input_group_class) if options[:input_group_class] + if wrapper_options.is_a?(Hash) form_group_options.merge!(wrapper_options) end @@ -455,7 +539,7 @@ def inputs_collection(name, collection, value, text, options = {}, &block) form_group_builder(name, options) do inputs = "" - collection.each do |obj| + collection.each_with_index do |obj, i| input_options = options.merge(label: text.respond_to?(:call) ? text.call(obj) : obj.send(text)) input_value = value.respond_to?(:call) ? value.call(obj) : obj.send(value) @@ -467,7 +551,7 @@ def inputs_collection(name, collection, value, text, options = {}, &block) end input_options.delete(:class) - inputs << block.call(name, input_value, input_options) + inputs << block.call(name, input_value, input_options, i) end inputs.html_safe diff --git a/lib/bootstrap_form/helpers/bootstrap.rb b/lib/bootstrap_form/helpers/bootstrap.rb index 058c9dc05..208c0cee9 100644 --- a/lib/bootstrap_form/helpers/bootstrap.rb +++ b/lib/bootstrap_form/helpers/bootstrap.rb @@ -66,18 +66,28 @@ def custom_control(*args, &block) form_group_builder(name, options, &block) end - def prepend_and_append_input(options, &block) + def prepend_and_append_input(name, options, &block) + help = options[:help] options = options.extract!(:prepend, :append, :input_group_class) input_group_class = ["input-group", options[:input_group_class]].compact.join(' ') - input = capture(&block) + input = capture(&block) || "".html_safe input = content_tag(:div, input_group_content(options[:prepend]), class: 'input-group-prepend') + input if options[:prepend] input << content_tag(:div, input_group_content(options[:append]), class: 'input-group-append') if options[:append] + input << generate_help(name, help).to_s unless in_radio_checkbox_collection? input = content_tag(:div, input, class: input_group_class) unless options.empty? input end + # Some helpers don't currently accept prepend and append. However, it's not + # clear if that's corrent. In the meantime, strip to options before calling + # methods that don't accept prepend and append. + def prevent_prepend_and_append!(options) + options.delete(:append) + options.delete(:prepend) + end + def input_group_content(content) return content if content.match(/btn/) content_tag(:span, content, class: 'input-group-text') diff --git a/test/bootstrap_checkbox_test.rb b/test/bootstrap_checkbox_test.rb index 6798963ed..80938c3db 100644 --- a/test/bootstrap_checkbox_test.rb +++ b/test/bootstrap_checkbox_test.rb @@ -109,6 +109,25 @@ class BootstrapCheckboxTest < ActionView::TestCase assert_equivalent_xml expected, @builder.check_box(:terms, label: 'I agree to the terms', inline: true) end + test "inline checkboxes from form layout" do + expected = <<-HTML.strip_heredoc +
+ HTML + actual = bootstrap_form_for(@user, layout: :inline) do |f| + f.check_box(:terms, label: 'I agree to the terms') + end + assert_equivalent_xml expected, actual + end + test "disabled inline check_box" do expected = <<-HTML.strip_heredoc