【Vue.js】Formバリデーションを作ってみた【Javascript】

# はじめに
インフラ系IT企業を2年でやめ、受託開発会社に勤務して約7ヶ月の3年目のエンジニアです。
今はほとんどインフラはやっておらず、フロントエンドばっかり書いています。勉強のためバリデーションでも作って見よう！と思って作りました。
※バリデーション自体はvuetifyやプラグイン使えばかんたんに実装できますが、自分で作るのって結構大事ですよね、プラグインに依存しないですし。

## 機能
1.フォームの送信ボタンをクリックするとバリデーションが発火する。
2.入力必須、文字数、形式(stringとかnumber)をチェックする。
3.エラーが発生してる状態だと送信できないようにする。

###見た目
<img width="1463" alt="スクリーンショット 2020-02-27 13.45.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322330/92045edd-3d61-cf60-8512-c7745c3f9ab0.png">


formの見た目は適宜調整してください。

まずは、vueのformのソースからです。
formの部分は結構見ずらいので、コンポーネントに分けた方が良いと思います。

# コード
## Form
```vue
<template>
  <div class="form">
      <div class="form-item">
        <div class="form-item__label">
          {{ input.name.name }}
          <span class="form-item__require" v-if="input.name.require">必須</span>
        </div>
        <div class="form-item__input">
          <div class="underline">
            <input v-model="input.name.value" :id="input.name.name" type="text" placeholder />
            <label :for="input.name.name">{{ input.name.name }}</label>
          </div>
          <div class="validation-message">{{input.name.errorMessage[0]}}</div>
        </div>
      </div>
      <div class="form-item">
        <div class="form-item__label">
          {{ input.name_kana.name }}
          <span class="form-item__require" v-if="input.name_kana.require">必須</span>
        </div>
        <div class="form-item__input">
          <div class="underline">
            <input v-model="input.name_kana.value" :id="input.name_kana.name" type="text" placeholder />
            <label :for="input.name_kana.name">{{ input.name_kana.name }}</label>
          </div>
          <div class="validation-message">{{input.name_kana.errorMessage[0]}}</div>
        </div>
      </div>
      <div class="form-item">
        <div class="form-item__label">
          {{ input.mail.name }}
          <span class="form-item__require" v-if="input.mail.require">必須</span>
        </div>
        <div class="form-item__input">
          <div class="underline">
            <input v-model="input.mail.value" :id="input.mail.name" type="text" placeholder />
            <label :for="input.mail.name">{{ input.mail.name }}</label>
          </div>
          <div class="validation-message">{{input.mail.errorMessage[0]}}</div>
        </div>
      </div>
        <button type="button" class="submit-btn" @click="submit">登録</button>
  </div>
</template>

<script>
import {validation} from '@/utilities/validation/validation.js';
const form = {
  name: 'aaaaaaa',
  name_kana: 'a',
  mail: 'testes@gmail.com',
}

export default {
  data() {
    return {
      form,
      input: {
        name: {
          name: '名前',
          value: '',
          length: [1, 28],
          require: true,
          errorMessage: []
        },
        name_kana: {
          name: '名前(かな)',
          value: '',
          length: [1, 28],
          format: 'Hiragana',
          require: true,
          errorMessage: []
        },
        mail: {
          name: 'メールアドレス',
          value: '',
          format: 'Mail',
          length: [1, 28],
          require: false,
          errorMessage: []
        },
      }
    };
  },
  watch: {
    form: {
      handler(newValue) {
        for(let key in this.input) {
            this.input[key].value = newValue[key];
        }
      },
      immediate: true
    }
  },
  methods: {
    handleValidate(result){
      return validation(this.input);
    },
    submit() {
      const result = this.handleValidate(this.input);
      if (result.validStatus) {
        console.log('送信')
      }
    }
  }
};
</script>

<style lang="scss">
.form{
  padding: 24px 120px;
}
.form-item {
  min-height: 72px;
  padding: 16px 0;
  display: flex;
  &__label {
    font-size: 13px;
    color: black;
    text-align: left;
    font-weight: 600;
    text-align: right;
    min-width: 140px;
    max-width: 140px;
    margin-right: 12px;
  }
  &__input{
    width: 100%;
  }
  &__require{
    background-color: red;
    font-size: 10px;
    color: white;
    border-radius: 4px;
    display: inline-block;
    padding: 1px 4px;
    vertical-align: middle;
  }
}

.validation-message {
  font-size: 12px;
  color: red;
  margin-top: 8px;
  text-align: left;
}
.underline {
  border-bottom: 2px solid #bbb;
  padding: 0 8px 2px 8px;
  position: relative;
  &::before {
    left: 0;
    content: "";
    height: 5px;
    width: 2px;
    position: absolute;
    bottom: 0;
    background-color: #bbb;
  }
  &::after {
    content: "";
    height: 5px;
    width: 2px;
    position: absolute;
    right: 0;
    bottom: 0;
    background-color: #bbb;
  }
  input {
    position: relative;
    border: none;
    border-image-width: 0;
    outline: none;
    margin: 0;
    font-size: 12px;
    padding: 0;
    z-index: 1;
    -webkit-appearance: none;
    border-radius: 0;
    background-color: transparent;
    width: 100%;
    display: block;
    box-sizing: border-box;
    color: #111;
    font-family: "Roboto", sans-serif;
    font-weight: bold;
  }
  label {
    width: 100%;
    padding-bottom: 2px;
    cursor: default;
    font-weight: bold;
    position: absolute;
    font-size: 12px;
    font-family: "Roboto", sans-serif;
    top: -14px;
    left: 8px;
    opacity: 0;
    color: #777;
  }
}
.placeholder {
  user-select: none;
  position: absolute;
  left: 8px;
  bottom: 8px;
  color: #777;
  font-family: "Roboto", sans-serif;
  font-weight: normal;
  font-size: 18px;
  z-index: 0;
  opacity: 0.99;
}
.submit-btn {
  margin: 0 auto;
	display: block;
	position: relative;
	width: 160px;
	padding: 0.8em;
	text-align: center;
	text-decoration: none;
	color: #fff;
	background: #3A8BA8;
  &:hover {
	cursor: pointer;
	text-decoration: none;
	background:#fff;
  color:  #3A8BA8;
  border:solid 1px #3A8BA8;
  }
}

</style>
```

まず data() オプションのinputオブジェクトにv-modelやバリデーションで使う値を定義しています。
名前のフォームを例に上げて説明すると、、、

```vue
 name: {
    name: '名前', // フォームの名前を定義します。エラーメッセージやラベルで使用されます。
    value: '', // v-modelで使用します。
    length: [1, 28], // 入力値の長さを確認に使用します。1つ目の数字が最小文字数、２つ目は最大文字数です。定義しない場合はチェックを行いません。
    require: true, // 入力必須の確認に使用します。定義しない場合は入力必須チェックを行いません。
    errorMessage: [] //　エラーメッセージの表示に使用します。配列なのでv-forで回せば全てのエラーを表示できます。
 },
```

これらを定義したあと、｢登録｣ボタンが押されたタイミングでバリデーションのメソッドが発火します。エラーがあればメッセージが表示されます。
エラーが無ければ任意のメソッドを実行させることができます。

※watchでform変数をみて、inputに代入していますが、これは編集画面等でバリデーションしたい場合に使用します。
vuex等でvalueをstateに入れてそれをinputのvalueにのみ入れます。

## バリデーション本体
かなりコードが汚いとおもいますが、一応動きます。

```js
// formatチェックで使用する正規表現です。必要に応じてファイルを分けたり、追加が可能です。
const ValidateSchema = {
  Number: {
    schema: /^[0-9]*$/,
    errorMessage: "は数字で入力してください"
  },
  NumberHyphen: {
    // eslint-disable-next-line no-useless-escape
    schema: /^[0-9\-]+$/,
    errorMessage: "に数字とハイフン以外が入力されています"
  },
  Hiragana: {
    schema: /^[ぁ-んー]+$/,
    errorMessage: "はひらがなで入力してください"
  },
  Mail: {
    // eslint-disable-next-line no-useless-escape
    schema: /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/,
    errorMessage: "は正しい形式で入力してください"
  }
};

// 必須チェック
function requireCheck(input) {
  let result;
  let errorMessage;
    if (input.value.length <= 0 || input.value === null) {
      errorMessage = input.name + "は必須です。";
      result = { valid: false };
    } else {
      result = { valid: true };
      errorMessage = '';
    }
  return { errorMessage, ...result };
};

// 長さチェック
function lengthCheck(input) {
  const minLength = input.length[0];
  const maxLength = input.length[1];
  let errorMessage;
  let result;
  if (input.value.length >= minLength && input.value.length <= maxLength) {
    result = { valid: true };
    errorMessage = '';
  } else {
    errorMessage = input.name + "は" + minLength + "文字以上" + maxLength + "文字以下で入力してください";
    result = { valid: false };
  }
  return { ...result, errorMessage };
};

// フォーマットチェック
function formatCheck(input) {
  const formatType = input.format;
  let errorMessage;
  let result;
  if (formatType && !input.value == '' || !input.value == null) {
    const validate = ValidateSchema[formatType].schema.test(input.value);
    if (validate) {
      result = { valid: true };
      errorMessage = '';
    } else {
      errorMessage = input.name + ValidateSchema[formatType].errorMessage;
      result = { valid: false };
    }
  } else {
    result = { valid: true };
    errorMessage = "";
  }
  return { ...result, errorMessage };
};

// エラーを初期化するメソッド
function clearErrorMessage(input) {
  return input.errorMessage = [];
}

// バリデーションメソッド
export const validation = (input) => {
  // 最初はエラーは0
  let errors = 0;
  const result = input;
  for (let key in input) {
    //エラーを初期化
    clearErrorMessage(result[key], errors);
    // 入力必須が設定されていた場合のみ実行
    if (input[key].require) {
      const requireCheckResult = requireCheck(result[key]);
      if (requireCheckResult.valid != true) {
        result[key].errorMessage.push(requireCheckResult.errorMessage);
        errors++;
      }
    }
    // 入力必須が設定されていて、長さチェックが設定されていた場合のみ実行
    if (input[key].length && input[key].require) {
      const lengthCheckResult = lengthCheck(result[key]);
      if (lengthCheckResult.valid != true) {
        result[key].errorMessage.push(lengthCheckResult.errorMessage);
        errors++;
      }
    }
    // フォーマットが設定されていた場合のみ実行
    if (input[key].format) {
      const formatCheckResult = formatCheck(result[key]);
      if (formatCheckResult.valid != true) {
        result[key].errorMessage.push(formatCheckResult.errorMessage);
        errors++;
      }
    }
    if (errors == 0) {
      clearErrorMessage(result[key], errors);
    }
  }
  if (errors === 0) {
    return { validStatus: true };
  }
  return { validStatus: false, input: { ...result } };
};
```

コンポーネントの方で定義したinputのオブジェクトを単純にforでまわして一つひとつバリデーションしています。
最終的にerrorsが0になった場合のみvalidStatusをtrueにして返します。


# まとめ
かなり遠回りなコードだと思いますが、一応バリデーションを実行することができます。
改善することとして、バリデーションのスキーマをファイルに分けたり、formをコンポーネント化したりすることですね。
何か不具合等あればコメントいただければと思います。

今後もjavascript頑張っていきたいです。