DEV Community

Cover image for Show Rails validation errors inline (with Bootstrap 4)
Frank Etoundi
Frank Etoundi

Posted on

Show Rails validation errors inline (with Bootstrap 4)

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

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
Enter fullscreen mode Exit fullscreen mode

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

# app/models/user.rb
class User < ApplicationRecord
    validates :email, presence: true
end
Enter fullscreen mode Exit fullscreen mode

and an edit or create form for a user

<%= 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 %>
Enter fullscreen mode Exit fullscreen mode

the html version of form-group block becomes this

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

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

<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>
Enter fullscreen mode Exit fullscreen mode

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

Line 1: we have a proc with 2 arguments.

ActionView::Base.field_error_proc = proc do |html_tag, instance|
Enter fullscreen mode Exit fullscreen mode

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

html_doc = Nokogiri::HTML::DocumentFragment.parse(html_tag, Encoding::UTF_8.to_s)
Enter fullscreen mode Exit fullscreen mode

which returns an instance of DocumentFragment

#(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" })]
      })]
  })
Enter fullscreen mode Exit fullscreen mode

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

element = html_doc.children[0]
Enter fullscreen mode Exit fullscreen mode

it returns instance of Element

#(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" })]
})
Enter fullscreen mode Exit fullscreen mode

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.

element.add_class('is-invalid')
Enter fullscreen mode Exit fullscreen mode

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

if %w[input select textarea select].include? element.name
Enter fullscreen mode Exit fullscreen mode

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

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

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.

instance.raw html_doc.to_html
Enter fullscreen mode Exit fullscreen mode

That's it.

Top comments (2)

Collapse
 
joathan profile image
Joathan Francisco

Hi, how to use i18n?

Collapse
 
etoundi_1er profile image
Frank Etoundi

You should go through the official docs guides.rubyonrails.org/i18n.html.

Your errors will be automatically translated so long as you have correctly set up your translation files (config/locales).

For example:
Assuming a User model with validation on the password presence and confirmation, we can do translation as seen below

# app/models/user.rb
validates :password, presence: true, confirmation: true
Enter fullscreen mode Exit fullscreen mode
# config/locales/en.yml
en:
  errors:
    format: "%{attribute} %{message}"
    messages:
      accepted: must be accepted
      blank: can't be blank
Enter fullscreen mode Exit fullscreen mode