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

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

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

- Ruby 2.7.1
- Rails 6.0.3

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

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

<b>名前（name）</b>

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

<b>値段（price）</b>

- 未入力はだめ。
- 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/`ページで`name`と`price`の入力をミスるとページトップにエラーが表示されるようになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/7d2c8e68-f18c-4492-9469-ea1d4ce59b38.png)

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

```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?`で全ての属性のうち１つでもエラーがあったかどうかをチェック
- `product.errors.full_messages`で全ての属性に関するエラーメッセージを表示

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

### 属性単位にエラー有無を判別する`include?`
`include?(attribute)`を用いることで属性単位のエラー有無を判別することができます。
`attribute`に関するエラーがモデルオブジェクトのエラーオブジェクトに格納されている場合`true`を、そうでない場合は`false`を返却するメソッドです。
[参考](https://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-include-3F)

### 属性単位にエラーメッセージを取得する`full_messages_for`
`full_messages`で全ての属性に関するエラーメッセージを取得できましたが、`full_messages_for(attribute)`を使うことで特定の属性に関するエラーメッセージを取得することができます。
[参考](https://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-full_messages_for)

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

```diff: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`のエラーメッセージの配列から１つ目のメッセージだけを表示するように`first`で配列の先頭のアイテムを取得しています。
この変更によってエラーメッセージの表示が以下のように変わります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254216/c264f8ac-4d7f-5e7e-2e63-8674330f433f.png)

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

# Reference
- [ActiveModel::Errors](https://api.rubyonrails.org/classes/ActiveModel/Errors.html)