DRY Input Errors in Rails

Tatacoa desert, Colombia

Photo by Omar Nava on Unsplash

Spinning up a new Rails app has always been a joy to me. You can quickly scaffold your screens, setup your initial models and go straight to creating value and business logic. However, there's always a part where I feel like the framework doesn't quite do the entire job: form validation and error handling. There's one thing that I find specially difficult, and that is showing validation errors.

If you run Rails scaffold generator (which is great way to get going fast but not really intended for production code), the form partial includes this snippet of code:

<div id="error_explanation">
<h2><%= pluralize(model.errors.count, "error") %> prohibited this model from being saved:</h2>

<ul>
<% model.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>

What it does is render an alert on top of the form wich contains the field name followed by the error message. Furthermore, since this bit of logic is fairly generic we could consider extracting it into it's own partial:

description

This technique is fine if your forms contain two or three inputs, but more than that it transfers the burden of mapping each error message to it's corresponding field to the user.

It would be clearer to show each error next to the input that produced it. To achieve this we'd have to delete this block of code generated by the scaffold with something else:

<%= form.label :email %>
<%= form.text_field :email %>

<!-- Show Email validation errors -->
<%= tag.p user.errors["email"].join(", ") if user.errors["email"].any? %>

While this seems good enough for start, it quickly becomes a very repetitive task to add the error fields to every input. Furthermore, if you've ever used something like Simple Form or Formtastic it feels like we're writing so much more code compared to:

<%= simple_form_for @user do |f| %>
<%= f.input :email, label: 'Your email please', error: 'Email is mandatory, please specify one' %>
<% end %>

This snippet is doing a lot behind the scenes. It creates a label for our input, correctly infers the input type , conditionally renders errors below the input an even wraps it all up inside a dive so that your CSS can target the whole group.

However I did not want to bring in Simple Form into this project for two reasons.

The first one is that Simple Form plays better with a full fledged css framework like Bootstrap or Bulma and I'm using tailwind in this project. Since I'm using tailwind here I don't think utility css plays well with passing options through keyword_args. It makes it harder to understand the output HTML and I have't been able to make the intellisense + autocomplete work in my editor:

<%= simple_form_for @user do |f| %>
<%= f.input :email,
label: 'Your email please',
error: 'Email is mandatory, please specify one',
wrapper_html: { class: "mt-2" },
label_html: { class: "font-semibold text-gray-800" },
input_html: { class: "border border-gray-800 rounded" },
%>

<% end %>

<!-- vs -->

<%= form_for @user do |f| %>
<div class="mt-2">
<label for="user_email" class="font-semibold text-gray-800">Email</label>
<%= f.input :email, class: "border border-gray-800 rounded" %>
</div>
<% end %>

The second reason is a self-imposed rule to avoid bringing in gems if I only need a small part of it's functionality and I could write it myself in a reasonable amount of time.

DRYing up error rendering with a hook #

Even if I didn't want all of Simple Form's functionality, conditionally rendering the model's validation errors seemed pretty useful to me so we could try to bring just that bit into our apps. Some googling around lead me to this great blog post by Jorge Manrubia. From there, we could modify his initializer to render errors next to each field instead of at the top of the form.

# Place this code in a initializer. E.g: config/initializers/field_error.rb

ActionView::Base.field_error_proc = Proc.new do |html_tag, instance_tag|
fragment = Nokogiri::HTML.fragment(html_tag)
field = fragment.at('input,select,textarea')

html = if field
error_message = instance_tag.error_message.join(', ')
field['class'] = "#{field['class']} border-red-600 border focus:outline-none"
html = <<-HTML
#{fragment.to_s}
<p class="mt-1 text-sm text-red-600">#{error_message.upcase_first}</p>
HTML
html
else
html_tag
end

html.html_safe
end

I'll walk through this code to see what's going on. While Rails is rendering the form elements it will call a Proc every time it detects the model has an error. You can hook into that and replace the default behaviour (which wraps the input in a div with a field_error css class) with something else.

Since The Proc gets called with all sort of elements, the first step is to check if we're dealing with an input element, and if that's the case append a <p /> tag with our error and styling on it. Thanks to the error_message) method on instance_tag we can get the corresponding errors on that field. For all other elements we return their html unchanged.

Closing thoughts #

And there you go! Now we have a way to show validation errors on all of our forms without having to write conditional checks againts every field on our views. I got to admit it felt a bit hacky at first, but so far I haven't encountered any issues with this technique it works great with ActiveModel validations and Form Objects server side validations. So what do you think? Is this a great or terrible idea? Let me know via email if you experiment with it and find any issues.