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