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
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
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 %>
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>
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>
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|
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)
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" })]
})]
})
Line 3: we fetch the actual html tag document we need
element = html_doc.children[0]
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" })]
})
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')
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
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>)
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
That's it.
Top comments (2)
Hi, how to use i18n?
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