Show Rails validation errors inline (with Bootstrap 4)
Tags: rails, ruby, bootstrap
Published: 2021-08-24 12:51:01

To show inline errors in a Ruby on Rails form, there exists a few gems out there such as `bootstrap_form` or `client_side_validations` (which shows validation errors inline client side).

However, we can achieve inline validation (server side) ourselves without using additional gems.

# Prerequisites
1 - Rails 4 or higher
2 - Booststrap 4 (just for styling, custom css is fine too)
3 - Nokogiri for html parsing (it is normally included in your Rails installation)

# The code

### step 1
Create an initializer: `config/initializers/fields_with_errors.rb`

### step 2
Add this code in it

```ruby
ActionView::Base.field_error_proc = proc do |html_tag, instance|
    html_doc = Nokogiri::HTML::DocumentFragment.parse(html_tag, Encoding::UTF_8.to_s)
    element = html_doc.children[0]

    if element
        element.add_class('is-invalid')

        if %w[input select textarea].include? element.name
            instance.raw %(#{html_doc.to_html} <div class="invalid-feedback">#{[*instance.error_message].to_sentence}</div>)
        else
            instance.raw html_doc.to_html
        end
    else
        html_tag
    end
end
```

That's it. You can keep reading if you want to understand how this code works.

----

# Explanation
Let's assume we have a `User` model with some validations

```ruby
# app/models/user.rb
class User < ApplicationRecord
    validates :email, presence: true
end
```

and an edit or create form for a user

```erb
<%= form_for User.new do |f| %>
    <div class="form-group">
        <%= f.label :email %>
        <%= f.text_field :email, class: 'form-control' %>
    </div>
    <%= f.submit 'Save' %>
<% end %>
```

the html version of `form-group` block becomes this

```html
<div class="form-group">
    <label for="user_email">Email</label>
    <input class="form-control" type="text" value="" name="user[email]" id="user_email"> 
</div>
```

What we want is to add CSS error classes to the `label` and `input` tag. These CSS classes should highlight the field in red (or any other color you want)
Additionally, we want to append a `div` or `span` bellow the `input` tag to show the error for that specific attribute

So, the final result should be this 

```html
<div class="form-group">
    <label for="user_email" class="is-invalid">Email</label>
    <input class="form-control is-invalid" type="text" value="" name="user[email]" id="user_email"> 
    <div class="invalid-feedback">cannot be blank</div>
</div>
```

So, how does the code in our initializer achieves all this?

**Line 1:** we have a `proc` with 2 arguments.

```ruby
ActionView::Base.field_error_proc = proc do |html_tag, instance|
```

The first is `html_tag`. It is an html string. In our example, it is equal to `<label for="user_email">Email</label>`. 
The second arguments is `instance`. This an instance of `ActionView::Helpers::Tags::Label` in this case. This also gives have access to all of `ActionView::Helpers` methods.

**Line 2:** we use Nokogiri to generate a document representation of the `html_tag` string

```ruby
html_doc = Nokogiri::HTML::DocumentFragment.parse(html_tag, Encoding::UTF_8.to_s)
```

which returns an instance of `DocumentFragment`

```ruby
#(DocumentFragment:0x3fcc12af6a68 {
  name = "#document-fragment",
  children = [
    #(Element:0x3fcc12af6888 {
      name = "input",
      attributes = [
        #(Attr:0x3fcc12af6860 { name = "class", value = "form-control" }),
        #(Attr:0x3fcc12af684c { name = "type", value = "text" }),
        #(Attr:0x3fcc12af6838 { name = "value", value = "" }),
        #(Attr:0x3fcc12af6824 { name = "name", value = "user[email]" }),
        #(Attr:0x3fcc12af6810 { name = "id", value = "user_email" })]
      })]
  })
```

**Line 3:** we fetch the actual html tag document we need

```ruby 
element = html_doc.children[0]
```

it returns instance of `Element`

```ruby
#(Element:0x3fcc12af6888 {
  name = "input",
  attributes = [
    #(Attr:0x3fcc12af6860 { name = "class", value = "form-control" }),
    #(Attr:0x3fcc12af684c { name = "type", value = "text" }),
    #(Attr:0x3fcc12af6838 { name = "value", value = "" }),
    #(Attr:0x3fcc12af6824 { name = "name", value = "company[co]" }),
    #(Attr:0x3fcc12af6810 { name = "id", value = "user_email" })]
})
```

**Line 5:** we skip if `element` is nil, we jump to line 14 and return the original `html_tag`, else we continue with the code execution.

**Line 6:** we add bootstrat `is-invalid` CSS class. You can replace it with any of your choice.

```ruby
element.add_class('is-invalid')
```

**Line 8:** we check if the element is a form tag which takes input. This is important in order to append the errors after the input

```ruby
if %w[input select textarea select].include? element.name
```

**Line 9:** we convert our Nokogiri instance back into an html String and append errors lists.

```ruby
instance.raw %(#{html_doc.to_html} <div class="invalid-feedback">#{[*instance.error_message].to_sentence}</div>)
```

We call `instance.raw` to ensure the string is rendered as html.
We access errors for the current attribute via `instance.error_message`. However, this can return a single string and an Array. 
To avoid doing `if else` conditions, we create new array, splat `[*instance.error_message]` in it, the call `to_sentence`


**Line 11:** in case the element is a `label`, we render parse it back to html.

```ruby
instance.raw html_doc.to_html
```

That's it.