-
-
Save telwell/db42a4dafbe9cc3b7988debe358c88ad to your computer and use it in GitHub Desktop.
# Adapted from https://rubyplus.com/articles/3401-Customize-Field-Error-in-Rails-5 | |
ActionView::Base.field_error_proc = Proc.new do |html_tag, instance| | |
html = '' | |
form_fields = [ | |
'textarea', | |
'input', | |
'select' | |
] | |
elements = Nokogiri::HTML::DocumentFragment.parse(html_tag).css "label, " + form_fields.join(', ') | |
elements.each do |e| | |
if e.node_name.eql? 'label' | |
html = %(#{e}).html_safe | |
elsif form_fields.include? e.node_name | |
e['class'] += ' is-invalid' | |
if instance.error_message.kind_of?(Array) | |
html = %(#{e}<div class="invalid-feedback">#{instance.error_message.uniq.join(', ')}</div>).html_safe | |
else | |
html = %(#{e}<div class="invalid-feedback">#{instance.error_message}</div>).html_safe | |
end | |
end | |
end | |
html | |
end |
undefined method `+' for nil:NilClass
with e['class'] += ' is-invalid'
I achieved this using Javascript.
<script>
$(document).ready(function(){
$('.field_with_errors input:first').addClass('is-invalid')
$('.field_with_errors select:first').addClass('is-invalid')
$('.field_with_errors textarea:first').addClass('is-invalid')
});
</script>
Here's a version with the nil issue fixed: https://gist.github.com/andreimoment/515715d9d56ffbdadc25697d9db52c62
@ro31337, html_escape and its h alias seem to be unavailable for initializers. Users having control over error messages is not a common use case so I'm leaving this alone.
How about this simpler solution which is compatible with Bootstrap 4:
# add in config/initializers/stock_errors.rb
ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
html_tag.html_safe
end
@pastullo how exactly your solution is compatible with Bootstrap 4? You just do not wrap html_tag
by div
with class field_with_errors
, but also you do not add any necessary Bootstrap 4 from classes to highlight errors.
My approach: add your error class to the element's classes instead of wrapping it in a div.
ActionView::Base.field_error_proc = proc do |html_tag, instance_tag|
match = html_tag.to_s.match(/class\s*=\s*"([^"]*)"/)
return html_tag unless match[1]
html_tag
.to_s
.gsub(match[0], "class=\"#{match[1]} field_with_errors\"")
.html_safe
end
I'm not sure how good/secure it is. It's just the first solution that I came up with.
I think you will have to update the regex to handle classes with single quotes like class='my-class'
.
This could be used as base
ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
if html_tag =~ /class="(.*?)"/
(html_tag.sub /class="(.*?)"/, 'class="\1 form-error"').html_safe
else
(html_tag.sub /(\/>|>)/, 'class="form-error" \1').html_safe
end
end
here is my solution
# /config/application.rb
config.action_view.field_error_proc = Proc.new do |html_tag, instance|
if html_tag.gsub!("class=\"", "class=\"field_with_errors ")
%Q(#{html_tag}).html_safe
elsif html_tag.gsub!("class='", "class='field_with_errors ")
%Q(#{html_tag}).html_safe
elsif html_tag.gsub!("<input", "<input class=\"field_with_errors\"")
%Q(#{html_tag}).html_safe
elsif html_tag.gsub!("<select", "<select class=\"field_with_errors\"")
%Q(#{html_tag}).html_safe
elsif html_tag.gsub!("<textarea", "<textarea class=\"field_with_errors\"")
%Q(#{html_tag}).html_safe
else
%Q(<div class="field_with_errors">#{html_tag}</div>).html_safe
end
end
I've create a custom initializer to have each field having its own errors below it
# app/config/initializers/bootstrap_form_errors_customizer.rb
ActionView::Base.field_error_proc = proc do |html_tag, instance|
is_label_tag = html_tag =~ /^<label/
class_attr_index = html_tag.index 'class="'
def format_error_message_to_html_list(error_msg)
html_list_errors = "<ul></ul>"
if error_msg.is_a?(Array)
error_msg.each do |msg|
html_list_errors.insert(-6,"<li>#{msg}</li>")
end
else
html_list_errors.insert(-6,"<li>#{msg}</li>")
end
html_list_errors
end
invalid_div =
"<div class='invalid-feedback'>#{format_error_message_to_html_list(instance.error_message)}</div>"
if class_attr_index && !is_label_tag
html_tag.insert(class_attr_index + 7, 'is-invalid ')
html_tag + invalid_div.html_safe
elsif !class_attr_index && !is_label_tag
html_tag.insert(html_tag.index('>'), ' class="is-invalid"')
html_tag + invalid_div.html_safe
else
html_tag.html_safe
end
end
Here's an alternative version to the bootstrap_form_errors_customizer.rb above for bootstrap 4.
- It assumes you're using form-control for inputs and textareas
- it sets the error message after the label if you're using form-check-label and form-check-input
- it only creates a html list if there's multiple errors
# app/config/initializers/bootstrap_form_errors_customizer.rb
ActionView::Base.field_error_proc = proc do |html_tag, instance|
def invalid_feedback_msg(error_msg)
if error_msg.is_a?(Array) && error_msg.size > 1
html_list_errors = "<ul class='pl-3'></ul>"
error_msg.each do |msg|
html_list_errors.insert(-6,"<li>#{msg}</li>")
end
%{<div class='invalid-feedback'>#{html_list_errors}</div>}.html_safe
elsif error_msg.is_a?(Array)
%{<div class='invalid-feedback mb-1'>#{error_msg.first}</div>}.html_safe
else
%{<div class='invalid-feedback mb-1'>#{error_msg}</div>}.html_safe
end
end
if html_tag =~ /^<label/
if html_tag =~ /form-check-label/
html_tag.html_safe + invalid_feedback_msg(instance.error_message)
else
html_tag.html_safe
end
else
html_tag.gsub!("form-check-input", "form-check-input is-invalid")
html_tag.gsub!("form-control", "form-control is-invalid")
if html_tag =~ /form-check-input/
html_tag.html_safe
else
html_tag.html_safe + invalid_feedback_msg(instance.error_message)
end
end
end
Here's my version, it's made to support MaterializeCss form fields for which you only need the correct classes, logic goes like this:
- Check if a class attribute is not present, then set the classes in the first space available and next the tag out of the proc (I believe this is called negative programming, dealing with edge cases first).
- Last blocks only execute if the class is present, in which case we replace the start of it with the same + the classes at the start and a space, then just
return/next
it at the end.
I've create a custom initializer to have each field having its own errors right below it
# app/config/initializers/bootstrap_form_errors_customizer.rb ActionView::Base.field_error_proc = proc do |html_tag, instance| is_label_tag = html_tag =~ /^<label/ class_attr_index = html_tag.index 'class="' def format_error_message_to_html_list(error_msg) html_list_errors = "<ul></ul>" if error_msg.is_a?(Array) error_msg.each do |msg| html_list_errors.insert(-6,"<li>#{msg}</li>") end else html_list_errors.insert(-6,"<li>#{msg}</li>") end html_list_errors end invalid_div = "<div class='invalid-feedback'>#{format_error_message_to_html_list(instance.error_message)}</div>" if class_attr_index && !is_label_tag html_tag.insert(class_attr_index + 7, 'is-invalid ') html_tag + invalid_div.html_safe elsif !class_attr_index && !is_label_tag html_tag.insert(html_tag.index('>'), ' class="is-invalid"') html_tag + invalid_div.html_safe else html_tag.html_safe end end
guided me perfectly thank you very much @Ggs91
—-
I edited the code example to work with rails 6 and webpackered bootstrap 5
precisely: rails (6.1.3.2)
and bootstrap@^5.0.1
# app/config/initializers/bootstrap_form_errors_customizer.rb
ActionView::Base.field_error_proc = proc do |html_tag, instance|
is_label_tag = html_tag =~ /^<label/
class_attr_index = html_tag.index 'class="'
def format_error_message_to_html_list(error_msg)
html_list_errors = '<ul></ul>'
if error_msg.is_a?(Array) || error_msg.is_a?(ActiveModel::DeprecationHandlingMessageArray)
error_msg.each do |msg|
html_list_errors.insert(-6, "<li>#{msg}</li>")
end
else
html_list_errors.insert(-6, "<li>#{msg}</li>")
end
html_list_errors
end
invalid_div =
"<div class='invalid-feedback'>#{format_error_message_to_html_list(instance.error_message)}</div>"
if class_attr_index && !is_label_tag
html_tag.insert(class_attr_index + 7, 'is-invalid ')
html_tag + invalid_div.html_safe
elsif !class_attr_index && !is_label_tag
html_tag.insert(html_tag.index('>'), ' class="is-invalid"')
html_tag + invalid_div.html_safe
else
html_tag.html_safe
end
end
and don't forget to restart rails server when tinkering with initializers 😎 like i did 🦀
Thanks @simonneutert I've reduce it even further:
ActionView::Base.field_error_proc = proc do |html_tag, instance|
def format_error_message_to_html_list(instance)
messages = [*instance.error_message].map { |msg| "<li>#{msg}</li>" }
return unless messages.present?
"<div class='invalid-feedback'><ul>#{messages.join('')}</ul></div>"
end
html_tag + format_error_message_to_html_list(instance).html_safe
end
Is there a reason not to use Nokogiri (since it's a Rails dependency)? Maybe it's slower?
In Rails 7 I could reduce this whole String manipulation with RegExes with:
# config/application.rb
...
config.action_view.field_error_proc = proc do |html_tag, instance|
input_tag = Nokogiri::HTML5::DocumentFragment.parse(html_tag).at_css('.form-control')
if input_tag
input_tag.add_class('is-invalid').to_s.html_safe
else
html_tag
end
end
Next step for me is adding the error messages on a sibling '.invalid-feedback' element.
You better
h(instance.error_message)
andh(instance.error_message.uniq.join(', '))
. If attacker has control over your error messages, you're screwed! Would be also nice to see example html/erb. It didn't work with my markup, error on line 17. Seems like I don't haveclass
for my tag.