Make it to make it

いろいろ作ってアウトプットするブログ

Vueでform validation

フォームの初期状態

下記のようなファイル構成。

.
├── app.js
└── index.html

index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Vue form validation</title>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
  <script src="https://unpkg.com/vue/dist/vue.js"></script>
</head>

<body>
  <div id="app">
    <div class="container">
      <h1>Vue form validation</h1>
      <form @submit.prevent="submitForm" autocomplete="off">
        <div class="form-group">
          <label for="name">Name(Only string):</label>
          <input v-model="form.name" class="form-control" id="name">
          <p v-if="!nameIsValid" class="error-message">The name field is required</p>
        </div>

        <div class="form-group">
          <label for="age">Age(Between 18 and 108):</label>
          <input v-model="form.age" class="form-control" id="age">
          <p v-if="!ageIsValid" class="error-message">The age field is invalid</p>
        </div>

        <div class="form-group form-group-last">
          <button class="btn btn-primary" :disabled="!formIsValid">Submit</button>
          <p v-show="form.sent" class="mt-2 text-info">Form has been successfully sent!</p>
        </div>
      </form>
    </div>
  </div>

  <script src="app.js"></script>
</body>

</html>

app.js

/* global Vue */
// eslint-disable-next-line no-unused-vars
const app = new Vue({
  el: '#app',
  data: {
    form: {
      name: null,
      age: null,
      sent: false
    }
  },
  computed: {
    nameIsValid () {
      return !Number(this.form.name) && !!this.form.name
    },
    ageIsValid () {
      return !!Number(this.form.age) && this.form.age >= 18 && this.form.age <= 108
    },
    formIsValid () {
      return this.nameIsValid && this.ageIsValid
    }
  },
  methods: {
    submitForm () {
      if (!this.formIsValid) return

      this.form.sent = true
    }
  }
})

内容としては、名前と年齢を入力してフォームを送信するというシンプルなもの。 名前には数字は入力できず、年齢には18-108間の数字のみが入力できる。

できるだけhtmlのinput側では制御せず、vueの機能を用いてリファクタリングしていってみる。

まずはinputへの型指定だが、v-modelに修飾子を付けることで行うことができる。

vuejs.org

v-model.numberで数値型に変換してくれるので、数値になってるかどうかのチェックが不要になる。

Before

<input v-model.number="form" class="form-control" id="age">
ageIsValid () {
   return !!Number(this.form.age) && this.form.age >= 18 && this.form.age <= 108
},

After

<input v-model.number="form.age" class="form-control" id="age">
ageIsValid () {
   return this.form.age >= 18 && this.form.age <= 108
},

しかしながら、初期状態でエラー文言が掲出されたままであったりして、まだこのままでは使い勝手がよくない。 一つひとつのエラー文言のハンドリングをしようとすると手間がかかってしまう。

そこで、Vueでvalidateのできるライブラリを用いる。

vuelidateを用いてリファクタリング

appVuelidate.vue

<template>
  <div id="app">
    <div class="container">
      <h1>Vue form validation</h1>
      <form @submit.prevent="submitForm" autocomplete="off">
        <!-- To validate on input synchronously, remove @blur and add v-model.trim="$v.form.name.$model" -->
        <!-- $error === $invalid && $dirty -->
        <div class="form-group">
          <label for="name">Name(Only string):</label>
          <input
            v-model.trim="form.name"
            @blur="$v.form.name.$touch()"
            :class="{ 'input-error': $v.form.name.$error, 'input-valid': !$v.form.name.$invalid }"
            class="form-control"
            id="name"
          />
          <template v-if="$v.form.name.$error">
            <p
              v-if="!$v.form.name.alpha"
              class="text-danger"
            >The name field should be alphabet characters</p>
            <p v-else class="text-danger">The name field is required</p>
          </template>
          <p
            class="text-info"
          >Invalid: {{ $v.form.name.$invalid }} | Dirty: {{ $v.form.name.$dirty }} | Error: {{ $v.form.name.$error }}</p>
        </div>
        <div class="form-group">
          <label for="age">Age(Between 18 and 108):</label>
          <input
            v-model.number.trim="form.age"
            @blur="$v.form.age.$touch()"
            :class="{ 'input-error': $v.form.age.$error, 'input-valid': !$v.form.age.$invalid }"
            class="form-control"
            id="age"
          />
          <template v-if="$v.form.age.$error">
            <p v-if="!$v.form.age.required" class="text-danger">The age field is required</p>
            <p
              v-else-if="!$v.form.age.integer"
              class="text-danger"
            >The age field should be an integer</p>
            <p
              v-else-if="!$v.form.age.between"
              class="text-danger"
            >You should be at least 18 and younger than 108 to coutinue</p>
          </template>
          <p
            class="text-info"
          >Invalid: {{ $v.form.age.$invalid }} | Dirty: {{ $v.form.age.$dirty }} | Error: {{ $v.form.age.$error }}</p>
        </div>
        <button :disabled="$v.form.$invalid" class="btn btn-primary">Submit</button>
        <p v-show="form.sent" class="mt-2 text-success">Form has been successfully sent!</p>
      </form>
    </div>
  </div>
</template>

<script>
import "bootstrap/dist/css/bootstrap.min.css";
import { validationMixin } from "vuelidate";
import * as validators from "vuelidate/lib/validators";

export default {
  data() {
    return {
      form: {
        name: null,
        age: null,
        sent: false
      }
    };
  },
  mixins: [validationMixin],
  validations: {
    form: {
      name: {
        required: validators.required,
        alpha: validators.alpha
      },
      age: {
        required: validators.required,
        integer: validators.integer,
        between: validators.between(18, 108)
      }
    }
  },
  methods: {
    submitForm() {
      this.$v.form.$touch();
      if (!this.$v.form.$invalid) {
        this.form.sent = true;
        return;
      }
      this.form.sent = false;
    }
  }
};
</script>

<style>
#app .input-valid {
  border-color: rgba(126, 239, 104, 0.8);
  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset,
    0 0 8px rgba(126, 239, 104, 0.6);
}
#app .input-error {
  border-color: tomato;
  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 8px tomato;
}
</style>

VeeValidateを用いてリファクタリング

appVeeValidate.vue

<template>
  <div class="container">
    <h1>Vue form validation</h1>
    <form @submit.prevent="submitForm" autocomplete="off">
      <div class="form-group">
        <label for="name">Name(Only alphabetic characters):</label>
        <input
          v-model.trim="form.name"
          v-validate="'alpha_spaces'"
          name="name"
          class="form-control"
          id="name"
        />
        <p class="text-danger">{{ errors.first('name') }}</p>
      </div>
      <div class="form-group">
        <label for="age">Age(Between 18 and 108):</label>
        <input
          v-model.number.trim="form.age"
          v-validate="'between:18,108'"
          name="age"
          class="form-control"
          id="age"
        />
        <p class="text-danger">{{ errors.first('age') }}</p>
      </div>
      <button class="btn btn-primary">Submit</button>
    </form>
  </div>
</template>

<script>
import { ValidationProvider } from "vee-validate";

export default {
  data() {
    return {
      form: {
        name: null,
        age: null,
        sent: false
      }
    };
  },
  components: {
    ValidationProvider
  },
  methods: {
    submitForm() {}
  }
};
</script>