Skip to content

Commit fbce97a

Browse files
authored
Move validation errors for check boxes and radio buttons into last div.form-check. (#448)
* Test from 419-check-radio-errors. * Tests from 419-check-radio-errors-b and 419-check-radio-errors-c. * Plain check_box working. * radio_button error_message. * Collections radios and checks working. * Correct options to Rails helper. * Upgrade doc first draft. * Fix tests and code for file and date/time selects. * Update CHANGELOG. * Remove commented-out test This test case was removed as it was something that's too difficult to support with Bootstrap 4. test 'form_group renders the "error" class and message corrrectly when object is invalid' do @user.email = nil assert @user.invalid? output = @builder.form_group :email do %{<p class="form-control-static">Bar</p>}.html_safe end expected = <<-HTML.strip_heredoc <div class="form-group"> <p class="form-control-static">Bar</p> <div class="invalid-feedback">can't be blank, is too short (minimum is 5 characters)</div> </div> HTML assert_equivalent_xml expected, output end In its place, we've recommended a workaround in the UPGRADE-4.0 doc, which is to output the error message manually. This workaround has a corresponding test to verify its correctness. * Error message must be sibling of input
2 parents 51fb089 + 7c62989 commit fbce97a

File tree

9 files changed

+362
-57
lines changed

9 files changed

+362
-57
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ In addition to these necessary markup changes, the bootstrap_form API itself has
1818
* `hide_label: true` and `skip_label: true` on individual check boxes and radio buttons apply Bootstrap 4 markup. This means the appearance of a page may change if you're upgrading from the Bootstrap 3 version of `bootstrap_form`, and you used `check_box` or `radio_button` with either of those options
1919
* `static_control` will no longer properly show error messages. This is the result of bootstrap changes.
2020
* `static_control` will also no longer accept a block, use the `value` option instead.
21-
* Your contribution here!
21+
* `form_group` with a block that produces arbitrary text needs to be modified to produce validation error messages (see the UPGRADE-4.0 document). [@lcreid](https://github.com/lcreid).
22+
* `form_group` with a block that contains more than one `check_box` or `radio_button` needs to be modified to produce validation error messages (see the UPGRADE-4.0 document). [@lcreid](https://github.com/lcreid).
2223
* [#456](https://github.com/bootstrap-ruby/bootstrap_form/pull/456): Fix label `for` attribute when passing non-english characters using `collection_check_boxes` - [@ug0](https://github.com/ug0).
2324
* [#449](https://github.com/bootstrap-ruby/bootstrap_form/pull/449): Bootstrap 4 no longer mixes in `.row` in `.form-group`. `bootstrap_form` adds `.row` to `div.form-group` when layout is horizontal.
25+
* Your contribution here!
2426

2527
### New features
2628

@@ -32,6 +34,7 @@ In addition to these necessary markup changes, the bootstrap_form API itself has
3234
* Adds support for `label_as_placeholder` option, which will set the label text as an input fields placeholder (and hiding the label for sr_only).
3335
* [#449](https://github.com/bootstrap-ruby/bootstrap_form/pull/449): Passing `.form-row` overrides default `.form-group.row` in horizontal layouts.
3436
* Added an option to the `submit` (and `primary`, by transitivity) form tag helper, `render_as_button`, which when truthy makes the submit button render as a button instead of an input. This allows you to easily provide further styling to your form submission buttons, without requiring you to reinvent the wheel and use the `button` helper (and having to manually insert the typical Bootstrap classes). - [@jsaraiva](https://github.com/jsaraiva).
37+
* Add `:error_message` option to `check_box` and `radio_button`, so they can output validation error messages if needed. [@lcreid](https://github.com/lcreid).
3538
* Your contribution here!
3639

3740
### Bugfixes

UPGRADE-4.0.md

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,63 @@
1+
# Upgrading to `bootstrap_form` 4.0
2+
We made every effort to make the upgrade from `bootstrap_form` v2.7 (Bootstrap 3) to `bootstrap_form` v4.0 (Bootstrap 4) as easy as possible. However, Bootstrap 4 is fundamentally different from Bootstrap 3, so some changes may be necessary in your code.
3+
4+
## Bootstrap 4 Changes
5+
If you made use of Bootstrap classes or Javascript, you should read the [Bootstrap 4 migration guide](https://getbootstrap.com/docs/4.0/migration/).
6+
7+
## Validation Error Messages
8+
With Bootstrap 4, in order for validation error messages to display, the message has to be a sibling of the `input` tag, and the `input` tag has to have the `.is-invalid` class. This was different from Bootstrap 3, and forced some changes to `bootstrap_form` that may affect programs that used `bootstrap_form` v2.7.
9+
10+
### Arbitrary Text in `form_group` Blocks
11+
In `bootstrap_form` v2.7, it was possible to write something like this:
12+
```
13+
<%= bootstrap_form_for(@user) do |f| %>
14+
<%= f.form_group(:email) do %>
15+
<p class="form-control-static">Bar</p>
16+
<%= end %>
17+
<%= end %>
18+
```
19+
and, if `@user.email` had validation errors, it would render:
20+
```
21+
<div class="form-group has-error">
22+
<p class="form-control-static">Bar</p>
23+
<span class="help-block">can't be blank, is too short (minimum is 5 characters)</span>
24+
</div>
25+
```
26+
which would show an error message in red.
27+
28+
That doesn't work in Bootstrap 4. Outputting error messages had to be moved to accommodate other changes, so `form_group` no longer outputs error messages unless whatever is inside the block is a `bootstrap_form` helper.
29+
30+
One way to make the above behave the same in `bootstrap_form` v4.0 is to write it like this:
31+
32+
```
33+
<%= bootstrap_form_for(@user) do |f| %>
34+
<%= f.form_group(:email) do %>
35+
<p class="form-control-plaintext">Bar</p>
36+
<%= content_tag(:div, @user.errors[:email].join(", "), class: "invalid-feedback", style: "display: block;") unless @user.errors[:email].empty? %>
37+
<%= end %>
38+
<%= end %>
39+
```
40+
41+
### Check Boxes and Radio Buttons
42+
Bootstrap 4 marks up check boxes and radio buttons differently. In particular, Bootstrap 4 wraps the `input` and `label` tags in a `div.form-check` tag. Because validation error messages have to be siblings of the `input` tag, there is now an `error_message` option to `check_box` and `radio_button` to cause them to put the validation error messages inside the `div.form-check`.
43+
44+
This change is mostly invisible to existing programs:
45+
46+
- Since the default for `error_message` is false, use of `check_box` and `radio_button` all by themselves behaves the same as in `bootstrap_form` v2.7
47+
- All the `collection*` helpers that output radio buttons and check boxes arrange to produce the validation error message on the last check box or radio button of the group, like `bootstrap_form` v2.7 did
48+
49+
There is one situation where an existing program will have to change. When rendering one or more check boxes or radio buttons inside a `form_group` block, the last call to `check_box` or `radio_button` in the block will have to have `error_message: true` added to its parameters, like this:
50+
51+
```
52+
<%= bootstrap_form_for(@user) do |f| %>
53+
<%= f.form_group(:education) do %>
54+
<%= f.radio_button(:misc, "primary school") %>
55+
<%= f.radio_button(:misc, "high school") %>
56+
<%= f.radio_button(:misc, "university", error_message: true) %>
57+
<%= end %>
58+
<%= end %>
59+
```
60+
161
## `form-group` and Horizontal Forms
262
In Bootstrap 3, `.form-group` mixed in `.row`. In Bootstrap 4, it doesn't. So `bootstrap_form` automatically adds `.row` to the `div.form-group`s that it creates, if the form group is in a horizontal layout. When migrating forms from the Bootstrap 3 version of `bootstrap_form` to the Bootstrap 4 version, check all horizontal forms to be sure they're being rendered properly.
363

@@ -17,4 +77,3 @@ bootstrap_form_for(@user, layout: "horizontal") do |f|
1777
...
1878
end
1979
end
20-
```

lib/bootstrap_form/form_builder.rb

Lines changed: 51 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ def initialize(object_name, object, template, options)
3838

3939
define_method(with_method_name) do |name, options = {}|
4040
form_group_builder(name, options) do
41-
send(without_method_name, name, options)
41+
prepend_and_append_input(name, options) do
42+
send(without_method_name, name, options)
43+
end
4244
end
4345
end
4446

@@ -50,28 +52,32 @@ def initialize(object_name, object, template, options)
5052
without_method_name = "#{method_name}_without_bootstrap"
5153

5254
define_method(with_method_name) do |name, options = {}, html_options = {}|
53-
prevent_prepend_and_append!(options)
5455
form_group_builder(name, options, html_options) do
55-
content_tag(:div, send(without_method_name, name, options, html_options), class: control_specific_class(method_name))
56+
content_tag(:div, class: control_specific_class(method_name)) do
57+
input_with_error(name) do
58+
send(without_method_name, name, options, html_options)
59+
end
60+
end
5661
end
5762
end
5863

5964
bootstrap_method_alias method_name
6065
end
6166

6267
def file_field_with_bootstrap(name, options = {})
63-
prevent_prepend_and_append!(options)
6468
options = options.reverse_merge(control_class: "custom-file-input")
6569
form_group_builder(name, options) do
6670
content_tag(:div, class: "custom-file") do
67-
placeholder = options.delete(:placeholder) || "Choose file"
68-
placeholder_opts = { class: "custom-file-label" }
69-
placeholder_opts[:for] = options[:id] if options[:id].present?
70-
71-
input = file_field_without_bootstrap(name, options)
72-
placeholder_label = label(name, placeholder, placeholder_opts)
73-
concat(input)
74-
concat(placeholder_label)
71+
input_with_error(name) do
72+
placeholder = options.delete(:placeholder) || "Choose file"
73+
placeholder_opts = { class: "custom-file-label" }
74+
placeholder_opts[:for] = options[:id] if options[:id].present?
75+
76+
input = file_field_without_bootstrap(name, options)
77+
placeholder_label = label(name, placeholder, placeholder_opts)
78+
concat(input)
79+
concat(placeholder_label)
80+
end
7581
end
7682
end
7783
end
@@ -80,49 +86,52 @@ def file_field_with_bootstrap(name, options = {})
8086

8187
def select_with_bootstrap(method, choices = nil, options = {}, html_options = {}, &block)
8288
form_group_builder(method, options, html_options) do
83-
select_without_bootstrap(method, choices, options, html_options, &block)
89+
prepend_and_append_input(method, options) do
90+
select_without_bootstrap(method, choices, options, html_options, &block)
91+
end
8492
end
8593
end
8694

8795
bootstrap_method_alias :select
8896

8997
def collection_select_with_bootstrap(method, collection, value_method, text_method, options = {}, html_options = {})
90-
prevent_prepend_and_append!(options)
9198
form_group_builder(method, options, html_options) do
92-
collection_select_without_bootstrap(method, collection, value_method, text_method, options, html_options)
99+
input_with_error(method) do
100+
collection_select_without_bootstrap(method, collection, value_method, text_method, options, html_options)
101+
end
93102
end
94103
end
95104

96105
bootstrap_method_alias :collection_select
97106

98107
def grouped_collection_select_with_bootstrap(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
99-
prevent_prepend_and_append!(options)
100108
form_group_builder(method, options, html_options) do
101-
grouped_collection_select_without_bootstrap(method, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options)
109+
input_with_error(method) do
110+
grouped_collection_select_without_bootstrap(method, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options)
111+
end
102112
end
103113
end
104114

105115
bootstrap_method_alias :grouped_collection_select
106116

107117
def time_zone_select_with_bootstrap(method, priority_zones = nil, options = {}, html_options = {})
108-
prevent_prepend_and_append!(options)
109118
form_group_builder(method, options, html_options) do
110-
time_zone_select_without_bootstrap(method, priority_zones, options, html_options)
119+
input_with_error(method) do
120+
time_zone_select_without_bootstrap(method, priority_zones, options, html_options)
121+
end
111122
end
112123
end
113124

114125
bootstrap_method_alias :time_zone_select
115126

116127
def check_box_with_bootstrap(name, options = {}, checked_value = "1", unchecked_value = "0", &block)
117-
prevent_prepend_and_append!(options)
118128
options = options.symbolize_keys!
119-
check_box_options = options.except(:label, :label_class, :help, :inline, :custom, :hide_label, :skip_label)
129+
check_box_options = options.except(:label, :label_class, :error_message, :help, :inline, :custom, :hide_label, :skip_label)
120130
check_box_classes = [check_box_options[:class]]
121131
check_box_classes << "position-static" if options[:skip_label] || options[:hide_label]
132+
check_box_classes << "is-invalid" if has_error?(name)
122133
if options[:custom]
123-
validation = nil
124-
validation = "is-invalid" if has_error?(name)
125-
check_box_options[:class] = (["custom-control-input", validation] + check_box_classes).compact.join(' ')
134+
check_box_options[:class] = (["custom-control-input"] + check_box_classes).compact.join(' ')
126135
else
127136
check_box_options[:class] = (["form-check-input"] + check_box_classes).compact.join(' ')
128137
end
@@ -148,45 +157,48 @@ def check_box_with_bootstrap(name, options = {}, checked_value = "1", unchecked_
148157
div_class.append("custom-control-inline") if layout_inline?(options[:inline])
149158
label_class = label_classes.prepend("custom-control-label").compact.join(" ")
150159
content_tag(:div, class: div_class.compact.join(" ")) do
151-
if options[:skip_label]
160+
html = if options[:skip_label]
152161
checkbox_html
153162
else
154163
# TODO: Notice we don't seem to pass the ID into the custom control.
155164
checkbox_html.concat(label(label_name, label_description, class: label_class))
156165
end
166+
html.concat(generate_error(name)) if options[:error_message]
167+
html
157168
end
158169
else
159170
wrapper_class = "form-check"
160171
wrapper_class += " form-check-inline" if layout_inline?(options[:inline])
161172
label_class = label_classes.prepend("form-check-label").compact.join(" ")
162173
content_tag(:div, class: wrapper_class) do
163-
if options[:skip_label]
174+
html = if options[:skip_label]
164175
checkbox_html
165176
else
166177
checkbox_html
167178
.concat(label(label_name,
168179
label_description,
169180
{ class: label_class }.merge(options[:id].present? ? { for: options[:id] } : {})))
170181
end
182+
html.concat(generate_error(name)) if options[:error_message]
183+
html
171184
end
172185
end
173186
end
174187

175188
bootstrap_method_alias :check_box
176189

177190
def radio_button_with_bootstrap(name, value, *args)
178-
prevent_prepend_and_append!(options)
179191
options = args.extract_options!.symbolize_keys!
180-
radio_options = options.except(:label, :label_class, :help, :inline, :custom, :hide_label, :skip_label)
192+
radio_options = options.except(:label, :label_class, :error_message, :help, :inline, :custom, :hide_label, :skip_label)
181193
radio_classes = [options[:class]]
182194
radio_classes << "position-static" if options[:skip_label] || options[:hide_label]
195+
radio_classes << "is-invalid" if has_error?(name)
183196
if options[:custom]
184197
radio_options[:class] = radio_classes.prepend("custom-control-input").compact.join(' ')
185198
else
186199
radio_options[:class] = radio_classes.prepend("form-check-input").compact.join(' ')
187200
end
188-
args << radio_options
189-
radio_html = radio_button_without_bootstrap(name, value, *args)
201+
radio_html = radio_button_without_bootstrap(name, value, radio_options)
190202

191203
disabled_class = " disabled" if options[:disabled]
192204
label_classes = [options[:label_class]]
@@ -197,32 +209,35 @@ def radio_button_with_bootstrap(name, value, *args)
197209
div_class.append("custom-control-inline") if layout_inline?(options[:inline])
198210
label_class = label_classes.prepend("custom-control-label").compact.join(" ")
199211
content_tag(:div, class: div_class.compact.join(" ")) do
200-
if options[:skip_label]
212+
html = if options[:skip_label]
201213
radio_html
202214
else
203215
# TODO: Notice we don't seem to pass the ID into the custom control.
204216
radio_html.concat(label(name, options[:label], value: value, class: label_class))
205217
end
218+
html.concat(generate_error(name)) if options[:error_message]
219+
html
206220
end
207221
else
208222
wrapper_class = "form-check"
209223
wrapper_class += " form-check-inline" if layout_inline?(options[:inline])
210224
label_class = label_classes.prepend("form-check-label").compact.join(" ")
211225
content_tag(:div, class: "#{wrapper_class}#{disabled_class}") do
212-
if options[:skip_label]
226+
html = if options[:skip_label]
213227
radio_html
214228
else
215229
radio_html
216230
.concat(label(name, options[:label], { value: value, class: label_class }.merge(options[:id].present? ? { for: options[:id] } : {})))
217231
end
232+
html.concat(generate_error(name)) if options[:error_message]
233+
html
218234
end
219235
end
220236
end
221237

222238
bootstrap_method_alias :radio_button
223239

224240
def collection_check_boxes_with_bootstrap(*args)
225-
prevent_prepend_and_append!(options)
226241
html = inputs_collection(*args) do |name, value, options|
227242
options[:multiple] = true
228243
check_box(name, options, value, nil)
@@ -233,7 +248,6 @@ def collection_check_boxes_with_bootstrap(*args)
233248
bootstrap_method_alias :collection_check_boxes
234249

235250
def collection_radio_buttons_with_bootstrap(*args)
236-
prevent_prepend_and_append!(options)
237251
inputs_collection(*args) do |name, value, options|
238252
radio_button(name, value, options)
239253
end
@@ -253,7 +267,7 @@ def form_group(*args, &block)
253267

254268
content_tag(:div, options.except(:append, :id, :label, :help, :icon, :input_group_class, :label_col, :control_col, :layout, :prepend)) do
255269
label = generate_label(options[:id], name, options[:label], options[:label_col], options[:layout]) if options[:label]
256-
control = prepend_and_append_input(name, options, &block).to_s
270+
control = capture(&block)
257271

258272
help = options[:help]
259273
help_text = generate_help(name, help).to_s
@@ -405,10 +419,6 @@ def form_group_builder(method, options, html_options = nil)
405419
class: wrapper_class
406420
}
407421

408-
form_group_options[:append] = options.delete(:append) if options[:append]
409-
form_group_options[:prepend] = options.delete(:prepend) if options[:prepend]
410-
form_group_options[:input_group_class] = options.delete(:input_group_class) if options[:input_group_class]
411-
412422
if wrapper_options.is_a?(Hash)
413423
form_group_options.merge!(wrapper_options)
414424
end
@@ -515,7 +525,7 @@ def inputs_collection(name, collection, value, text, options = {}, &block)
515525
form_group_builder(name, options) do
516526
inputs = ""
517527

518-
collection.each do |obj|
528+
collection.each_with_index do |obj, i|
519529
input_options = options.merge(label: text.respond_to?(:call) ? text.call(obj) : obj.send(text))
520530

521531
input_value = value.respond_to?(:call) ? value.call(obj) : obj.send(value)
@@ -527,7 +537,7 @@ def inputs_collection(name, collection, value, text, options = {}, &block)
527537
end
528538

529539
input_options.delete(:class)
530-
inputs << block.call(name, input_value, input_options)
540+
inputs << block.call(name, input_value, input_options.merge(error_message: i == collection.size - 1))
531541
end
532542

533543
inputs.html_safe

lib/bootstrap_form/helpers/bootstrap.rb

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,9 @@ def prepend_and_append_input(name, options, &block)
8989
input
9090
end
9191

92-
# Some helpers don't currently accept prepend and append. However, it's not
93-
# clear if that's corrent. In the meantime, strip to options before calling
94-
# methods that don't accept prepend and append.
95-
def prevent_prepend_and_append!(options)
96-
options.delete(:append)
97-
options.delete(:prepend)
92+
def input_with_error(name, &block)
93+
input = capture(&block)
94+
input << generate_error(name)
9895
end
9996

10097
def input_group_content(content)

0 commit comments

Comments
 (0)