Rails Tip: Accessible Field Errors in Tight Spaces

Problem

I am currently working on a project to migrate a bunch of one-off promotional sites onto a single platform that can serve them all. Pretty basic user experience: a consumer performs a search for a product, clicks on an advertisement, is brought to one of these promotional sites, and, if interested, the consumer fills out a form and is taken off-site to perform the necessary action to complete the promotion. One problem I inherited is an image heavy layout, completed by a third-party agency, which allows no room for form field error messages. Current site behavior is to report errors by highlighting the invalid fields with a red border:

This can be pretty uninformative if the consumer enters data, but the data fails validation. What I want is to be able to give consumers a way to find out why the field is invalid, while not breaking the third-party developed layout that doesn’t supply room for error messages.

Solution

Instead of highlighting the field that is invalid, I want to highlight the field’s label; and within that label, I want to apply a title tag, that if the consumer hovers over, will give them a description of why the field is invalid.

Implementation

In order to implement the solution, we need to build a custom FormBuilder that will auto-generate our label and form field for us, applying a title tag with error messages when applicable.

Filename: app/helpers/tagged_builder.rb

class TaggedBuilder < ActionView::Helpers::FormBuilder
  def self.create_tagged_field(method_name)
    define_method(method_name) do |label, *args|
      errors = object.errors.on(label.to_sym)
      klass = errors.blank? ? nil : 'errorLabel'
      msg = label.to_s.humanize
      unless errors.blank?
        msg += ' ' + ((errors.class == Array) ? errors.join(' and ') : errors)
      end
      
      @template.content_tag("div", @template.content_tag("label", "#{label.to_s.humanize}:", :for => "#{@object_name}_#{label}", :class => klass, :title => msg) + super)
    end
  end
  
  field_helpers.each do |name|
    create_tagged_field(name) unless ('hidden_field' == name)
  end
end

or each of the predefined field_helpers (except hidden fields) we want to process our custom create_tagged_field method to build a containing div and our labelwith optional error messages, before we call super to build the form field.

In generating the error message string, we check to see if there are any error messages recorded on the field we are building: object.errors.on(label.to_sym). If we find errors, (errors.blank? is false), we assign an errorLabel class to the label we are creating. errors.on will return a string if one error was found, or an array of error messages if more than one were found. To format our error messages appropriately for a title attribute, we need to perform a join if we have an array of errors (errors.join(' and ')), otherwise we can just use the error message string.

When creating a form in our view, we need to tell form_for to use the custom builder we just wrote:

Filename: app/views/your_class/_form.html.erb

<% form_for(@model, :builder => TaggedBuilder) do |f| %>
  <%= f.text_field :my_field %>
  ...
<%- end -%>

This will now generate our labels and form fields appropriately, but another thing we should consider doing is overwriting the default error handling behavior of Rails. By default, when a field has an error, Rails will surround the form field in a div with a class of fieldWithErrors — even with our custom form builder. What we are going to do is keep the fieldWithErrors class, but move it into the field directly by overwriting ActionView::Base.field_error_proc:

Filename: config/environment.rb

ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
  error_class = "fieldWithErrors"
  if html_tag =~ /<(input|textarea|select)[^>]+class=/
    class_attribute = html_tag =~ /class=['"]/
    html_tag.insert(class_attribute + 7, "#{error_class} ")
  elsif html_tag =~ /<(input|textarea|select)/
    first_whitespace = html_tag =~ /\s/
    html_tag[first_whitespace] = " class='#{error_class}' "
  end
  html_tag
end

This procedure keeps any user defined classes and appends the fieldWithErrors class when necessary, without wrapping the form field in a div.

Finally, we add a little style to make the errors stand out more:

label.errorLabel { color: #F00; cursor: help; }

What we end up with is a nice compact form that still informs consumers of validation errors: