Skip to content

Instantly share code, notes, and snippets.

@telwell
Last active May 18, 2023 16:54
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • 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
@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