DEV Community

Cover image for Better Inline Validation for Rails Forms (with ViewComponent or partials)
Rails Designer
Rails Designer

Posted on • Originally published at railsdesigner.com

Better Inline Validation for Rails Forms (with ViewComponent or partials)

This article was originally published on [Rails Designer]https://railsdesigner.com/inline-form-validations/) — The First Professionally-designed UI Components Library for Rails


Validations have been a key part of Ruby on Rails. It provides an easy way to show any errors made by the user to the any inputs. Example of validation errors could be:

  1. Email has already been taken
  2. Password must be at least 8 characters long
  3. Username can only contain letters, numbers, and underscores

Typically these validation messages are shown as follows:

<% if @user.errors.any? %>
  <div id="error_explanation">
    <h2><%= pluralize(@user.errors.count, "error") %> prohibited this <%= @user.class.name.downcase %> from being saved:</h2>

    <ul>
      <% @user.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
    </ul>
  </div>
<%% end %>
Enter fullscreen mode Exit fullscreen mode

While this does get the message across, it is better to place the validation messages close to input fields to enhance UX by providing immediate, contextual feedback and reducing cognitive load.

So what am I looking for? Something like this example:

Image description

It shows the input's label by default, then replaces its text with the validation error message.

Let's go over the steps to create this in your Rails app. 💡 No time to write this yourself? A FormLabelComponent is included in Rails Designer UI components library.

How to do this with ViewComponent

I assume you have a Rails app ready with ViewComponent installed. Generate the basic component: rails g component FormLabel --inline.

Then add the initializer passing along the FormBuilder object and the field name (eg. “email”).

class FormLabelComponent < ViewComponent::Base
  def initialize(form:, field:)
    @form = form
    @field = field
  end
end
Enter fullscreen mode Exit fullscreen mode

There's not much HTML needed for this component, so the HTML can be added inline (read this article about the various rendering strategies in ViewComponent:

class FormLabelComponent < ViewComponent::Base
  # …
 erb_template <<-ERB
    <⁠%= @form.label @field, label_value, class: css %>
  ERB
  # …
end
Enter fullscreen mode Exit fullscreen mode

It will create a typical label tag using the FormBuilder object (that you pass along; @form). Let's look at label_value and css methods.

class FormLabelComponent < ViewComponent::Base
  # …
  private

  def label_value
    has_errors? ? field_error_message : @field.to_s.capitalize
  end

  def css
    # Tailwind CSS utility classes are used
    class_names(
      "text-sm text-gray-700 font-medium",
      {
        "text-red-600": has_errors?
      }
    )
  end

  # …
end
Enter fullscreen mode Exit fullscreen mode

Looks like two more methods are used here: has_errors? and field_error_message. Have not seen class_names before? Read this article on lesser known Rails view helpers.

Let's create those last two methods:

class FormLabelComponent < ViewComponent::Base
  # …

  # Using Ruby 3.0+ syntax for one-line methods
  def field_error_message = @form.object.errors.full_messages_for(@field).first

  def has_errors? = @form.object.errors.full_messages_for(@field).any?
end
Enter fullscreen mode Exit fullscreen mode

The first method returns the first error message associated with a specific field (@field) in the form object.

And has_errors? checks if there are any error messages associated with a specific field (@field) in the form object. It returns true if there are errors, false otherwise.

You can then render it in your views like so: render FormLabelComponent.new(form: form, field: "email")

How to do this with Rails' partials

But what if you don't use ViewComponent? Well, first, do read this article on embracing ViewComponent when you come from partials (and helpers).

Let's go over how this can be done with a small partial and helper. Not saying it is as pretty as the ViewComponent!

Create a partial. I'd typically would put it in app/views/components:

<%# locals: (form:, field:) %>

<%= form.label field, label_value(form, field), class: form_label_css(form, field) %>
Enter fullscreen mode Exit fullscreen mode

Notice the use of strict locals here (introduced in Rails 7.1).

Then create a helper to store those label_value and form_label_css methods.

# app/helpers/form_label_helper.rb
module FormLabelHelper
  def label_value(form, field)
    has_errors?(form, field) ? field_error_message(form, field) : field.to_s.capitalize
  end

  def form_label_css(form, field)
    class_names(
      "text-sm text-gray-700 font-medium",
      {
        "text-red-600": has_errors?(form, field)
      }
    )
  end

  private

  def field_error_message(form, field) = form.object.errors.full_messages_for(field).first

  def has_errors?(form, field) = form.object.errors.full_messages_for(field).any?
end
Enter fullscreen mode Exit fullscreen mode

In your views you can then render it like this: render "components/form_label", form: form, field: "email".

And that's it. Two ways to improve the UX of your Rails app.

If you are in need of more UX and UI improvements, why not check out Rails Designer: it's the first professionally-designed UI components library for Rails.

Top comments (1)

Collapse
 
railsdesigner profile image
Rails Designer

Anyone with deep a11y knowledge know about any (possible) downsides to this?