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; }
Contents
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 end
If 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 end
Now 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 end
Now 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 :)
Comments
blog comments powered by Disqus