【jQuery】RailsでValidation Pluginを使った動的なバリデーションチェックの実装 〜詳細実装/Bootstrap編〜

# はじめに

[前回の記事](https://qiita.com/tiphp452/items/2ac12f7d95a230ef9d02)では導入編をまとめましたので本記事では詳細実装編をまとめます。後半では以下の場合についてもまとめています。

+ Bootstrapを使用している場合
+ deviseを使用している場合
+ 正規表現チェックメソッドのサンプル一覧

# 開発環境
Ruby 2.5.1
Rails 5.0.7.2
jQuery 3.4.1
jQuery Validation Plugin 1.19.1
Bootstrap 4.3.1

# 実装手順

入力フォームを実装済み、プラグインを導入済みであることを前提としています。

## パスワードと確認用パスワードのバリデーションチェック

### 完成イメージ
[![Image from Gyazo](https://i.gyazo.com/d36d27f39c5a2170c757e1d9867cbf61.gif)](https://gyazo.com/d36d27f39c5a2170c757e1d9867cbf61)

### 解説・サンプルコード
eqaulToはデフォルトで使えるメソッドです。指定したidの入力欄と入力値が一致しているかをチェックできます。以下が本箇所のJSファイルの記述サンプルです。

```jquery.validate.handler.user.js
$(function () {
  // メソッドの定義
  var methods = {
    password: function (value, element) { // パスワードの正規表現 
      return this.optional(element) || /^(?=.*?[a-z])(?=.*?\d)[a-z\d]{8,100}$/i.test(value);
    },
  }
  // メソッドの追加
  $.each(methods, function (key) {
    $.validator.addMethod(key, this);
  });
  // バリデーションの実行
  $("#signup-form, #charge-form").validate({
    // ルール設定
    rules: {
      "user[password]": {
        required: true, // 入力必須
        password: true // 正規表現
      },
      "user[password_confirmation]": {
        required: true, // 入力必須
        password: true, // 正規表現
        equalTo: "#password" // パスワードと確認用パスワードが一致しているかチェック
      },
    },
    // エラーメッセージの定義
    messages: {
      "user[password]": {
        required: "パスワードを入力してください",
        password: "英字と数字両方を含むパスワードを入力してください"
      },
      "user[password_confirmation]": {
        required: "確認用パスワードを入力してください",
        password: "英字と数字両方を含むパスワードを入力してください",
        equalTo: "パスワードが一致していません"
      },
    },
    errorClass: "invalid",
    errorElement: "p",
    validClass: "valid",
  });
  // 入力欄をフォーカスアウトしたときにバリデーションを実行
  $("#password, #password_confirmation").blur(function () {
    $(this).valid();
  });
});
```

## プルダウンリストの選択状況確認
プルダウンリストのバリデーションチェックを実装します。
OK/NGの基準を以下のように設けました。

+ OK：プルダウンダウンリストが選択されて初期値から値が変わっている
+ NG：初期値から値が変わっていない

```sample.html.haml
= form_tag(signup_index_path, method: :post, id:"charge-form", name: "inputForm", class: "pay_way__main__form") do
  %div.pay_way__main__form__content
    %div.pay_way__main__form__content__group
      %label.pay_way__main__form__content__group__label  有効期限
      %span.pay_way__main__form__content__group__require   必須
      .pay_way__main__form__content__group__expire
        .pay_way__main__form__content__group__expire__select
          %i.fas.fa-chevron-down.fa-lg.pay_way__main__form__content__group__expire__select__icon
          %select#exp_month{name: "exp_month", type: "text", class: "pay_way__main__form__content__group__expire__select__pulldown", name: "exp_month" }
            %option{value: ""} --
            %option{value: "1"}01
            %option{value: "2"}02
            %option{value: "3"}03
            %option{value: "4"}04
            %option{value: "5"}05
            %option{value: "6"}06
            %option{value: "7"}07
            %option{value: "8"}08
            %option{value: "9"}09
            %option{value: "10"}10
            %option{value: "11"}11
            %option{value: "12"}12
        %span.pay_way__main__form__content__group__expire__select__text  月
      .pay_way__main__form__content__group__expire
        .pay_way__main__form__content__group__expire__select
          %i.fas.fa-chevron-down.fa-lg.pay_way__main__form__content__group__expire__select__icon
          %select#exp_year{name: "exp_year", type: "text", class:"pay_way__main__form__content__group__expire__select__pulldown", name: "exp_year" }
            %option{value: ""} --
            %option{value: "2019"}19
            %option{value: "2020"}20
            %option{value: "2021"}21
            %option{value: "2022"}22
            %option{value: "2023"}23
            %option{value: "2024"}24
            %option{value: "2025"}25
            %option{value: "2026"}26
            %option{value: "2027"}27
            %option{value: "2028"}28
            %option{value: "2029"}29
        %span.pay_way__main__form__content__group__expire__select__text  年
      #exp_date_error
```

```jquery.validate.handler.user.js
$(function () {
  // メソッドの定義
  var methods = {
    valueNotEquals: function (value, element, arg) { // プルダウンリストが選択されているかの確認
      return arg !== value;
    },
  }
  // メソッドの追加
  $.each(methods, function (key) {
    $.validator.addMethod(key, this);
  });
  // バリデーションの実行
  $("#charge-form").validate({
    // ルール設定
    rules: {
      exp_month: {
        valueNotEquals: ""
      },
      exp_year: {
        valueNotEquals: ""
      }
    },
    // エラーメッセージの定義
    messages: {
      exp_month: {
        valueNotEquals: "有効期限を選択してください"
      },
      exp_year: {
        valueNotEquals: "有効期限を選択してください"
      }
    },
    groups: { //グループ化
      exp_date: "exp_month exp_year"
    },
    errorClass: "invalid",
    errorElement: "p",
    validClass: "valid",
    // エラーメッセージ表示位置のカスタム設定
    errorPlacement: function (error, element) {
      if (element.attr("name") == "exp_month" || element.attr("name") == "exp_year") {
        error.insertAfter("#exp_date_error");
      }
      else {
        error.insertAfter(element);
      }
    }
  });
  // 選択欄をフォーカスアウトしたときにバリデーションを実行(ウィザードページ毎)
  $("#exp_month, #exp_year").blur(function () {
    $(this).valid();
  });
});

```


## エラーメッセージの表示位置の設定

入力欄が横並びになっている場合、エラーメッセージはデフォルトでは各入力欄の直後にエラーメッセージが表示されます。そのため、入力欄が縦並びに変わってしまいます。そのようなときは本設定でメッセージの位置を指定できます。

### 完成イメージ

[![Image from Gyazo](https://i.gyazo.com/34a975d0f86608df6f443cea0ee8ed9a.gif)](https://gyazo.com/34a975d0f86608df6f443cea0ee8ed9a)

### 解説・サンプルコード

以下のようにエラーメッセージを表示させたい場所の直前にidを付与した要素を設けます。

```sample.html.haml
= f.text_field :lastname, class: "クラス名", placeholder:"例）山田", id: "lastname"
= f.text_field :firstname, class: "クラス名", placeholder:"例）彩", id: "firstname"
%div.#name_error
# 〜エラーを表示させたい場所〜
```

errorPlacementを使用して場所を指定します。

```jquery.validate.handler.user.js
$(function () {
  // バリデーションの実行
  $("#signup-form").validate({
    // ルール設定
    rules: {
      "user[lastname]": {
        required: true
      },
      "user[firstname]": {
        required: true
      }
    },
    // エラーメッセージの定義
    messages: {
      "user[lastname]": {
        required: "姓を入力してください"
      },
      "user[firstname]": {
        required: "名を入力してください"
      },
    errorClass: "invalid",
    errorElement: "p",
    validClass: "valid",
    // エラーメッセージ表示位置のカスタム設定
    errorPlacement: function (error, element) {
      if (element.attr("name") == "user[lastname]" || element.attr("name") == "user[firstname]") {
        error.insertAfter("#name_error"); // 指定した要素の後ろにエラーを表示
      } else {
        error.insertAfter(element);
      }
    }
  });
  // 入力欄をフォーカスアウトしたときにバリデーションを実行
  $("#lastname, #firstname").blur(function () {
    $(this).valid();
  });
});
```

## Bootstrapを使用している場合
Bootstrapを使用している場合、バリデーションチェック後のクラス名追加にBootstrapのValidationのclass名を指定することで使用することができます。

[Bootstrap 公式リファレンス(forms)](https://getbootstrap.com/docs/4.4/components/forms/)
[Bootstrap 日本語リファレンス(forms)](https://getbootstrap.jp/docs/4.4/components/forms/)

### 完成イメージ

[![Image from Gyazo](https://i.gyazo.com/3ddf6b463235091801e9b033f288aeab.gif)](https://gyazo.com/3ddf6b463235091801e9b033f288aeab)

### 解説・サンプルコード

Bootstrapを適用したフォームを用意します。

```app/views/devise/sessions/new.html.erb
<div class="form-label-group">
  <%= f.email_field :email, placeholder: "Email Address", class: 'form-control' %>
  <%= f.label :email %>
</div>

<div class="form-label-group">
  <%= f.password_field :password, placeholder: "Password", class: 'form-control' %>
  <%= f.label :password %>
</div>
```

JSファイルでerrorClassとvalidClassを下記のように設定します。

```jquery.validate.handler.user.js
// 〜省略〜
errorClass: "is-invalid"
validClass: "is-valid"
// ~省略〜
```

## deviseを使用している場合
deviseを使用している場合でユーザー情報の変更において下記2つの入力パターンが想定されます。

+ パスワードを変更しない場合：現在のパスワードの入力が必須
+ パスワードを変更する場合：確認用パスワードが必須、現在のパスワードの入力は不要

バリデーションチェックを上記どちらでも対応できるように実装します。

### 完成イメージ

#### パスワードを変更しない場合：現在のパスワードの入力が必須

[![Image from Gyazo](https://i.gyazo.com/3b17f302e0c5c6ab845fade85b85fbf5.gif)](https://gyazo.com/3b17f302e0c5c6ab845fade85b85fbf5)

#### パスワードを変更する場合：確認用パスワードが必須、現在のパスワードの入力は不要

[![Image from Gyazo](https://i.gyazo.com/ba42f7b2e662cd497b2e8cb2f15c0170.gif)](https://gyazo.com/ba42f7b2e662cd497b2e8cb2f15c0170)

### 解説・サンプルコード

```jquery.validate.handler.user.js
// 〜省略〜
"user[password]": { // requiredは設定しない
  password: true
},
"user[password_confirmation]": { // requiredは設定しない
  password: true,
  equalTo: "#user_password" // 新しいパスワードと一致しているか確認
},
"user[current_password]": {
  password: true,
  required: function (element) { // 新しいパスワード欄が空欄の場合は入力必須にする
    return $("#user_password").val() == "";
  }
},
// 〜省略〜
```

## 正規表現チェックメソッド一覧

今回の開発で様々な正規表現チェックを実装したので、それもまとめたいと思います。

```jquery.validate.handler.user.js
  // メソッドの定義
  var methods = {
    email: function (value, element) { // メールアドレスの正規表現 
      return this.optional(element) || /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/i.test(value);
    },
    password: function (value, element) { // パスワードの正規表現 
      return this.optional(element) || /^(?=.*?[a-z])(?=.*?\d)[a-z\d]{8,100}$/i.test(value);
    },
    phone: function (value, element) { // 電話番号の正規表現
      return this.optional(element) || /^0\d{9,10}$/.test(value);
    },
     // クレジットカード番号の正規表現
     // VISA, MasterCard, Discover, Diners, Amex, JCBの番号規則に対応
    cardNumber: function (value, element) {
      return this.optional(element) || /^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6011[0-9]{12}|3(?:0[0-5]|[68][0-9])[0-9]{11}|3[47]{13}|(?:2131|1800|35[0-9]{3})[0-9]{11})$/.test(value);
    },
    cvc: function (value, element) { // セキュリティコードの正規表現
      return this.optional(element) || /^\d{3,4}$/.test(value);
    },
    postalCode: function (value, element) { // 郵便番号の正規表現
      return this.optional(element) || /^\d{3}[-]\d{4}$/.test(value);
    },
    kana: function (value, element) { // カタカナの正規表現
      return this.optional(element) || /^[ァ-ヴ]+$/.test(value);
    },
  }
```

# まとめ

デフォルトで様々なメソッドが準備されていますが、カスタムメソッドを作ることができるため様々なバリデーションチェックが実装できます。Bootstrapのクラス名を組み込めばBootstrapと連携可能です。

# 参考URL

[正規表現を可視化してまとめたチートシート](https://qiita.com/sea_ship/items/0b35b5c1c98eebfa5128#%E3%82%AF%E3%83%AC%E3%82%B8%E3%83%83%E3%83%88%E3%82%AB%E3%83%BC%E3%83%89)
[jquery.validate.jsでセレクトボックスのチェック、他カスタマイズ色々](https://qiita.com/ma_me/items/9f2473c4b2c9b5a87dac)