【Vue3】Vueでユーザー登録のバリデーション

## 初めに
Vue.jsを使った簡単なアプリケーションのユーザー登録機能を作成した際に考えたことを纏めよう思います。
`veevalidate`みたいな便利なプラグインもあるのですが、勉強も兼ねて自分でバリデーション機能を作ってみようかと思いました。


## 環境

ruby 3.1.0
Rails 7.0.4
vue@3.2.41

## 実装イメージ
以下のようにユーザー登録画面があったとして、
そのバリデーション機能を`Vue`を使って実現したいと思います。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2508607/6d591020-0900-c46f-b863-a54349a195a8.png" width=50%>

## 要件

**姓名**
　・存在確認
　・20文字以内
**email**
　・存在確認
　・xxx@xxxx.yy の形式になる
　・ユニークになる
**パスワード**
　・存在確認
　・4文字以上
　・パスワード（確認）と同じになる

### データの流れ
・登録をクリックすると各attributeの値を確認し、バリデーションエラーが生じたらの内容を画面に描画

## 実装
著者は`importmap`を使用しているため、拡張子は`.vue`ではなく`.js`を使用しています。

ひとまず画面を構成するテンプレートは以下のような感じ（CSSは割愛）
```register.vue

const registerMain = {
  template:`
<div class="register">
  <p class="middle-title">アカウント登録</p>
  <!--名前-->
  <div class="form">
    <p class="form-label">氏名</p>
    <div class="name-form">
      <div class="form">
          <input type="text"
                 placeholder="姓"
                 v-model="first_name">
    <!--エラーメッセージ-->
    <div class="error-message">
      <p class="error-message">{{ validation.firstNameResult }}</p>
    </div>                    
  </div>

  <div class="form name-right">
    <input type="text"
           placeholder="名"
           v-model="last_name">
    <!--エラーメッセージ-->
    <div class="error-message">
      <p class="error-message">{{ validation.lastNameResult }}</p>
    </div>
  </div>

  <!--メールアドレス-->
  <div class="form">
    <p class="form-label">メールアドレス</p>
    <input type="email"
           class="form"
           placeholder="メールアドレス"
           v-model="email">
    <!--エラーメッセージ-->
    <div class="error-message">
      <p class="error-message">{{ validation.emailResult }}</p>
    </div>
  </div>

  <!--パスワード-->
  <div class="form">
    <p class="form-label">パスワード</p>
    <input type="password"
           class="form"
           placeholder="パスワード"
           v-model="password">
    <!--エラーメッセージ-->
    <div class="error-message">
      <p class="error-message">{{ validation.passwordResult }}</p>
    </div>
  </div>

  <!--パスワード（確認）-->
  <div class="form">
    <p class="form-label">パスワード（確認）</p>
    <input type="password"
           class="form"
           placeholder="パスワード（確認）"
           v-model="password_confirm">
    <!--エラーメッセージ-->
    <div class="error-message">
      <p class="error-message">{{ validation.passwordConfirmationResult }}</p>
    </div>
  </div>

  <!--ログインボタン-->
  <div class="btn">
    <a class="blue-btn login-btn"
        type="submit"
        @click="checkValidate()">
      登録</a>
  </div>
</div>
`,

  data() {
    return {
      isVisibleValidation: false,
      first_name: '',
      last_name: '',
      email: '',
      password: '',
      password_confirm: '',
      errorMessage: '',

      validation: {
        firstNameResult: '',
        lastNameResult: '',
        emailResult: '',
        passwordResult: '',
        passwordConfirmationResult: ''
      }
    }
  },

  methods: {
    checkValidate() {
      // ここにバリデーションの処理を書いていく
    }

  },

}
export default registerMain
```

実装方法は「登録」ボタンクリック時、`checkValidateメソッド`が発火して、値の確認を行います。
一旦は姓の値確認ロジックを書いていきます。

値の内容を`nameValidateメソッド`で確認し、バリデーションに引っかかった場合、そのエラーメッセージを画面に描画するための`data`に詰め込む。引っ掛からなかった場合、バリデーションメッセージをリセットします。


```vue.js
    checkValidate() {
      // 姓
      const first_name_error_message = this.nameValidate(this.first_name)
      if(first_name_error_message === true) {
        this.validation.firstNameResult = '';
      } else {
        this.validation.firstNameResult = first_name_error_message;
      }
    }
    
    nameValidate(name) {
      if(!name) {
        return '入力してください';
      }
      if(name.length > 20) {
        return '20文字以内で入力してください';
      }
      return true;
    },

```

ひとまずは、姓のバリデーションを実装することができました。
では、同じような処理を他の`attribute`にも展開していきます。

※emailのユニーク確認はDBに問い合わせする必要があるのでこのタイミングでは確認しません。


```vue.js
checkValidate() {
      // 姓
      const first_name_error_message = this.nameValidate(this.first_name)
      if(first_name_error_message === true) {
        this.validation.firstNameResult = '';
      } else {
        this.validation.firstNameResult = first_name_error_message;
      }
      
      // 名
      const last_name_error_message = this.nameValidate(this.last_name)
      if(last_name_error_message === true) {
        this.validation.lastNameResult = '';
      } else {
        this.validation.lastNameResult = last_name_error_message;
      }

      // email
      const email_error_message = this.emailValidate(this.email);
      if(email_error_message === true) {
        this.validation.emailResult = '';
      } else {
        this.validation.emailResult = email_error_message;
      }
      
      // パスワード
      const password_error_message = this.passwordValidate(this.password, this.password_confirm)
      if(password_error_message === true) {
        this.validation.passwordResult = '';
      } else {
        this.validation.passwordResult = password_error_message;
      }
      
      // パスワード（確認）
      const password_confirmation_error_message = this.passwordConfirmationValidate(this.password_confirm)
      if(password_confirmation_error_message === true){
        this.validation.passwordConfirmationResult = '';
      } else {
        this.validation.passwordConfirmationResult = password_confirmation_error_message;
      }
    },

    // emailの値確認
    emailValidate(email) {
      if(!email) {
        return '入力してください';
      }
      const regex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i;
      if (!regex.test(email)) {
        return '正しく入力してください';
      }
      return true
    },

    // 名前の値確認
    nameValidate(name) {
      if(!name){
        return '入力してください';
      }
      if(name.length > 20){
        return '20文字以内で入力してください';
      }
      return true;
    },

    // パスワードの値確認
    passwordValidate(password, password_confirmation) {
      if(!password) {
        return '入力してください';
      }
      if(password.length < 4) {
        return '4文字以上で入力してください';
      }
      if(password !== password_confirmation) {
        return 'パスワードとパスワード（確認）が違います';
      }
      return true;
    },

    // 確認用パスワードの値確認
    passwordConfirmationValidate(password_confirmation) {
      if(!password_confirmation) {
        return '入力してください';
      }
      return true;
    }

```

はい、冗長ですね^^;


以下の処理が非常に似通っていて、何度も登場しているのが気になります。
```vue.js
     const first_name_error_message = this.nameValidate(this.first_name)
      if(first_name_error_message === true) {
        this.validation.firstNameResult = '';
      } else {
        this.validation.firstNameResult = first_name_error_message;
      }
```
ここの処理を全ての`attribute`に使えるように共通化していきたいと思います。
そのためには、`data`の持ち方を変えてあげる必要があるので`data`を以下のように修正します。

`form_attributes`という`form`の`attribute`の上位要素を用意します。
※dataも変更したのでテンプレートの修正も忘れずに
```vue.js
  data() {
    return {
      form_attributes: {
        first_name: {
          value: '',
          error_message: ''
        },
        last_name: {
          value: '',
          error_message: ''
        },
        email: {
          value: '',
          error_message: ''
        },
        password: {
          value: '',
          error_message: ''
        },
        pasword_confirmation: {
          value: '',
          error_message: ''
        }
      },
    }
  },
```

共通化したロジックが以下のようになります。
`form_attributes`の要素一つずつを`forEach`を使うことで順番にバリデーションに掛けています。


```vue.js
checkValidate(){
    // クリック時バリデーション発火
    checkValidate() {
      Object.keys(this.form_attributes).forEach(function (key) {
        const error_message = this.Validation(key, this.form_attributes[key].value)
        if (error_message === true) {
          this.form_attributes[key].error_message = '';
        } else {
          this.form_attributes[key].error_message = error_message;
        }
      }, this)
    },

    // どのattributeをバリデーションするか条件分岐
    Validation(attribute, value) {
      switch (attribute) {
        case 'first_name':
        case 'last_name':
          return this.nameValidate(value);
          break;
        case 'email':
          return this.emailValidate(value);
          break;
        case 'password':
          return this.passwordValidate(value);
          break;
        case 'password_confirmation':
          return this.passwordConfirmationValidate(value);
          break;
        default:
          return true;
      }
    },

    // emailの値確認
    emailValidate(email) {
     // 略
    },

    // 名前の値確認
    nameValidate(name) {
     // 略
    },

    // パスワードの値確認
    passwordValidate(password, password_confirmation) {
     // 略
    },

    // 確認用パスワードの値確認
    passwordConfirmationValidate(password_confirmation) {
     // 略
    }

```


以下のロジックだけ少々ややこしいので少し説明します。
```vue.js
    checkValidate() {
      Object.keys(this.form_attributes).forEach(function (key) {
        const error_message = this.Validation(key, this.form_attributes[key].value)
        if (error_message === true) {
          this.form_attributes[key].error_message = '';
        } else {
          this.form_attributes[key].error_message = error_message;
        }
      }, this)
    },
```

JSではオブジェクト（[object Object]）に対し、`forEach`を使うことはできません。
というのも、`forEach`はメソッドだからです。
メソッドはそのメソッドを持っているものにしか使うことはできません。
`forEach`を持っているオブジェクトは`Array`とか`Map`オブジェクトであり
オブジェクトには備わっていません。

よって、オブジェクトに対し、`forEach`を使っても、「そんなメソッドないよ」と怒られてしまいます。
その解決方法として、**オブジェクトを配列にしてからループさせる**という方法があります。

オブジェクトを配列に変換するメソッドは以下の3つがあります。

```js
    Object.keys()
    Object.values()
    Object.entries()
```
その中の`Object.keys()`を使いました。
`Object.keys()`は、オブジェクトのキーの配列を返すので、`forEach`が使えるようになります。

また、第２引数に`this`を使っています。
`Vue.js`の`this.data`を`forEach`で使用するときは、少々注意が必要になります。
[※こちらの記事で勉強させていただきました。](https://qiita.com/beanzou/items/1d161758f67a438029bd)
```vue.js
     Object.keys(this.form_attributes).forEach(function (key) {
        console.log(key, this.form_attributes[key])
      }, this)
```
`forEach`は、第二引数に定義したものを第一引数の関数内で`this`として取得することができます。
つまり、`forEach`は、第二引数に`this`をセットすることで、`Vue.js`でよく使う
`this.メソッド（）`を使うことができるようになります。

### コード全体

```vue.js

const registerMain = {
  template:
    `
      <div class="register">
        <p class="middle-title">アカウント登録</p>
        <!--名前-->
        <div class="form">
          <p class="form-label">氏名</p>
          <div class="name-form">
            <div class="form">
              <input type="text"
                     placeholder="姓"
                     v-model="form_attributes.first_name.value">
              <!--エラーメッセージ-->
              <div class="error-message">
                <p class="error-message">{{ form_attributes.first_name.error_message }}</p>
              </div>
            </div>

            <div class="form name-right">
              <input type="text"
                     placeholder="名"
                     v-model="form_attributes.last_name.value">
              <!--エラーメッセージ-->
              <div class="error-message">
                <p class="error-message">{{ form_attributes.last_name.error_message }}</p>
              </div>
            </div>
          </div>

        </div>

        <!--メールアドレス-->
        <div class="form">
          <p class="form-label">メールアドレス</p>
          <input type="email"
                 class="form"
                 placeholder="メールアドレス"
                 v-model="form_attributes.email.value">
          <!--エラーメッセージ-->
          <div class="error-message">
            <p class="error-message">{{ form_attributes.email.error_message }}</p>
          </div>
        </div>

        <!--パスワード-->
        <div class="form">
          <p class="form-label">パスワード</p>
          <input type="password"
                 class="form"
                 placeholder="パスワード"
                 v-model="form_attributes.password.value">
          <!--エラーメッセージ-->
          <div class="error-message">
            <p class="error-message">{{ form_attributes.password.error_message }}</p>
          </div>
        </div>

        <!--パスワード（確認）-->
        <div class="form">
          <p class="form-label">パスワード（確認）</p>
          <input type="password"
                 class="form"
                 placeholder="パスワード（確認）"
                 v-model="form_attributes.password_confirmation.value">
          <!--エラーメッセージ-->
          <div class="error-message">
            <p class="error-message">{{ form_attributes.password_confirmation.error_message }}</p>
          </div>
        </div>

        <!--ログインボタン-->
        <div class="btn">
          <a class="blue-btn login-btn"
             type="submit"
             @click="checkValidate()">
            登録</a>
        </div>
      </div>
    `,

  data() {
    return {
      form_attributes: {
        first_name: {
          value: '',
          error_message: ''
        },
        last_name: {
          value: '',
          error_message: ''
        },
        email: {
          value: '',
          error_message: ''
        },
        password: {
          value: '',
          error_message: ''
        },
        password_confirmation: {
          value: '',
          error_message: ''
        }
      },
    }
  },

  methods: {
    // クリック時バリデーション発火
    checkValidate() {
      Object.keys(this.form_attributes).forEach(function (key) {
        const error_message = this.Validation(key, this.form_attributes[key].value)
        if (error_message === true) {
          this.form_attributes[key].error_message = '';
        } else {
          this.form_attributes[key].error_message = error_message;
        }
      }, this)
    },

    // attributeごとにバリデーション
    Validation(attribute, value) {
      switch (attribute){
        case 'first_name':
        case 'last_name':          
          return this.nameValidate(value);
          break;
        case 'email':
          return this.emailValidate(value);
          break;
        case 'password':
          return this.passwordValidate(value);
          break;
        case 'password_confirmation':
          return this.passwordConfirmationValidate(value);
          break;
        default:
          return true;
      }
    },

    emailValidate(email) {
      if (!email) {
        return '入力してください';
      }
      const regex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i;
      if (!regex.test(email)) {
        return '正しく入力してください';
      }
      return true
    },
    nameValidate(name) {
      if (!name) {
        return '入力してください';
      }
      if (name.length > 20) {
        return '20文字以内で入力してください';
      }
      return true;
    },

    passwordValidate(password) {
      if (!password) {
        return '入力してください';
      }
      if (password.length < 4) {
        return '4文字以上で入力してください';
      }
      if (password !== this.form_attributes.password_confirmation.value) {
        return 'パスワードとパスワード（確認）が違います';
      }
      return true;
    },

    passwordConfirmationValidate(password_confirmation) {
      if (!password_confirmation) {
        return '入力してください';
      }
      return true;
    }

  },
}
export default registerMain
```

これで、`controller`にポストするまでに、値を確認し、
バリデーションメッセージを表示することができました。

`password`と`password（確認）`の比較だけ、`data`を参照してしまうのが少し悔しい。

動作は以下のような感じ。

![画面収録_2023-01-21_16_03_54_AdobeExpress (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2508607/2a8c1983-99f8-0571-4dde-96a84e479467.gif)

## 最後に
ここまで見ていただきありがとうございます。m(__)m
コードもそれなりの量になってしまったので、もっと簡潔にかける方法があれば教えていただけると幸いです。

`Vue.js`は`veevalidate`みたいに便利なプラグインがあり、そっちを使った方が簡潔に書けそうな感じがします。