Skip to content
Open
Show file tree
Hide file tree
Changes from all 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] = aria_feedback_id(name:, id:) if error?(name) && label_errors
end
end
end
Expand Down
8 changes: 6 additions & 2 deletions lib/bootstrap_form/components/validation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,14 @@ 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

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

def get_error_messages(name)
Expand All @@ -84,6 +84,10 @@ def get_error_messages(name)
safe_join(object.errors[name], ", ")
end
# rubocop:enable Metrics/AbcSize

def aria_feedback_id(name:, id: nil)
id.present? ? "#{id}_feedback" : field_id(name, :feedback)
end
end
end
end
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
5 changes: 4 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,10 @@ 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"
css_options[:aria] = { labelledby: aria_feedback_id(id: options[:id], name: method) }
end
css_options[:placeholder] = form_group_placeholder(options, method) if options[:label_as_placeholder]
css_options
end
Expand Down
12 changes: 8 additions & 4 deletions lib/bootstrap_form/helpers/bootstrap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ 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: aria_feedback_id(id: options[:id], name:)
) do
errors = if hide_attribute_name
object.errors[name]
else
Expand Down Expand Up @@ -66,20 +69,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
3 changes: 3 additions & 0 deletions lib/bootstrap_form/inputs/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ def bootstrap_field(field_name)

def bootstrap_select_group(field_name)
define_method(:"#{field_name}_with_bootstrap") do |name, options={}, html_options={}|
# Specifying the id for a select doesn't work. The Rails helpers need to generate
# what they generate, and that includes the ids for each select option.
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
3 changes: 2 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,7 @@ 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)
check_box_options[:aria] = { labelledby: aria_feedback_id(id: options[:id], name:) } if error?(name)
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
7 changes: 3 additions & 4 deletions lib/bootstrap_form/inputs/inputs_collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def field_group(name, options, &)
: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) },
aria: { labelledby: group_label_div_id(id: options[:id], name:) },
role: :group
) do
group_label_div = generate_group_label_div(name, options)
Expand All @@ -100,15 +100,14 @@ def field_group(name, options, &)

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:
id: group_label_div_id(id: options[:id], name:)
) { label_text(name, options.dig(:label, :text)) }
end

def default_id(name) = raw("#{object_name}_#{name}") # rubocop:disable Rails/OutputSafety
def group_label_div_id(id:, name:) = id || field_id(name)
end
end
end
3 changes: 2 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,7 @@ 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)
radio_button_options[:aria] = { labelledby: aria_feedback_id(id: options[:id], name:) } if error?(name)
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