Skip to content

Instantly share code, notes, and snippets.

@telwell
Last active May 18, 2023 16:54
Show Gist options
  • Save telwell/db42a4dafbe9cc3b7988debe358c88ad to your computer and use it in GitHub Desktop.
Save telwell/db42a4dafbe9cc3b7988debe358c88ad to your computer and use it in GitHub Desktop.
Customize Field Errors with Rails 5 and Bootstrap
# 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
@ro31337
Copy link

ro31337 commented Sep 10, 2017

You better h(instance.error_message) and h(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 have class for my tag.

@plog
Copy link

plog commented Nov 9, 2017

undefined method `+' for nil:NilClass
with e['class'] += ' is-invalid'

@martinvelez
Copy link

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> 

@andreimoment
Copy link

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.

@pastullo
Copy link

pastullo commented Jun 7, 2018

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

@ycherniavskyi
Copy link

@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.

@rendon
Copy link

rendon commented Dec 17, 2018

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'.

@rowend
Copy link

rowend commented Feb 1, 2020

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

@haroldus-
Copy link

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

@Ggs91
Copy link

Ggs91 commented Dec 12, 2020

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

@allard
Copy link

allard commented Apr 7, 2021

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

@luchillo17
Copy link

luchillo17 commented Jun 13, 2021

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.

Screenshot from 2021-06-13 16-09-07

@simonneutert
Copy link

simonneutert commented Jul 21, 2021

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 🦀

@saxxi
Copy link

saxxi commented Mar 31, 2022

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

Screenshot 2022-03-31 at 18 25 54

Screenshot 2022-03-31 at 18 28 52

@gabrielso
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment