The other day I was working on a new Rails application. Though I’m currently obsessed with Tailwind CSS, this project was a better fit for Bootstrap. I needed some predefined styling, quick. Unfortunately, fields with errors got in my way.

The app, like most web applications, required forms. Using Ruby on Rails’ (ActiveRecord) model validations, I was able to setup displaying error messages efficiently. I reached for a Bootstrap alert, shoved in a loop of error messages, and done.

Or, so I thought. 🤔

Isn’t it a better experience for the user if the fields that failed validation change border color? In fact, Bootstrap 4 has support for this.

 The Problem(s) was Fields with Errors

Great! Except there’s one problem.

If you’re familiar with Rails, you’ll know that when the page rerenders after a failed form validation, the field failing validation is wrapped inside <div class="field-with-errors"></div>.

In my case, I wanted to add the class name is-invalid to the failed input. In fact, “field with errors” doesn’t do much for me at all, since I’m not writing custom styling.

Writing custom styling would allow me to write some CSS, like this:

.field-with-errors input {
  border: 1px solid red;
}

This would have been extremely powerful, and a boost in development speed had I been writing my own CSS.

In this case, though, we want to defer styling decisions to Bootstrap. As mentioned before, Bootstrap already has support for this. By writing this CSS above we’re hurting ourselves in one of two ways:

  1. We're distancing ourselves from the styles provided by the CSS framework we've chosen to use. Or;
  2. We're creating more work for ourselves by trying to match the Bootstrap style with custom CSS.

The Solution

If you’ve encountered this problem before, you might know that you can override this functionality. The solution I like best is a Stack Overflow answer. Let’s start with the code in this solution, break down what it does, and then tweak it for Bootstrap 4.

1. Create an Initializer

Create an empty Ruby file: config/initializers/customize_error.rb

2. Use the Code

Inside your newly created file, add the code.

ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
  class_attr_index = html_tag.index 'class="'

  if class_attr_index
    html_tag.insert class_attr_index+7, 'error '
  else
    html_tag.insert html_tag.index('>'), ' class="error"'
  end
end

Let’s talk about what this chunk of code does.

If your ActiveRecord backed form fails validation, ActionView calls this proc for each field that failed validation. The proc passes an html_tag (the form-field) and the instance into the block.

If we didn’t overwrite it, it would wrap the field in a div that looks something like this:

<div class="field-with-errors">
... html_tag ...
</div>

However, we’re going to override that by modifying the field directly, versus wrapping it in a div.

class_attr_index = html_tag.index 'class="'

👆🏻 This variable tells us if the field already has a class attribute, or if it doesn’t.

If it does, we’ll get the index where class=” is in the raw HTML string. If it does not have class=” anywhere, we will set the variable to nil.

if class_attr_index
  html_tag.insert class_attr_index+7, 'error '

👆🏻 If the variable is not nil, we can append the class name error to the beginning of the class name list. The extra space is essential. If “class” already exists as an attribute on the form field, we want to keep the existing class intact.

else
  html_tag.insert html_tag.index('>'), ' class="error"'

👆🏻 If the variable is nil, right before we close the html_tag, we need to insert the entire class attribute assignment.

3. Modify the Code for Bootstrap 4

There are only a few changes I’ll make here.

ActionView::Base.field_error_proc = proc do |html_tag, _instance|
  class_attr_index = html_tag.index 'class="'

  if class_attr_index
    html_tag.insert class_attr_index + 7, 'is-invalid '
  else
    html_tag.insert html_tag.index('>'), ' class="is-invalid"'
  end
end

The most important change is converting the class name from error to is-invalid. With this done, be sure to restart your Rails app as initializers load on boot.

In front of the argument “instance,” I added an underscore. This underscore may look silly to you, but it is a standard convention for block variables passed in and never used.

Do What Feels "Right"

There were many ways I could have Made This Work™. However, they would have felt like a hack.

For instance, I could have used JavaScript to apply the Bootstrap field is-invalid to the form field once the page re-rendered. Or, I could have tried to re-create the Bootstrap styling for errors.

However, with a little patience and research, it turns out Rails had the tools at my disposal all along.