Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1493,38 +1493,38 @@ Generated HTML:
<form accept-charset="UTF-8" action="/users" class="new_user" id="new_user" method="post">
<div class="mb-3">
<label class="form-label required" for="user_email">Email</label>
<input class="form-control is-invalid" id="user_email" name="user[email]" required="required" type="email" value="steve.example.com">
<div class="invalid-feedback">is invalid</div>
<input aria-labelledby="user_email_feedback" class="form-control is-invalid" id="user_email" name="user[email]" required="required" type="email" value="steve.example.com">
<div class="invalid-feedback" id="user_email_feedback">is invalid</div>
</div>
<div aria-labelledby="user_misc" class="mb-3" role="group">
<div class="form-label" id="user_misc">Misc</div>
<div class="form-check">
<input checked class="form-check-input is-invalid" id="user_misc_1" name="user[misc]" type="radio" value="1">
<input aria-labelledby="user_misc_feedback" checked class="form-check-input is-invalid" id="user_misc_1" name="user[misc]" type="radio" value="1">
<label class="form-check-label" for="user_misc_1">Mind reading</label>
</div>
<div class="form-check">
<input class="form-check-input is-invalid" id="user_misc_2" name="user[misc]" type="radio" value="2">
<input aria-labelledby="user_misc_feedback" class="form-check-input is-invalid" id="user_misc_2" name="user[misc]" type="radio" value="2">
<label class="form-check-label" for="user_misc_2">Farming</label>
<div class="invalid-feedback">is invalid</div>
<div class="invalid-feedback" id="user_misc_feedback">is invalid</div>
</div>
</div>
<input id="user_preferences" name="user[preferences][]" type="hidden" value="">
<div aria-labelledby="user_preferences" class="mb-3" role="group">
<div class="form-label" id="user_preferences">Preferences</div>
<div class="form-check">
<input checked class="form-check-input is-invalid" id="user_preferences_1" name="user[preferences][]" type="checkbox" value="1">
<input aria-labelledby="user_preferences_feedback" checked class="form-check-input is-invalid" id="user_preferences_1" name="user[preferences][]" type="checkbox" value="1">
<label class="form-check-label" for="user_preferences_1">Good</label>
</div>
<div class="form-check">
<input class="form-check-input is-invalid" id="user_preferences_2" name="user[preferences][]" type="checkbox" value="2">
<input aria-labelledby="user_preferences_feedback" class="form-check-input is-invalid" id="user_preferences_2" name="user[preferences][]" type="checkbox" value="2">
<label class="form-check-label" for="user_preferences_2">Bad</label>
<div class="invalid-feedback">is invalid</div>
<div class="invalid-feedback" id="user_preferences_feedback">is invalid</div>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="user_address_attributes_street">Street</label>
<input class="form-control is-invalid" id="user_address_attributes_street" name="user[address_attributes][street]" type="text" value="Bar">
<div class="invalid-feedback">is invalid</div>
<input aria-labelledby="user_address_attributes_street_feedback" class="form-control is-invalid" id="user_address_attributes_street" name="user[address_attributes][street]" type="text" value="Bar">
<div class="invalid-feedback" id="user_address_attributes_street_feedback">is invalid</div>
</div>
</form>
```
Expand Down Expand Up @@ -1554,8 +1554,8 @@ Generated HTML:
```html
<form accept-charset="UTF-8" action="/users" class="new_user" id="new_user" method="post">
<div class="mb-3">
<label class="form-label required text-danger" for="user_email">Email is invalid</label>
<input class="form-control is-invalid" id="user_email" name="user[email]" required="required" type="email" value="steve.example.com">
<label class="form-label required text-danger" for="user_email" id="user_email_feedback">Email is invalid</label>
<input aria-labelledby="user_email_feedback" class="form-control is-invalid" id="user_email" name="user[email]" required="required" type="email" value="steve.example.com">
</div>
</form>
```
Expand Down Expand Up @@ -1652,7 +1652,7 @@ Which outputs:
```html
<form accept-charset="UTF-8" action="/users" class="new_user" id="new_user" method="post">
<input autocomplete="off" class="is-invalid" disabled type="hidden">
<div class="invalid-feedback">Email is invalid</div>
<div class="invalid-feedback" id="user_email_feedback">Email is invalid</div>
</form>
```

Expand All @@ -1673,7 +1673,7 @@ Which outputs:
```html
<form accept-charset="UTF-8" action="/users" class="new_user" id="new_user" method="post">
<input autocomplete="off" class="is-invalid" disabled type="hidden">
<div class="invalid-feedback">is invalid</div>
<div class="invalid-feedback" id="user_email_feedback">is invalid</div>
</form>
```

Expand All @@ -1692,7 +1692,7 @@ Which outputs:
```html
<form accept-charset="UTF-8" action="/users" class="new_user" id="new_user" method="post">
<input autocomplete="off" class="is-invalid" disabled type="hidden">
<div class="custom-error">Email is invalid</div>
<div class="custom-error" id="user_email_feedback">Email is invalid</div>
</form>
```

Expand Down
1 change: 1 addition & 0 deletions lib/bootstrap_form/components/labels.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def prepare_label_options(id, name, options, custom_label_col, group_layout)

options[:class] = label_classes(name, options, custom_label_col, group_layout)
options.delete(:class) if options[:class].none?
options[:id] = id.present? ? "#{id}_feedback" : field_id(name, :feedback) if error?(name) && label_errors
end
end
end
Expand Down
5 changes: 3 additions & 2 deletions lib/bootstrap_form/components/validation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,15 @@ def inline_error?(name)
error?(name) && inline_errors
end

def generate_error(name)
def generate_error(name, id)
return unless inline_error?(name)

help_text = get_error_messages(name)
help_klass = "invalid-feedback"
help_tag = :div
id = id.present? ? "#{id}_feedback" : field_id(name, :feedback)

content_tag(help_tag, help_text, class: help_klass)
content_tag(help_tag, help_text, class: help_klass, id:)
end

def get_error_messages(name)
Expand Down
2 changes: 1 addition & 1 deletion lib/bootstrap_form/form_group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def form_group_content_tag(name, field_name, without_field_name, options, html_o
html_class = control_specific_class(field_name)
html_class = "#{html_class} col-auto g-3" if @layout == :horizontal && options[:skip_inline].blank?
tag.div(class: html_class) do
input_with_error(name) do
input_with_error(name, options[:id]) do
send(without_field_name, name, options, html_options)
end
end
Expand Down
6 changes: 5 additions & 1 deletion lib/bootstrap_form/form_group_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,11 @@ def form_group_css_options(method, html_options, options)
# Add control_class; allow it to be overridden by :control_class option
control_classes = css_options.delete(:control_class) { control_class }
css_options[:class] = safe_join([control_classes, css_options[:class]].compact, " ")
css_options[:class] << " is-invalid" if error?(method)
if error?(method)
css_options[:class] << " is-invalid"
labelledby = options[:id].present? ? "#{options[:id]}_feedback" : field_id(method, :feedback)
css_options[:aria] = { labelledby: }
end
css_options[:placeholder] = form_group_placeholder(options, method) if options[:label_as_placeholder]
css_options
end
Expand Down
9 changes: 5 additions & 4 deletions lib/bootstrap_form/helpers/bootstrap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def errors_on(name, options={})
hide_attribute_name = options[:hide_attribute_name] || false
custom_class = options[:custom_class] || false

tag.div class: custom_class || "invalid-feedback" do
tag.div(class: custom_class || "invalid-feedback", id: field_id(name, :feedback)) do
errors = if hide_attribute_name
object.errors[name]
else
Expand Down Expand Up @@ -66,20 +66,21 @@ def custom_control(*args, &)
end

def prepend_and_append_input(name, options, &)
id = options[:id]
options = options.extract!(:prepend, :append, :input_group_class).compact

input = capture(&) || ActiveSupport::SafeBuffer.new

input = attach_input(options, :prepend) + input + attach_input(options, :append)
input << generate_error(name)
input << generate_error(name, id)
options.present? &&
input = tag.div(input, class: ["input-group", options[:input_group_class]].compact)
input
end

def input_with_error(name, &)
def input_with_error(name, id, &)
input = capture(&)
input << generate_error(name)
input << generate_error(name, id)
end

def input_group_content(content)
Expand Down
1 change: 1 addition & 0 deletions lib/bootstrap_form/inputs/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def bootstrap_field(field_name)

def bootstrap_select_group(field_name)
define_method(:"#{field_name}_with_bootstrap") do |name, options={}, html_options={}|
options.delete(:id)
html_options = html_options.reverse_merge(control_class: "form-select")
form_group_builder(name, options, html_options) do
form_group_content_tag(name, field_name, "#{field_name}_without_bootstrap", options, html_options)
Expand Down
6 changes: 5 additions & 1 deletion lib/bootstrap_form/inputs/check_box.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def check_box_with_bootstrap(name, options={}, checked_value="1", unchecked_valu
content = tag.div(class: check_box_wrapper_class(options), **options[:wrapper].to_h.except(:class)) do
html = check_box_without_bootstrap(name, check_box_options(name, options), checked_value, unchecked_value)
html << check_box_label(name, options, checked_value, &block) unless options[:skip_label]
html << generate_error(name) if options[:error_message]
html << generate_error(name, options[:id]) if options[:error_message]
html
end
wrapper(content, options)
Expand Down Expand Up @@ -41,6 +41,10 @@ def check_box_options(name, options)
:inline, :label, :label_class, :label_col, :layout, :skip_label,
:switch, :wrapper, :wrapper_class)
check_box_options[:class] = check_box_classes(name, options)
if error?(name)
labelledby = options[:id].present? ? "#{options[:id]}_feedback" : field_id(name, :feedback)
check_box_options[:aria] = { labelledby: }
end
check_box_options.merge!(required_field_options(options, name))
end

Expand Down
1 change: 1 addition & 0 deletions lib/bootstrap_form/inputs/collection_check_boxes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module CollectionCheckBoxes

included do
def collection_check_boxes_with_bootstrap(*args)
args[4]&.delete(:id)
html = inputs_collection(*args) do |name, value, options|
options[:multiple] = true
check_box(name, options, value, nil)
Expand Down
1 change: 1 addition & 0 deletions lib/bootstrap_form/inputs/collection_radio_buttons.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module CollectionRadioButtons

included do
def collection_radio_buttons_with_bootstrap(*args)
args[4]&.delete(:id)
inputs_collection(*args) do |name, value, options|
radio_button(name, value, options)
end
Expand Down
6 changes: 5 additions & 1 deletion lib/bootstrap_form/inputs/radio_button.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def radio_button_with_bootstrap(name, value, *args)
tag.div(**wrapper_attributes) do
html = radio_button_without_bootstrap(name, value, radio_button_options(name, options))
html << radio_button_label(name, value, options) unless options[:skip_label]
html << generate_error(name) if options[:error_message]
html << generate_error(name, options[:id]) if options[:error_message]
html
end
end
Expand All @@ -28,6 +28,10 @@ def radio_button_options(name, options)
radio_button_options = options.except(:class, :label, :label_class, :error_message, :help,
:inline, :hide_label, :skip_label, :wrapper, :wrapper_class)
radio_button_options[:class] = radio_button_classes(name, options)
if error?(name)
labelledby = options[:id].present? ? "#{options[:id]}_feedback" : field_id(name, :feedback)
radio_button_options[:aria] = { labelledby: }
end
radio_button_options.merge!(required_field_options(options, name))
end

Expand Down
2 changes: 1 addition & 1 deletion lib/bootstrap_form/inputs/time_zone_select.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ module TimeZoneSelect
def time_zone_select_with_bootstrap(method, priority_zones=nil, options={}, html_options={})
html_options = html_options.reverse_merge(control_class: "form-select")
form_group_builder(method, options, html_options) do
input_with_error(method) do
input_with_error(method, options[:id]) do
time_zone_select_without_bootstrap(method, priority_zones, options, html_options)
end
end
Expand Down
24 changes: 22 additions & 2 deletions test/bootstrap_checkbox_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,11 @@ class BootstrapCheckboxTest < ActionView::TestCase
<form accept-charset="UTF-8" action="/users" class="new_user" id="new_user" method="post">
<div class="form-check mb-3">
<input #{autocomplete_attr} name="user[terms]" type="hidden" value="0" />
<input class="form-check-input is-invalid" id="user_terms" name="user[terms]" type="checkbox" value="1" />
<input class="form-check-input is-invalid" id="user_terms" aria-labelledby="user_terms_feedback" name="user[terms]" type="checkbox" value="1" />
<label class="form-check-label" for="user_terms">
I agree to the terms
</label>
<div class="invalid-feedback">You must accept the terms.</div>
<div class="invalid-feedback" id="user_terms_feedback">You must accept the terms.</div>
</div>
</form>
HTML
Expand All @@ -209,6 +209,26 @@ class BootstrapCheckboxTest < ActionView::TestCase
assert_equivalent_html expected, actual
end

test "check_box renders error when asked with specified id:" do
@user.errors.add(:terms, "You must accept the terms.")
expected = <<~HTML
<form accept-charset="UTF-8" action="/users" class="new_user" id="new_user" method="post">
<div class="form-check mb-3">
<input #{autocomplete_attr} name="user[terms]" type="hidden" value="0" />
<input class="form-check-input is-invalid" id="custom-id" aria-labelledby="custom-id_feedback" name="user[terms]" type="checkbox" value="1" />
<label class="form-check-label" for="custom-id">
I agree to the terms
</label>
<div class="invalid-feedback" id="custom-id_feedback">You must accept the terms.</div>
</div>
</form>
HTML
actual = bootstrap_form_for(@user) do |f|
f.check_box(:terms, label: "I agree to the terms", error_message: true, id: "custom-id")
end
assert_equivalent_html expected, actual
end

test "check box with custom wrapper class" do
expected = <<~HTML
<div class="form-check mb-3 custom-class">
Expand Down
Loading