【Rails】バリデーションエラーをそのインプットフィールドの近くに1つだけ表示したかった。


この記事でやりたいこと

この記事では、Railsのフォームのエラーメッセージをまとめて出すのではなく、各インプットフィールドの近くに出すにはどうすればいいかやってみた結果を残しておきます。(やりたいことのイメージは下の方に結果画像あります。)

環境

この記事を書くにあたり、私の環境は以下の通りです。

  • Ruby 2.7.1
  • Rails 6.0.3

サンプルアプリの前提

今回は単純に「商品名(name)」と「値段(price)」を属性にもつ商品(Product)を管理するアプリを前提とします。
登録フォームで「商品名」と「値段」を入力できるようにしますが、それぞれのバリデーションに引っかかった場合にそれぞれのインプットフィールドに対してエラーメッセージを表示できるようにします。
また、一つのインプットフィールドに複数のバリデーションエラーメッセージが表示されるのも訳がわからなくなってしまうので、1つのエラーメッセージだけを表示するようにします。

それぞれの属性は以下のバリデーションを持っていることとします。

名前(name)

  • 未入力はだめ。
  • 20文字以内じゃないとだめ。

値段(price)

  • 未入力はだめ。
  • 100以上じゃないとだめ。
  • 100000未満じゃないとだめ。

このProductモデルを管理するscaffoldをサンプルアプリとします。

$ rails g scaffold product name:string value:integer
$ rails db:migrate
app/models/product.rb
class Product < ApplicationRecord
  validates :name,
    presence: true,
    length: {maximum: 20}

  validates :price,
    numericality: {
      greater_than_or_equal_to: 100,
      less_than: 100000
    }
end

このアプリでは、/products/new/ページでnamepriceの入力をミスるとページトップにエラーが表示されるようになっています。

エラーメッセージの表示については、app/views/products/_form.html.erbの部分テンプレートで定義されています。

app/views/products/_form.html.erb
<%= form_with(model: product, local: true) do |form| %>
  <%# ここからエラーメッセージ %>
  <% if product.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(product.errors.count, "error") %> prohibited this product from being saved:</h2>

      <ul>
        <% product.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>
  <%# ここまでエラーメッセージ %>

  <div class="field">
    <%= form.label :name %>
    <%= form.text_field :name %>
  </div>

  <div class="field">
    <%= form.label :price %>
    <%= form.number_field :price %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

実装例

このapp/views/products/_form.html.erbを編集して、nameのエラーメッセージはnameのインプットフィールドの下に、priceのエラーメッセージはpriceのインプットフィールドの下に表示するようにします。

現状のapp/views/products/_form.html.erbでは

  • product.errors.any?で全ての属性のうち1つでもエラーがあったかどうかをチェック
  • product.errors.full_messagesで全ての属性に関するエラーメッセージを表示

しています。
つまり、属性単位にエラー有無を判別して、属性単位にエラーメッセージを取得できればよさそうです。

属性単位にエラー有無を判別するinclude?

include?(attribute)を用いることで属性単位のエラー有無を判別することができます。
attributeに関するエラーがモデルオブジェクトのエラーオブジェクトに格納されている場合trueを、そうでない場合はfalseを返却するメソッドです。
参考

属性単位にエラーメッセージを取得するfull_messages_for

full_messagesで全ての属性に関するエラーメッセージを取得できましたが、full_messages_for(attribute)を使うことで特定の属性に関するエラーメッセージを取得することができます。
参考

これらを組み合わせることでapp/views/products/_form.html.erbを以下のように編集します。

app/views/products/_form.html.erb
  <%= form_with(model: product, local: true) do |form| %>
    <% if product.errors.any? %>
       <div id="error_explanation">
        <h2><%= pluralize(product.errors.count, "error") %> prohibited this product from being saved:</h2>
-
-       <ul>
-         <% product.errors.full_messages.each do |message| %>
-           <li><%= message %></li>
-         <% end %>
-       </ul>
      </div>
    <% end %>

    <div class="field">
      <%= form.label :name %>
      <%= form.text_field :name %>
+     <% if product.errors.include?(:name) %>
+       <p style="color: red;"><%= product.errors.full_messages_for(:name).first %>
+     <% end %>
    </div>

    <div class="field">
      <%= form.label :price %>
      <%= form.number_field :price %>
+     <% if product.errors.include?(:price) %>
+       <p style="color: red;"><%= product.errors.full_messages_for(:price).first %>
+     <% end %>
    </div>

    <div class="actions">
      <%= form.submit %>
    </div>
  <% end %>

今回はfull_messages_forのエラーメッセージの配列から1つ目のメッセージだけを表示するようにfirstで配列の先頭のアイテムを取得しています。
この変更によってエラーメッセージの表示が以下のように変わります。

やりたいことができました。

Reference