'How can I highlight multiple fields but show a single error message when ActiveRecord validation fails?

My use case is that at least one out of two fields needs to be filled in. I'd like to be able to show a single error message but use Rails' built-in error highlighting to identify the fields in question, something along the lines of:

Show a single error message but highlight two fields

The best approach I've come up with so far is:

  • Have a single :validate method which checks at least one is present
  • If neither is present, it adds three errors:
    • A :base error with a custom message
    • :user_name and :email errors with a custom type that can later be (partially) ignored
  • In my view I then need to reject the superfluous messages before displaying the error count or displaying the messages
# app/models/person.rb
class Person < ApplicationRecord
  validate :username_or_email_present

  private

  def username_or_email_present
    unless user_name.present? || email.present?
      errors.add(:base, :username_and_email_blank, message: "At least one of User name and Email must be provided")
      errors.add(:user_name, :hide_message_but_highlight_field, message: "no message, this error is just to highlight the field")
      errors.add(:email, :hide_message_but_highlight_field, message: "no message, this error is just to highlight the field")
    end
  end
end
# app/views/people/_form.html.erb
<%= form_with(model: person) do |form| %>

  <%# Remove the messages from the summary view using `.reject` %>
  <% visible_errors = person.errors.reject{ |e| e.type == :hide_message_but_highlight_field } %>
  <% if visible_errors.any? %>
    <div class="text-red-700">
      <h2><%= pluralize(visible_errors.count, "error") %> prohibited this person from being saved:</h2>

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

  <div class="md:flex md:items-center mb-6">
    <div class="md:w-1/3">
      <%= form.label :user_name, class: "block text-gray-700 font-bold md:text-right mb-1 md:mb-0 pr-4" %>
    </div>
    <div class="md:w-2/3">
      <%= form.text_field :user_name, required: true, class: "bg-gray-100 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-purple-500 focus:ring-0" %>
    </div>
  </div>

  <div class="md:flex md:items-center mb-6">
    <div class="md:w-1/3">
      <%= form.label :email, class: "block text-gray-700 font-bold md:text-right mb-1 md:mb-0 pr-4" %>
    </div>
    <div class="md:w-2/3">
      <%= form.email_field :email, required: true, class: "bg-gray-100 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-purple-500 focus:ring-0" %>
    </div>
  </div>

<% end %>


Solution 1:[1]

The approach I discovered while writing this question seems to be the least bad solution. I have adopted this solution in my app with a few minor tweaks.

In the model, add a custom :validate method and flag the errors with a custom type (I settled on :highlight_field_but_hide_message):

# app/models/person.rb
class Person < ApplicationRecord
  validate :username_or_email_present

  private

  def username_or_email_present
    if email.blank? && username.blank?
      errors.add(:base, :email_and_username_blank, message: "At least one of Email and Username must be provided")
      errors.add(:email, :highlight_field_but_hide_message)
      errors.add(:username, :highlight_field_but_hide_message)
    end
  end
end

In the view, reject the superfluous error messages and then display the remaining errors:

# app/views/people/_form.html.erb
<%= form_with(model: person) do |form| %>

  <%# Remove the messages from the summary view using `.reject` %>
  <% visible_errors = person.errors.reject{ |e| e.type == :highlight_field_but_hide_message } %>
  <% if visible_errors.any? %>
    <div class="text-red-700">
      <h2><%= pluralize(visible_errors.count, "error") %> prohibited this person from being saved:</h2>

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

  ...

<% end %>

Pros:

  • This approach leaves all other errors and messages untouched
  • It can scale up to as many fields as needed (e.g. if three or more fields are interdependent... although then you might have other usability concerns)
  • It's self documenting
  • It's fairly consistent with The Rails Way

Cons:

  • Additional logic in the view (could potentially be extracted into a ViewComponent)
  • Can't custom colour the fields* (e.g. to highlight as a warning instead of an error)

* You can modify the way Rails tags the error fields by overriding Rails.application.config.action_view.field_error_proc but so far in my research I have not seen a way to access the error type from that scope. If we knew the only error on that field was of type :highlight_field_but_hide_message we could highlight it with a warning colour instead of an error colour.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1