Rails: Bootstrap form validation
Bootstrap has nice server-side form validation (https://getbootstrap.com/docs/5.3/forms/validation):
<input class="form-control is-invalid">
<div class="invalid-feedback">can't be blank</div>But doesn't work out-of-the-box in Rails, because Rails' form.input puts a div.field_with_errors around it:
<div class="field_with_errors">
<input class="form-control is-invalid">
</div>
<div class="invalid-feedback">can't be blank</div>Causing div.invalid-feedback to hide, because Bootstrap CSS-rule doesn't work, because .is-invalid & .invalid-feedback aren't siblings any more (~ in CSS means siblings/on the same level):
.is-invalid ~ .invalid-feedback {
display: block;
}Solution: overwrite field_error_proc
By overriding ActionView::Base.field_error_proc, all children of .field_with_errors will now automatically receive Bootstrap class .is-invalid:
# config/initializers/field_error_bootstrap.rb
# See: https://stackoverflow.com/questions/7454682/customizing-field-with-errors#73173569
ActionView::Base.field_error_proc = proc do |html|
frag = Nokogiri::HTML5::DocumentFragment.parse(html)
klass = frag.children[0].attributes['class']
if klass.present?
frag.children[0].attributes['class'].value = [klass, 'is-invalid'].join(' ')
else
frag.children[0].set_attribute('class', 'is-invalid')
end
frag.to_html.html_safe
endIf you want the above constrained to a namespaced part of your website, e.g. module Admin, add to the above:
ActionView::Base.field_error_proc = proc do |html|
+ if controller.class.module_parents.include?(Admin)
...
+ else
+ html
+ end
endNow to list the errors underneath the field, add this helper:
# app/helpers/application_helper.rb
# Bootstrap field error
def errors_for(obj, attr)
res = ''
if obj && invalid?(obj, attr)
errors = obj.errors&.messages&.try(:'[]', attr).try(:join, ', ')
res = "<div id='#{obj.class.name.underscore}_#{attr}_feedback' class='invalid-feedback'>" \
"<i class='bi bi-exclamation-triangle-fill me-1'></i>#{errors}</div>".html_safe
end
res
endNow you can write form_with fields like this:
<%= form_with ... do |f| %>
<%= f.label :name, "name" %>
<%= errors_for(@person, :name) %>
<% end %>Workaround
Adding .d-block (display: block):
<div class="invalid-feedback d-block">can't be blank</div>has this drawback: you still need to add .is-invalid to the HTML form elements.
Caveat: form_with object/scope/model/url and multiple models
The solution didn't work for this form - borders weren't displayed red:
form_with(model: [current_tenant, :admin, user, order], scope: :cart)
Case: ActionView::Helpers::ActiveModelHelper.error_wrapping does an object_has_errors? and object was Order-object with 0 errors, because errors are in Cart-object.
Solution: rewrite form_with so that object_has_errors? uses Cart-object instead of Order-object:
url = url_for([current_tenant, :admin, user, order]) form_with(model: cart, url:)
Invalid-field-borders are now displayed Bootstrap-red :)