diff --git a/.yarnrc b/.yarnrc deleted file mode 100644 index adf41e8f..00000000 --- a/.yarnrc +++ /dev/null @@ -1,5 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -lastUpdateCheck 1763256402231 diff --git a/README.md b/README.md index b233c2d3..b6b3d637 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,8 @@ This generates the following HTML: ``` +Note: All examples in this README are generated with the configuration option `group_around_collections` set to `true`. See the [Configuration](#configuration) section. + ### bootstrap_form_tag If your form is not backed by a model, use the `bootstrap_form_tag`. Usage of this helper is the same as `bootstrap_form_for`, except no model object is passed in as the first argument. Here's an example: @@ -233,6 +235,7 @@ The current configuration options are: | Option | Default value | Description | |---------------------------|------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `default_form_attributes` | {} | `bootstrap_form` versions 3 and 4 added a role="form" attribute to all forms. The W3C validator will raise a **warning** on forms with a role="form" attribute. `bootstrap_form` version 5 drops this attribute by default. Set this option to `{ role: "form" }` to make forms non-compliant with W3C, but generate the `role="form"` attribute like `bootstrap_form` versions 3 and 4. | +| `group_around_collections` | false | Historically, `bootstrap_form` generated a wrapper around `collection_checkboxes` and `collection_radio_buttons` using the same `form_group` as individual controls used. This markup caused accessibility problems. Setting `group_around_collections = true` will generate collections of checkboxes and radio buttons wrapper in a `
` with the text as a `` (https://www.w3.org/WAI/tutorials/forms/grouping/). This _will_ make visible changes to pages that use the collection methods.

The default for this option will be changed to `true` in a future version. | Example: @@ -781,8 +784,8 @@ This generates: This generates: ```html -
- +
+
Skill level
@@ -793,8 +796,8 @@ This generates:
-
- +
+
Skills
@@ -829,8 +832,8 @@ To add `data-` attributes to a collection of radio buttons, map your models to a This generates: ```html -
- +
+
Misc
@@ -1417,7 +1420,7 @@ This generates: ``` -A form-level `layout: :inline` can't be overridden because of the way Bootstrap 4 implements in-line layouts. One possible work-around is to leave the form-level layout as default, and specify the individual fields as `layout: :inline`, except for the fields(s) that should be other than in-line. +A form-level `layout: :inline` can't be overridden because of the way Bootstrap implements in-line layouts. One possible work-around is to leave the form-level layout as default, and specify the individual fields as `layout: :inline`, except for the fields(s) that should be other than in-line. ### Floating Labels @@ -1493,8 +1496,8 @@ Generated HTML:
is invalid
-
- +
+
Misc
@@ -1506,8 +1509,8 @@ Generated HTML:
-
- +
+
Preferences
diff --git a/demo/test/system/bootstrap_test.rb b/demo/test/system/bootstrap_test.rb index b55681ff..cb81ecc7 100644 --- a/demo/test/system/bootstrap_test.rb +++ b/demo/test/system/bootstrap_test.rb @@ -4,7 +4,14 @@ require "capybara_screenshot_diff/minitest" class BootstrapTest < ApplicationSystemTestCase - setup { screenshot_section :bootstrap } + setup do + screenshot_section :bootstrap + Rails.application.config.bootstrap_form.group_around_collections = true + end + + teardown do + Rails.application.config.bootstrap_form.group_around_collections = false + end test "visiting the index" do screenshot_group :index diff --git a/lib/bootstrap_form/components/labels.rb b/lib/bootstrap_form/components/labels.rb index 579957ed..d8f03655 100644 --- a/lib/bootstrap_form/components/labels.rb +++ b/lib/bootstrap_form/components/labels.rb @@ -10,16 +10,8 @@ module Labels def generate_label(id, name, options, custom_label_col, group_layout) return if options.blank? - # id is the caller's options[:id] at the only place this method is called. - # The options argument is a small subset of the options that might have - # been passed to generate_label's caller, and definitely doesn't include - # :id. - options[:for] = id if acts_like_form_tag - - options[:class] = label_classes(name, options, custom_label_col, group_layout) - options.delete(:class) if options[:class].none? - - label(name, label_text(name, options), options.except(:text)) + prepare_label_options(id, name, options, custom_label_col, group_layout) + label(name, label_text(name, options[:text]), options.except(:text)) end def label_classes(name, options, custom_label_col, group_layout) @@ -42,14 +34,25 @@ def label_layout_classes(custom_label_col, group_layout) end end - def label_text(name, options) - label = options[:text] || object&.class&.try(:human_attribute_name, name)&.html_safe # rubocop:disable Rails/OutputSafety, Style/SafeNavigationChainLength + def label_text(name, text) + label = text || object&.class&.try(:human_attribute_name, name)&.html_safe # rubocop:disable Rails/OutputSafety, Style/SafeNavigationChainLength if label_errors && error?(name) (" ".html_safe + get_error_messages(name)).prepend(label) else label end end + + def prepare_label_options(id, name, options, custom_label_col, group_layout) + # id is the caller's options[:id] at the only place this method is called. + # The options argument is a small subset of the options that might have + # been passed to generate_label's caller, and definitely doesn't include + # :id. + options[:for] = id if acts_like_form_tag + + options[:class] = label_classes(name, options, custom_label_col, group_layout) + options.delete(:class) if options[:class].none? + end end end end diff --git a/lib/bootstrap_form/engine.rb b/lib/bootstrap_form/engine.rb index 8f7c7151..33784878 100644 --- a/lib/bootstrap_form/engine.rb +++ b/lib/bootstrap_form/engine.rb @@ -9,6 +9,7 @@ class Engine < Rails::Engine config.bootstrap_form = BootstrapForm.config config.bootstrap_form.default_form_attributes ||= {} + config.bootstrap_form.group_around_collections = Rails.env.development? if config.bootstrap_form.group_around_collections.nil? initializer "bootstrap_form.configure" do |app| BootstrapForm.config = app.config.bootstrap_form diff --git a/lib/bootstrap_form/form_group_builder.rb b/lib/bootstrap_form/form_group_builder.rb index 79eff7e5..36e7155a 100644 --- a/lib/bootstrap_form/form_group_builder.rb +++ b/lib/bootstrap_form/form_group_builder.rb @@ -7,6 +7,16 @@ module FormGroupBuilder private def form_group_builder(method, options, html_options=nil, &) + form_group_builder_wrapper(method, options, html_options) do |form_group_options, no_wrapper| + if no_wrapper + yield + else + form_group(method, form_group_options, &) + end + end + end + + def form_group_builder_wrapper(method, options, html_options=nil) no_wrapper = options[:wrapper] == false options = form_group_builder_options(options, method) @@ -18,11 +28,7 @@ def form_group_builder(method, options, html_options=nil, &) :hide_label, :skip_required, :label_as_placeholder, :wrapper_class, :wrapper ) - if no_wrapper - yield - else - form_group(method, form_group_options, &) - end + yield(form_group_options, no_wrapper) end def form_group_builder_options(options, method) diff --git a/lib/bootstrap_form/inputs/inputs_collection.rb b/lib/bootstrap_form/inputs/inputs_collection.rb index 0da18b60..2b5dc808 100644 --- a/lib/bootstrap_form/inputs/inputs_collection.rb +++ b/lib/bootstrap_form/inputs/inputs_collection.rb @@ -7,24 +7,20 @@ module InputsCollection private - def inputs_collection(name, collection, value, text, options={}) + def inputs_collection(name, collection, value, text, options={}, &) options[:label] ||= { class: group_label_class(field_layout(options)) } options[:inline] ||= layout_inline?(options[:layout]) - form_group_builder(name, options) do - inputs = ActiveSupport::SafeBuffer.new - - collection.each_with_index do |obj, i| - input_value = value.respond_to?(:call) ? value.call(obj) : obj.send(value) - input_options = form_group_collection_input_options(options, text, obj, i, input_value, collection) - inputs << yield(name, input_value, input_options) - end + return group_inputs_collection(name, collection, value, text, options, &) if BootstrapForm.config.group_around_collections - inputs + form_group_builder(name, options) do + render_collection(name, collection, value, text, options, &) end end - def field_layout(options) = options[:layout] || (:inline if options[:inline] == true) + def field_layout(options) + (:inline if options[:inline] == true) || options[:layout] + end def group_label_class(field_layout) if layout_horizontal?(field_layout) @@ -56,6 +52,63 @@ def form_group_collection_input_checked?(checked, obj, input_value) checked == input_value || Array(checked).try(:include?, input_value) || checked == obj || Array(checked).try(:include?, obj) end + + def group_inputs_collection(name, collection, value, text, options={}, &) + group_builder(name, options) do + render_collection(name, collection, value, text, options, &) + end + end + + def render_collection(name, collection, value, text, options={}, &) + inputs = ActiveSupport::SafeBuffer.new + + collection.each_with_index do |obj, i| + input_value = value.respond_to?(:call) ? value.call(obj) : obj.send(value) + input_options = form_group_collection_input_options(options, text, obj, i, input_value, collection) + inputs << yield(name, input_value, input_options) + end + + inputs + end + + def group_builder(method, options, html_options=nil, &) + form_group_builder_wrapper(method, options, html_options) do |form_group_options, no_wrapper| + if no_wrapper + yield + else + field_group(method, form_group_options, &) + end + end + end + + def field_group(name, options, &) + options[:class] = form_group_classes(options) + + tag.div( + **options.except( + :add_control_col_class, :append, :control_col, :floating, :help, :icon, :id, + :input_group_class, :label, :label_col, :layout, :prepend + ), + aria: { labelledby: options[:id] || default_id(name) }, + role: :group + ) do + group_label_div = generate_group_label_div(name, options) + prepare_label_options(options[:id], name, options[:label], options[:label_col], options[:layout]) + form_group_content(group_label_div, generate_help(name, options[:help]), options, &) + end + end + + def generate_group_label_div(name, options) + group_label_div_class = options.dig(:label, :class) || "form-label" + id = options[:id] || default_id(name) + + tag.div( + **{ class: group_label_div_class }.compact, + id: + ) { label_text(name, options.dig(:label, :text)) } + end + + def default_id(name) = raw("#{object_name}_#{name}") # rubocop:disable Rails/OutputSafety end end end diff --git a/test/bootstrap_checkbox_test.rb b/test/bootstrap_checkbox_test.rb index 48a8bb94..121ba276 100644 --- a/test/bootstrap_checkbox_test.rb +++ b/test/bootstrap_checkbox_test.rb @@ -168,350 +168,6 @@ class BootstrapCheckboxTest < ActionView::TestCase assert_equivalent_html expected, @builder.check_box(:terms, inline: true, label_class: "btn") end - test "collection_check_boxes renders the form_group correctly" do - collection = [Address.new(id: 1, street: "Foobar")] - expected = <<~HTML - -
- -
- - -
- With a help! -
- HTML - - assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street, - label: "This is a checkbox collection", help: "With a help!") - end - - if Rails::VERSION::MAJOR >= 8 - test "collection_checkboxes renders the form_group correctly" do - collection = [Address.new(id: 1, street: "Foobar")] - expected = <<~HTML - -
- -
- - -
- With a help! -
- HTML - - assert_equivalent_html expected, @builder.collection_checkboxes(:misc, collection, :id, :street, - label: "This is a checkbox collection", help: "With a help!") - end - end - - test "collection_check_boxes renders multiple checkboxes correctly" do - collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] - expected = <<~HTML - -
- -
- - -
-
- - -
-
- HTML - - assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street) - end - - test "collection_check_boxes renders multiple checkboxes contains unicode characters in IDs correctly" do - struct = Struct.new(:id, :name) - collection = [struct.new(1, "Foo"), struct.new("二", "Bar")] - expected = <<~HTML - -
- -
- - -
-
- - -
-
- HTML - - assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :name) - end - - test "collection_check_boxes renders inline checkboxes correctly" do - collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] - expected = <<~HTML - -
- -
- - -
-
- - -
-
- HTML - - assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street, inline: true) - end - - test "collection_check_boxes renders with checked option correctly" do - collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] - expected = <<~HTML - -
- -
- - -
-
- - -
-
- HTML - - assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street, - checked: 1) - assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street, - checked: collection.first) - end - - test "collection_check_boxes renders with multiple checked options correctly" do - collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] - expected = <<~HTML - -
- -
- - -
-
- - -
-
- HTML - - assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street, - checked: [1, 2]) - assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street, - checked: collection) - end - - test "collection_check_boxes sanitizes values when generating label `for`" do - collection = [Address.new(id: 1, street: "Foo St")] - expected = <<~HTML - -
- -
- - -
-
- HTML - assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :street, :street) - end - - test "collection_check_boxes renders multiple checkboxes with labels defined by Proc :text_method correctly" do - collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] - expected = <<~HTML - -
- -
- - -
-
- - -
-
- HTML - - assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, proc { |a| a.street.reverse }) - end - - test "collection_check_boxes renders multiple checkboxes with values defined by Proc :value_method correctly" do - collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] - expected = <<~HTML - -
- -
- - -
-
- - -
-
- HTML - assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, proc { |a| "address_#{a.id}" }, - :street) - end - - test "collection_check_boxes renders multiple checkboxes with labels defined by lambda :text_method correctly" do - collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] - expected = <<~HTML - -
- -
- - -
-
- - -
-
- HTML - - assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, ->(a) { a.street.reverse }) - end - - test "collection_check_boxes renders multiple checkboxes with values defined by lambda :value_method correctly" do - collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] - expected = <<~HTML - -
- -
- - -
-
- - -
-
- HTML - - assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, ->(a) { "address_#{a.id}" }, - :street) - end - - test "collection_check_boxes renders with checked option correctly with Proc :value_method" do - collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] - expected = <<~HTML - -
- -
- - -
-
- - -
-
- HTML - - assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, proc { |a| "address_#{a.id}" }, - :street, checked: "address_1") - assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, proc { |a| "address_#{a.id}" }, - :street, checked: collection.first) - end - - test "collection_check_boxes renders with multiple checked options correctly with lambda :value_method" do - collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] - expected = <<~HTML - -
- -
- - -
-
- - -
-
- HTML - - assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, ->(a) { "address_#{a.id}" }, - :street, checked: %w[address_1 address_2]) - assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, ->(a) { "address_#{a.id}" }, - :street, checked: collection) - end - - test "collection_check_boxes renders with include_hidden options correctly" do - collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] - expected = <<~HTML -
- -
- - -
-
- - -
-
- HTML - - assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street, include_hidden: false) - end - test "check_box skip label" do expected = <<~HTML
@@ -533,84 +189,6 @@ class BootstrapCheckboxTest < ActionView::TestCase assert_equivalent_html expected, @builder.check_box(:terms, label: "I agree to the terms", hide_label: true) end - test "collection_check_boxes renders error after last check box" do - collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] - @user.errors.add(:misc, "a box must be checked") - - expected = <<~HTML -
- -
- -
- - -
-
- - -
a box must be checked
-
-
-
- HTML - - actual = bootstrap_form_for(@user) do |f| - f.collection_check_boxes(:misc, collection, :id, :street) - end - - assert_equivalent_html expected, actual - end - - test "collection_check_boxes renders data attributes" do - collection = [ - ["1", "Foo", { "data-city": "east" }], - ["2", "Bar", { "data-city": "west" }] - ] - expected = <<~HTML -
- -
- - -
-
- - -
-
- HTML - - assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :first, :second, include_hidden: false) - end - - test "collection_check_boxes renders multiple check boxes with error correctly" do - @user.errors.add(:misc, "error for test") - collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] - expected = <<~HTML -
- -
- -
- - -
-
- - -
error for test
-
-
-
- HTML - - actual = bootstrap_form_for(@user) do |f| - f.collection_check_boxes(:misc, collection, :id, :street, checked: collection) - end - assert_equivalent_html expected, actual - end - test "check_box renders error when asked" do @user.errors.add(:terms, "You must accept the terms.") expected = <<~HTML diff --git a/test/bootstrap_collection_checkboxes_test.rb b/test/bootstrap_collection_checkboxes_test.rb new file mode 100644 index 00000000..667b3008 --- /dev/null +++ b/test/bootstrap_collection_checkboxes_test.rb @@ -0,0 +1,873 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class BootstrapCollectionCheckboxesTest < ActionView::TestCase + include BootstrapForm::ActionViewExtensions::FormHelper + + setup do + setup_test_fixture + Rails.application.config.bootstrap_form.group_around_collections = true + end + + teardown do + Rails.application.config.bootstrap_form.group_around_collections = false + end + + test "collection_check_boxes renders the form_group correctly" do + collection = [Address.new(id: 1, street: "Foobar")] + expected = <<~HTML + +
+
This is a checkbox collection
+
+ + +
+ With a help! +
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street, + label: "This is a checkbox collection", help: "With a help!") + end + + if Rails::VERSION::MAJOR >= 8 + test "collection_checkboxes renders the form_group correctly" do + collection = [Address.new(id: 1, street: "Foobar")] + expected = <<~HTML + +
+
This is a checkbox collection
+
+ + +
+ With a help! +
+ HTML + + assert_equivalent_html expected, @builder.collection_checkboxes(:misc, collection, :id, :street, + label: "This is a checkbox collection", help: "With a help!") + end + end + + test "collection_check_boxes renders multiple checkboxes correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML + +
+
Misc
+
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street) + end + + test "collection_check_boxes renders multiple checkboxes contains unicode characters in IDs correctly" do + struct = Struct.new(:id, :name) + collection = [struct.new(1, "Foo"), struct.new("二", "Bar")] + expected = <<~HTML + +
+
Misc
+
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :name) + end + + test "collection_check_boxes renders inline checkboxes correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML + +
+
Misc
+
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street, inline: true) + end + + test "collection_check_boxes renders with checked option correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML + +
+
Misc
+
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street, + checked: 1) + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street, + checked: collection.first) + end + + test "collection_check_boxes renders with multiple checked options correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML + +
+
Misc
+
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street, + checked: [1, 2]) + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street, + checked: collection) + end + + test "collection_check_boxes sanitizes values when generating label `for`" do + collection = [Address.new(id: 1, street: "Foo St")] + expected = <<~HTML + +
+
Misc
+
+ + +
+
+ HTML + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :street, :street) + end + + test "collection_check_boxes renders multiple checkboxes with labels defined by Proc :text_method correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML + +
+
Misc
+
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, proc { |a| a.street.reverse }) + end + + test "collection_check_boxes renders multiple checkboxes with values defined by Proc :value_method correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML + +
+
Misc
+
+ + +
+
+ + +
+
+ HTML + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, proc { |a| "address_#{a.id}" }, + :street) + end + + test "collection_check_boxes renders multiple checkboxes with labels defined by lambda :text_method correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML + +
+
Misc
+
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, ->(a) { a.street.reverse }) + end + + test "collection_check_boxes renders multiple checkboxes with values defined by lambda :value_method correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML + +
+
Misc
+
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, ->(a) { "address_#{a.id}" }, + :street) + end + + test "collection_check_boxes renders with checked option correctly with Proc :value_method" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML + +
+
Misc
+
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, proc { |a| "address_#{a.id}" }, + :street, checked: "address_1") + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, proc { |a| "address_#{a.id}" }, + :street, checked: collection.first) + end + + test "collection_check_boxes renders with multiple checked options correctly with lambda :value_method" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML + +
+
Misc
+
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, ->(a) { "address_#{a.id}" }, + :street, checked: %w[address_1 address_2]) + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, ->(a) { "address_#{a.id}" }, + :street, checked: collection) + end + + test "collection_check_boxes renders with include_hidden options correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+
Misc
+
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street, include_hidden: false) + end + + test "collection_check_boxes renders error after last check box" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + @user.errors.add(:misc, "a box must be checked") + + expected = <<~HTML +
+ +
+
Misc
+
+ + +
+
+ + +
a box must be checked
+
+
+
+ HTML + + actual = bootstrap_form_for(@user) do |f| + f.collection_check_boxes(:misc, collection, :id, :street) + end + + assert_equivalent_html expected, actual + end + + test "collection_check_boxes renders data attributes" do + collection = [ + ["1", "Foo", { "data-city": "east" }], + ["2", "Bar", { "data-city": "west" }] + ] + expected = <<~HTML +
+
Misc
+
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :first, :second, include_hidden: false) + end + + test "collection_check_boxes renders multiple check boxes with error correctly" do + @user.errors.add(:misc, "error for test") + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+ +
+
Misc
+
+ + +
+
+ + +
error for test
+
+
+
+ HTML + + actual = bootstrap_form_for(@user) do |f| + f.collection_check_boxes(:misc, collection, :id, :street, checked: collection) + end + assert_equivalent_html expected, actual + end +end + +class BootstrapLegacyCollectionCheckboxesTest < ActionView::TestCase + include BootstrapForm::ActionViewExtensions::FormHelper + + setup do + setup_test_fixture + Rails.application.config.bootstrap_form.group_around_collections = false + end + + teardown do + Rails.application.config.bootstrap_form.group_around_collections = true + end + + test "collection_check_boxes renders the form_group correctly" do + collection = [Address.new(id: 1, street: "Foobar")] + expected = <<~HTML + +
+ +
+ + +
+ With a help! +
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street, + label: "This is a checkbox collection", help: "With a help!") + end + + if Rails::VERSION::MAJOR >= 8 + test "collection_checkboxes renders the form_group correctly" do + collection = [Address.new(id: 1, street: "Foobar")] + expected = <<~HTML + +
+ +
+ + +
+ With a help! +
+ HTML + + assert_equivalent_html expected, @builder.collection_checkboxes(:misc, collection, :id, :street, + label: "This is a checkbox collection", help: "With a help!") + end + end + + test "collection_check_boxes renders multiple checkboxes correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML + +
+ +
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street) + end + + test "collection_check_boxes renders multiple checkboxes contains unicode characters in IDs correctly" do + struct = Struct.new(:id, :name) + collection = [struct.new(1, "Foo"), struct.new("二", "Bar")] + expected = <<~HTML + +
+ +
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :name) + end + + test "collection_check_boxes renders inline checkboxes correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML + +
+ +
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street, inline: true) + end + + test "collection_check_boxes renders with checked option correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML + +
+ +
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street, + checked: 1) + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street, + checked: collection.first) + end + + test "collection_check_boxes renders with multiple checked options correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML + +
+ +
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street, + checked: [1, 2]) + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street, + checked: collection) + end + + test "collection_check_boxes sanitizes values when generating label `for`" do + collection = [Address.new(id: 1, street: "Foo St")] + expected = <<~HTML + +
+ +
+ + +
+
+ HTML + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :street, :street) + end + + test "collection_check_boxes renders multiple checkboxes with labels defined by Proc :text_method correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML + +
+ +
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, proc { |a| a.street.reverse }) + end + + test "collection_check_boxes renders multiple checkboxes with values defined by Proc :value_method correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML + +
+ +
+ + +
+
+ + +
+
+ HTML + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, proc { |a| "address_#{a.id}" }, + :street) + end + + test "collection_check_boxes renders multiple checkboxes with labels defined by lambda :text_method correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML + +
+ +
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, ->(a) { a.street.reverse }) + end + + test "collection_check_boxes renders multiple checkboxes with values defined by lambda :value_method correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML + +
+ +
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, ->(a) { "address_#{a.id}" }, + :street) + end + + test "collection_check_boxes renders with checked option correctly with Proc :value_method" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML + +
+ +
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, proc { |a| "address_#{a.id}" }, + :street, checked: "address_1") + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, proc { |a| "address_#{a.id}" }, + :street, checked: collection.first) + end + + test "collection_check_boxes renders with multiple checked options correctly with lambda :value_method" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML + +
+ +
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, ->(a) { "address_#{a.id}" }, + :street, checked: %w[address_1 address_2]) + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, ->(a) { "address_#{a.id}" }, + :street, checked: collection) + end + + test "collection_check_boxes renders with include_hidden options correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+ +
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :id, :street, include_hidden: false) + end + + test "collection_check_boxes renders error after last check box" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + @user.errors.add(:misc, "a box must be checked") + + expected = <<~HTML +
+ +
+ +
+ + +
+
+ + +
a box must be checked
+
+
+
+ HTML + + actual = bootstrap_form_for(@user) do |f| + f.collection_check_boxes(:misc, collection, :id, :street) + end + + assert_equivalent_html expected, actual + end + + test "collection_check_boxes renders data attributes" do + collection = [ + ["1", "Foo", { "data-city": "east" }], + ["2", "Bar", { "data-city": "west" }] + ] + expected = <<~HTML +
+ +
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_check_boxes(:misc, collection, :first, :second, include_hidden: false) + end + + test "collection_check_boxes renders multiple check boxes with error correctly" do + @user.errors.add(:misc, "error for test") + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+ +
+ +
+ + +
+
+ + +
error for test
+
+
+
+ HTML + + actual = bootstrap_form_for(@user) do |f| + f.collection_check_boxes(:misc, collection, :id, :street, checked: collection) + end + assert_equivalent_html expected, actual + end +end diff --git a/test/bootstrap_collection_radio_buttons_test.rb b/test/bootstrap_collection_radio_buttons_test.rb new file mode 100644 index 00000000..2c9344fe --- /dev/null +++ b/test/bootstrap_collection_radio_buttons_test.rb @@ -0,0 +1,535 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +class BootstrapCollectionRadioButtonsTest < ActionView::TestCase + include BootstrapForm::ActionViewExtensions::FormHelper + + setup do + setup_test_fixture + Rails.application.config.bootstrap_form.group_around_collections = true + end + + teardown do + Rails.application.config.bootstrap_form.group_around_collections = false + end + + test "collection_radio_buttons renders the form_group correctly" do + collection = [Address.new(id: 1, street: "Foobar")] + expected = <<~HTML +
+
This is a radio button collection
+
+ + +
+ With a help! +
+ HTML + + assert_equivalent_html expected, + @builder.collection_radio_buttons(:misc, collection, :id, :street, + label: "This is a radio button collection", help: "With a help!") + end + + test "collection_radio_buttons renders multiple radios correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+
Misc
+
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_radio_buttons(:misc, collection, :id, :street) + end + + test "collection_radio_buttons renders multiple radios with error correctly" do + @user.errors.add(:misc, "error for test") + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+
+
Misc
+
+ + +
+
+ + +
error for test
+
+
+
+ HTML + + actual = bootstrap_form_for(@user) do |f| + f.collection_radio_buttons(:misc, collection, :id, :street) + end + assert_equivalent_html expected, actual + end + + test "collection_radio_buttons renders inline radios correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+
Misc
+
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_radio_buttons(:misc, collection, :id, :street, inline: true) + end + + test "collection_radio_buttons renders with checked option correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+
Misc
+
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_radio_buttons(:misc, collection, :id, :street, checked: 1) + end + + test "collection_radio_buttons renders label defined by Proc correctly" do + collection = [Address.new(id: 1, street: "Foobar")] + expected = <<~HTML +
+
This is a radio button collection
+
+ + +
+ With a help! +
+ HTML + + assert_equivalent_html expected, + @builder.collection_radio_buttons(:misc, collection, :id, proc { |a| a.street.reverse }, + label: "This is a radio button collection", help: "With a help!") + end + + test "collection_radio_buttons renders value defined by Proc correctly" do + collection = [Address.new(id: 1, street: "Foobar")] + expected = <<~HTML +
+
This is a radio button collection
+
+ + +
+ With a help! +
+ HTML + + assert_equivalent_html expected, + @builder.collection_radio_buttons(:misc, collection, proc { |a| "address_#{a.id}" }, + :street, label: "This is a radio button collection", + help: "With a help!") + end + + test "collection_radio_buttons renders multiple radios with label defined by Proc correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+
Misc
+
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_radio_buttons(:misc, collection, :id, proc { |a| a.street.reverse }) + end + + test "collection_radio_buttons renders multiple radios with value defined by Proc correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+
Misc
+
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_radio_buttons(:misc, collection, proc { |a| "address_#{a.id}" }, :street) + end + + test "collection_radio_buttons renders label defined by lambda correctly" do + collection = [Address.new(id: 1, street: "Foobar")] + expected = <<~HTML +
+
This is a radio button collection
+
+ + +
+ With a help! +
+ HTML + + assert_equivalent_html expected, + @builder.collection_radio_buttons(:misc, collection, :id, ->(a) { a.street.reverse }, + label: "This is a radio button collection", help: "With a help!") + end + + test "collection_radio_buttons renders value defined by lambda correctly" do + collection = [Address.new(id: 1, street: "Foobar")] + expected = <<~HTML +
+
This is a radio button collection
+
+ + +
+ With a help! +
+ HTML + + assert_equivalent_html expected, + @builder.collection_radio_buttons(:misc, collection, ->(a) { "address_#{a.id}" }, + :street, label: "This is a radio button collection", + help: "With a help!") + end + + test "collection_radio_buttons renders multiple radios with label defined by lambda correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+
Misc
+
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_radio_buttons(:misc, collection, :id, ->(a) { a.street.reverse }) + end + + test "collection_radio_buttons renders multiple radios with value defined by lambda correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+
Misc
+
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_radio_buttons(:misc, collection, ->(a) { "address_#{a.id}" }, :street) + end +end + +class BootstrapCLegacyollectionRadioButtonsTest < ActionView::TestCase + include BootstrapForm::ActionViewExtensions::FormHelper + + setup do + setup_test_fixture + Rails.application.config.bootstrap_form.group_around_collections = false + end + + teardown do + Rails.application.config.bootstrap_form.group_around_collections = true + end + + test "collection_radio_buttons renders the form_group correctly" do + collection = [Address.new(id: 1, street: "Foobar")] + expected = <<~HTML +
+ +
+ + +
+ With a help! +
+ HTML + + assert_equivalent_html expected, + @builder.collection_radio_buttons(:misc, collection, :id, :street, + label: "This is a radio button collection", help: "With a help!") + end + + test "collection_radio_buttons renders multiple radios correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+ +
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_radio_buttons(:misc, collection, :id, :street) + end + + test "collection_radio_buttons renders multiple radios with error correctly" do + @user.errors.add(:misc, "error for test") + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+
+ +
+ + +
+
+ + +
error for test
+
+
+
+ HTML + + actual = bootstrap_form_for(@user) do |f| + f.collection_radio_buttons(:misc, collection, :id, :street) + end + assert_equivalent_html expected, actual + end + + test "collection_radio_buttons renders inline radios correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+ +
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_radio_buttons(:misc, collection, :id, :street, inline: true) + end + + test "collection_radio_buttons renders with checked option correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+ +
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_radio_buttons(:misc, collection, :id, :street, checked: 1) + end + + test "collection_radio_buttons renders label defined by Proc correctly" do + collection = [Address.new(id: 1, street: "Foobar")] + expected = <<~HTML +
+ +
+ + +
+ With a help! +
+ HTML + + assert_equivalent_html expected, + @builder.collection_radio_buttons(:misc, collection, :id, proc { |a| a.street.reverse }, + label: "This is a radio button collection", help: "With a help!") + end + + test "collection_radio_buttons renders value defined by Proc correctly" do + collection = [Address.new(id: 1, street: "Foobar")] + expected = <<~HTML +
+ +
+ + +
+ With a help! +
+ HTML + + assert_equivalent_html expected, + @builder.collection_radio_buttons(:misc, collection, proc { |a| "address_#{a.id}" }, + :street, label: "This is a radio button collection", + help: "With a help!") + end + + test "collection_radio_buttons renders multiple radios with label defined by Proc correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+ +
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_radio_buttons(:misc, collection, :id, proc { |a| a.street.reverse }) + end + + test "collection_radio_buttons renders multiple radios with value defined by Proc correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+ +
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_radio_buttons(:misc, collection, proc { |a| "address_#{a.id}" }, :street) + end + + test "collection_radio_buttons renders label defined by lambda correctly" do + collection = [Address.new(id: 1, street: "Foobar")] + expected = <<~HTML +
+ +
+ + +
+ With a help! +
+ HTML + + assert_equivalent_html expected, + @builder.collection_radio_buttons(:misc, collection, :id, ->(a) { a.street.reverse }, + label: "This is a radio button collection", help: "With a help!") + end + + test "collection_radio_buttons renders value defined by lambda correctly" do + collection = [Address.new(id: 1, street: "Foobar")] + expected = <<~HTML +
+ +
+ + +
+ With a help! +
+ HTML + + assert_equivalent_html expected, + @builder.collection_radio_buttons(:misc, collection, ->(a) { "address_#{a.id}" }, + :street, label: "This is a radio button collection", + help: "With a help!") + end + + test "collection_radio_buttons renders multiple radios with label defined by lambda correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+ +
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_radio_buttons(:misc, collection, :id, ->(a) { a.street.reverse }) + end + + test "collection_radio_buttons renders multiple radios with value defined by lambda correctly" do + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + expected = <<~HTML +
+ +
+ + +
+
+ + +
+
+ HTML + + assert_equivalent_html expected, @builder.collection_radio_buttons(:misc, collection, ->(a) { "address_#{a.id}" }, :street) + end +end diff --git a/test/bootstrap_form_test.rb b/test/bootstrap_form_test.rb index 6bd2deb0..22050769 100644 --- a/test/bootstrap_form_test.rb +++ b/test/bootstrap_form_test.rb @@ -5,7 +5,14 @@ class BootstrapFormTest < ActionView::TestCase include BootstrapForm::ActionViewExtensions::FormHelper - setup :setup_test_fixture + setup do + setup_test_fixture + Rails.application.config.bootstrap_form.group_around_collections = true + end + + teardown do + Rails.application.config.bootstrap_form.group_around_collections = false + end test "default-style forms" do expected = <<~HTML @@ -29,8 +36,8 @@ class BootstrapFormTest < ActionView::TestCase
-
- +
+
Misc
@@ -78,8 +85,8 @@ class BootstrapFormTest < ActionView::TestCase
-
- +
+
Misc
@@ -136,8 +143,8 @@ class BootstrapFormTest < ActionView::TestCase
-
- +
+
Misc
@@ -186,8 +193,8 @@ class BootstrapFormTest < ActionView::TestCase
-
- +
+
Misc
@@ -238,8 +245,8 @@ class BootstrapFormTest < ActionView::TestCase
-
- +
+
Misc
@@ -287,8 +294,8 @@ class BootstrapFormTest < ActionView::TestCase
-
- +
+
Misc
@@ -814,3 +821,823 @@ def warn(message, ...) assert_equivalent_html expected, @builder.errors_on(:email, custom_class: "custom-error-class") end end + +class LegacyBootstrapFormTest < ActionView::TestCase + include BootstrapForm::ActionViewExtensions::FormHelper + + setup do + setup_test_fixture + Rails.application.config.bootstrap_form.group_around_collections = false + end + + teardown do + Rails.application.config.bootstrap_form.group_around_collections = true + end + + test "default-style forms" do + expected = <<~HTML +
+
+ HTML + assert_equivalent_html expected, bootstrap_form_for(@user) { |_f| nil } + end + + test "default-style form fields layout horizontal" do + expected = <<~HTML +
+
+ +
+ +
+
+
+ + + +
+
+ +
+
+ + +
+
+ + +
+
+
+
+ +
+ +
+
+
+ HTML + + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + actual = bootstrap_form_for(@user) do |f| + concat(f.email_field(:email, layout: :horizontal)) + concat(f.check_box(:terms, label: "I agree to the terms")) + concat(f.collection_radio_buttons(:misc, collection, :id, :street, layout: :horizontal)) + concat(f.select(:status, [["activated", 1], ["blocked", 2]], layout: :horizontal)) + end + + assert_equivalent_html expected, actual + # See the rendered output at: https://www.bootply.com/S2WFzEYChf + end + + test "default-style form fields layout inline" do + expected = <<~HTML +
+
+ + +
+
+ + + +
+
+ +
+ + +
+
+ + +
+
+
+ + +
+
+ HTML + + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + actual = bootstrap_form_for(@user) do |f| + concat(f.email_field(:email, layout: :inline)) + concat(f.check_box(:terms, label: "I agree to the terms", inline: true)) + concat(f.collection_radio_buttons(:misc, collection, :id, :street, layout: :inline)) + concat(f.select(:status, [["activated", 1], ["blocked", 2]], layout: :inline)) + end + + assert_equivalent_html expected, actual + # See the rendered output at: https://www.bootply.com/fH5sF4fcju + # Note that the baseline of the label text to the left of the two radio buttons + # isn't aligned with the text of the radio button labels. + # TODO: Align baseline better. + end + + test "default-style forms bootstrap_form_with Rails 7.1+" do + expected = <<~HTML +
+
+ HTML + assert_equivalent_html expected, bootstrap_form_with(model: @user) { |_f| nil } + end + + test "inline-style forms" do + expected = <<~HTML +
+
+ + +
+
+
+ + + +
+
+
+ +
+ + +
+
+ + +
+
+
+ + +
+
+ HTML + + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + actual = bootstrap_form_for(@user, layout: :inline) do |f| + concat(f.email_field(:email)) + concat(f.check_box(:terms, label: "I agree to the terms")) + concat(f.collection_radio_buttons(:misc, collection, :id, :street)) + concat(f.select(:status, [["activated", 1], ["blocked", 2]])) + end + + assert_equivalent_html expected, actual + end + + class WarningFormBuilder < BootstrapForm::FormBuilder + cattr_accessor :instance + attr_reader :warnings + + def self.new(...) + self.instance = super + end + + def warn(message, ...) + @warnings ||= [] + @warnings << message + end + end + + test "old default layout gives warnings" do + expected = <<~HTML +
+
+ + +
+
+ HTML + assert_equivalent_html expected, + bootstrap_form_for(@user, builder: WarningFormBuilder, layout: :default) { |f| + f.email_field :email, layout: :default + } + assert_equal [ + "Layout `:default` is deprecated, use `:vertical` instead.", + "Layout `:default` is deprecated, use `:vertical` instead." + ], WarningFormBuilder.instance.warnings + end + + test "given role attribute should not be covered by default role attribute" do + expected = <<~HTML +
+
+ HTML + assert_equivalent_html expected, bootstrap_form_for(@user, html: { role: "not-a-form" }) { |_f| nil } + end + + test "allows to set blank default form attributes via configuration" do + BootstrapForm.config.stubs(:default_form_attributes).returns({}) + expected = <<~HTML +
+
+ HTML + assert_equivalent_html expected, bootstrap_form_for(@user) { |_f| nil } + end + + test "allows to set custom default form attributes via configuration" do + BootstrapForm.config.stubs(:default_form_attributes).returns(foo: "bar") + expected = <<~HTML +
+
+ HTML + assert_equivalent_html expected, bootstrap_form_for(@user) { |_f| nil } + end + + test "bootstrap_form_tag acts like a form tag" do + expected = <<~HTML +
+
+ + +
+
+ HTML + assert_equivalent_html expected, + bootstrap_form_tag(url: "/users") { |f| f.text_field :email, label: "Your Email" } + end + + test "bootstrap_form_for does not clobber custom options" do + expected = <<~HTML +
+
+ + +
+
+ HTML + assert_equivalent_html expected, bootstrap_form_for(@user) { |f| f.text_field :email, name: "NAME", id: "ID" } + end + + test "bootstrap_form_tag does not clobber custom options" do + expected = <<~HTML +
+
+ + +
+
+ HTML + assert_equivalent_html expected, bootstrap_form_tag(url: "/users") { |f| f.text_field :email, name: "NAME", id: "ID" } + end + + test "bootstrap_form_tag allows an empty name for checkboxes" do + expected = <<~HTML +
+
+ + + +
+
+ HTML + assert_equivalent_html expected, bootstrap_form_tag(url: "/users") { |f| f.check_box :misc } + end + + test "errors display correctly and inline_errors are turned off by default when label_errors is true" do + @user.email = nil + assert @user.invalid? + + expected = <<~HTML +
+
+ + +
+
+ HTML + assert_equivalent_html expected, bootstrap_form_for(@user, label_errors: true) { |f| f.text_field :email } + end + + test "errors display correctly and inline_errors can also be on when label_errors is true" do + @user.email = nil + assert @user.invalid? + + expected = <<~HTML +
+
+ + +
can't be blank, is too short (minimum is 5 characters) +
+ + HTML + assert_equivalent_html expected, bootstrap_form_for(@user, label_errors: true, inline_errors: true) { |f| f.text_field :email } + end + + test "label error messages use humanized attribute names" do + I18n.backend.store_translations(:en, activerecord: { attributes: { user: { email: "Your e-mail address" } } }) + + @user.email = nil + assert @user.invalid? + + expected = <<~HTML +
+
+ + +
can't be blank, is too short (minimum is 5 characters)
+
+
+ HTML + assert_equivalent_html expected, bootstrap_form_for(@user, label_errors: true, inline_errors: true) { |f| f.text_field :email } + ensure + I18n.backend.store_translations(:en, activerecord: { attributes: { user: { email: nil } } }) + end + + test "alert message is wrapped correctly" do + @user.email = nil + assert @user.invalid? + + expected = <<~HTML +
+

Please fix the following errors:

+
    +
  • Email can't be blank
  • +
  • Email is too short (minimum is 5 characters)
  • +
  • Terms must be accepted
  • +
+
+ HTML + assert_equivalent_html expected, @builder.alert_message("Please fix the following errors:") + end + + test "changing the class name for the alert message" do + @user.email = nil + assert @user.invalid? + + expected = <<~HTML +
+

Please fix the following errors:

+
    +
  • Email can't be blank
  • +
  • Email is too short (minimum is 5 characters)
  • +
  • Terms must be accepted
  • +
+
+ HTML + assert_equivalent_html expected, @builder.alert_message("Please fix the following errors:", class: "my-css-class") + end + + test "alert_message contains the error summary when inline_errors are turned off" do + @user.email = nil + assert @user.invalid? + + output = bootstrap_form_for(@user, inline_errors: false) do |f| + f.alert_message("Please fix the following errors:") + end + + expected = <<~HTML +
+
+

Please fix the following errors:

+
    +
  • Email can't be blank
  • +
  • Email is too short (minimum is 5 characters)
  • +
  • Terms must be accepted
  • +
+
+
+ HTML + assert_equivalent_html expected, output + end + + test "alert_message allows the error_summary to be turned off" do + @user.email = nil + assert @user.invalid? + + output = bootstrap_form_for(@user, inline_errors: false) do |f| + f.alert_message("Please fix the following errors:", error_summary: false) + end + + expected = <<~HTML +
+
Please fix the following errors:
+
+ HTML + assert_equivalent_html expected, output + end + + test "alert_message allows the error_summary to be turned on with inline_errors also turned on" do + @user.email = nil + assert @user.invalid? + + output = bootstrap_form_for(@user, inline_errors: true) do |f| + f.alert_message("Please fix the following errors:", error_summary: true) + end + + expected = <<~HTML +
+
+

Please fix the following errors:

+
    +
  • Email can't be blank
  • +
  • Email is too short (minimum is 5 characters)
  • +
  • Terms must be accepted
  • +
+
+
+ HTML + assert_equivalent_html expected, output + end + + test "error_summary returns an unordered list of errors" do + @user.email = nil + assert @user.invalid? + + expected = <<~HTML +
    +
  • Email can't be blank
  • +
  • Email is too short (minimum is 5 characters)
  • +
  • Terms must be accepted
  • +
+ HTML + assert_equivalent_html expected, @builder.error_summary + end + + test "error_summary returns nothing if no errors" do + @user.terms = true + assert @user.valid? + + assert_nil @builder.error_summary + end + + test "errors_on renders the errors for a specific attribute when invalid" do + @user.email = nil + assert @user.invalid? + + expected = <<~HTML +
Email can't be blank, Email is too short (minimum is 5 characters)
+ HTML + assert_equivalent_html expected, @builder.errors_on(:email) + end + + test "custom label width for horizontal forms" do + expected = <<~HTML +
+
+ +
+ +
+
+
+ HTML + assert_equivalent_html expected, + bootstrap_form_for(@user, layout: :horizontal) { |f| f.email_field :email, label_col: "col-sm-1" } + end + + test "offset for form group without label respects label width for horizontal forms" do + expected = <<~HTML +
+
+
+ +
+
+
+ HTML + assert_equivalent_html expected, + bootstrap_form_for(@user, layout: :horizontal, label_col: "col-md-2", + control_col: "col-md-10") { |f| f.form_group { f.submit } } + end + + test "offset for form group without label respects multiple label widths for horizontal forms" do + expected = <<~HTML +
+
+
+ +
+
+
+ HTML + assert_equivalent_html expected, + bootstrap_form_for(@user, layout: :horizontal, label_col: %w[col-sm-4 col-md-2], + control_col: "col-sm-8 col-md-10") { |f| f.form_group { f.submit } } + end + + test "custom input width for horizontal forms" do + expected = <<~HTML +
+
+ +
+ +
+
+
+ HTML + assert_equivalent_html expected, + bootstrap_form_for(@user, layout: :horizontal) { |f| f.email_field :email, control_col: "col-sm-5" } + end + + test "additional input col class" do + expected = <<~HTML +
+
+ +
+ +
+
+
+ HTML + actual = bootstrap_form_for(@user, + layout: :horizontal) { |f| f.email_field :email, add_control_col_class: "custom-class" } + assert_equivalent_html expected, actual + end + + test "the field contains the error and is not wrapped in div.field_with_errors when bootstrap_form_for is used" do + @user.email = nil + assert @user.invalid? + + output = bootstrap_form_for(@user) do |f| + f.text_field(:email, help: "This is required") + end + + expected = <<~HTML +
+
+ + +
can't be blank, is too short (minimum is 5 characters)
+ This is required +
+
+ HTML + assert_equivalent_html expected, output + end + + test "the field is wrapped with div.field_with_errors when form_for is used" do + @user.email = nil + assert @user.invalid? + + output = form_for(@user, builder: BootstrapForm::FormBuilder) do |f| + f.text_field(:email, help: "This is required") + end + + expected = <<~HTML +
+
+
+ +
+
+ +
+
can't be blank, is too short (minimum is 5 characters)
+ This is required +
+
+ HTML + assert_equivalent_html expected, output + end + + test "help is preserved when inline_errors: false is passed to bootstrap_form_for" do + @user.email = nil + assert @user.invalid? + + output = bootstrap_form_for(@user, inline_errors: false) do |f| + f.text_field(:email, help: "This is required") + end + + expected = <<~HTML +
+
+ + + This is required +
+
+ HTML + assert_equivalent_html expected, output + end + + test "help translations do not escape HTML when _html is appended to the name" do + I18n.backend.store_translations(:en, activerecord: { help: { user: { email_html: "This is useful help" } } }) + + output = bootstrap_form_for(@user) do |f| + f.text_field(:email) + end + + expected = <<~HTML +
+
+ + + This is useful help +
+
+ HTML + assert_equivalent_html expected, output + ensure + I18n.backend.store_translations(:en, activerecord: { help: { user: { email_html: nil } } }) + end + + test "allows the form object to be nil" do + builder = BootstrapForm::FormBuilder.new :other_model, nil, self, {} + expected = <<~HTML +
+ + +
+ HTML + assert_equivalent_html expected, builder.text_field(:email) + end + + test "errors_on hide attribute name in message" do + @user.email = nil + assert @user.invalid? + + expected = '
can\'t be blank, is too short (minimum is 5 characters)
' + + assert_equivalent_html expected, @builder.errors_on(:email, hide_attribute_name: true) + end + + test "errors_on use custom CSS classes" do + @user.email = nil + assert @user.invalid? + + expected = '
Email can\'t be blank, Email is too short (minimum is 5 characters)
' + + assert_equivalent_html expected, @builder.errors_on(:email, custom_class: "custom-error-class") + end + + test "horizontal-style forms" do + expected = <<~HTML +
+
+ +
+ +
+
+
+
+
+ + + +
+
+
+
+ +
+
+ + +
+
+ + +
+
+
+
+ +
+ +
+
+
+ HTML + + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + actual = bootstrap_form_for(@user, layout: :horizontal) do |f| + concat(f.email_field(:email)) + concat(f.check_box(:terms, label: "I agree to the terms")) + concat(f.collection_radio_buttons(:misc, collection, :id, :street)) + concat(f.select(:status, [["activated", 1], ["blocked", 2]])) + end + + assert_equivalent_html expected, actual + end + + test "horizontal-style form fields layout vertical" do + expected = <<~HTML +
+
+ + +
+
+
+
+ + + +
+
+
+
+ +
+ + +
+
+ + +
+
+
+ + +
+
+ HTML + + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + actual = bootstrap_form_for(@user, layout: :horizontal) do |f| + concat(f.email_field(:email, layout: :vertical)) + concat(f.check_box(:terms, label: "I agree to the terms")) + concat(f.collection_radio_buttons(:misc, collection, :id, :street, layout: :vertical)) + concat(f.select(:status, [["activated", 1], ["blocked", 2]], layout: :vertical)) + end + + assert_equivalent_html expected, actual + # See the rendered output at: https://www.bootply.com/4f23be1nLn + end + + test "horizontal-style form fields layout inline" do + expected = <<~HTML +
+
+ + +
+
+
+
+ + + +
+
+
+
+ +
+ + +
+
+ + +
+
+
+ + +
+
+ HTML + + collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] + actual = bootstrap_form_for(@user, layout: :horizontal) do |f| + concat(f.email_field(:email, layout: :inline)) + concat(f.check_box(:terms, label: "I agree to the terms", inline: true)) + concat(f.collection_radio_buttons(:misc, collection, :id, :street, layout: :inline)) + concat(f.select(:status, [["activated", 1], ["blocked", 2]], layout: :inline)) + end + + assert_equivalent_html expected, actual + # See the rendered output here: https://www.bootply.com/Qby9FC9d3u# + end + + test "existing styles aren't clobbered when specifying a form style" do + expected = <<~HTML +
+
+ +
+ +
+
+
+ HTML + assert_equivalent_html expected, + bootstrap_form_for(@user, layout: :horizontal, html: { class: "my-style" }) { |f| f.email_field :email } + end +end diff --git a/test/bootstrap_radio_button_test.rb b/test/bootstrap_radio_button_test.rb index c7543bdf..50b4d6fc 100644 --- a/test/bootstrap_radio_button_test.rb +++ b/test/bootstrap_radio_button_test.rb @@ -140,259 +140,6 @@ class BootstrapRadioButtonTest < ActionView::TestCase @builder.radio_button(:misc, "1", label: "This is a radio button", inline: true, label_class: "btn") end - test "collection_radio_buttons renders the form_group correctly" do - collection = [Address.new(id: 1, street: "Foobar")] - expected = <<~HTML -
- -
- - -
- With a help! -
- HTML - - assert_equivalent_html expected, - @builder.collection_radio_buttons(:misc, collection, :id, :street, - label: "This is a radio button collection", help: "With a help!") - end - - test "collection_radio_buttons renders multiple radios correctly" do - collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] - expected = <<~HTML -
- -
- - -
-
- - -
-
- HTML - - assert_equivalent_html expected, @builder.collection_radio_buttons(:misc, collection, :id, :street) - end - - test "collection_radio_buttons renders multiple radios with error correctly" do - @user.errors.add(:misc, "error for test") - collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] - expected = <<~HTML -
-
- -
- - -
-
- - -
error for test
-
-
-
- HTML - - actual = bootstrap_form_for(@user) do |f| - f.collection_radio_buttons(:misc, collection, :id, :street) - end - assert_equivalent_html expected, actual - end - - test "collection_radio_buttons renders inline radios correctly" do - collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] - expected = <<~HTML -
- -
- - -
-
- - -
-
- HTML - - assert_equivalent_html expected, @builder.collection_radio_buttons(:misc, collection, :id, :street, inline: true) - end - - test "collection_radio_buttons renders with checked option correctly" do - collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] - expected = <<~HTML -
- -
- - -
-
- - -
-
- HTML - - assert_equivalent_html expected, @builder.collection_radio_buttons(:misc, collection, :id, :street, checked: 1) - end - - test "collection_radio_buttons renders label defined by Proc correctly" do - collection = [Address.new(id: 1, street: "Foobar")] - expected = <<~HTML -
- -
- - -
- With a help! -
- HTML - - assert_equivalent_html expected, - @builder.collection_radio_buttons(:misc, collection, :id, proc { |a| a.street.reverse }, - label: "This is a radio button collection", help: "With a help!") - end - - test "collection_radio_buttons renders value defined by Proc correctly" do - collection = [Address.new(id: 1, street: "Foobar")] - expected = <<~HTML -
- -
- - -
- With a help! -
- HTML - - assert_equivalent_html expected, - @builder.collection_radio_buttons(:misc, collection, proc { |a| "address_#{a.id}" }, - :street, label: "This is a radio button collection", - help: "With a help!") - end - - test "collection_radio_buttons renders multiple radios with label defined by Proc correctly" do - collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] - expected = <<~HTML -
- -
- - -
-
- - -
-
- HTML - - assert_equivalent_html expected, @builder.collection_radio_buttons(:misc, collection, :id, proc { |a| a.street.reverse }) - end - - test "collection_radio_buttons renders multiple radios with value defined by Proc correctly" do - collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] - expected = <<~HTML -
- -
- - -
-
- - -
-
- HTML - - assert_equivalent_html expected, @builder.collection_radio_buttons(:misc, collection, proc { |a| "address_#{a.id}" }, :street) - end - - test "collection_radio_buttons renders label defined by lambda correctly" do - collection = [Address.new(id: 1, street: "Foobar")] - expected = <<~HTML -
- -
- - -
- With a help! -
- HTML - - assert_equivalent_html expected, - @builder.collection_radio_buttons(:misc, collection, :id, ->(a) { a.street.reverse }, - label: "This is a radio button collection", help: "With a help!") - end - - test "collection_radio_buttons renders value defined by lambda correctly" do - collection = [Address.new(id: 1, street: "Foobar")] - expected = <<~HTML -
- -
- - -
- With a help! -
- HTML - - assert_equivalent_html expected, - @builder.collection_radio_buttons(:misc, collection, ->(a) { "address_#{a.id}" }, - :street, label: "This is a radio button collection", - help: "With a help!") - end - - test "collection_radio_buttons renders multiple radios with label defined by lambda correctly" do - collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] - expected = <<~HTML -
- -
- - -
-
- - -
-
- HTML - - assert_equivalent_html expected, @builder.collection_radio_buttons(:misc, collection, :id, ->(a) { a.street.reverse }) - end - - test "collection_radio_buttons renders multiple radios with value defined by lambda correctly" do - collection = [Address.new(id: 1, street: "Foo"), Address.new(id: 2, street: "Bar")] - expected = <<~HTML -
- -
- - -
-
- - -
-
- HTML - - assert_equivalent_html expected, @builder.collection_radio_buttons(:misc, collection, ->(a) { "address_#{a.id}" }, :street) - end - test "radio button skip label" do expected = <<~HTML