Nuxt.js: VeeValidateでバリデーション (TypeScript)

Nuxt.jsアプリにおいて、VeeValidate はフォームバリデーションを容易に実現してくれるパッケージです。ここではNuxt.js + TypeScript + VeeValidate (ValidationObserver & ValidationProvider) の導入手順を紹介します。

Demoは こちら

パッケージバージョン情報

nuxt: 2.12.2
vee-validate: 3.3.0
TypeScript: 3.8.3
tailwindcss: 1.4.1
tailwindcss/custom-forms: 0.2.1

  1. VeeValidateのインストール
  2. Pluginの作成
  3. ValidationObserver & ValidationProviderの作成

VeeValidateのインストール

yarn add vee-validate

or

npm i -S vee-validate

Pluginの作成

veeValidate.ts

import Vue from 'vue'
import { ValidationProvider, ValidationObserver, extend } from 'vee-validate'
import * as rules from 'vee-validate/dist/rules'

// 全てのルールを利用
Object.keys(rules).forEach((rule) => {
  extend(rule, rules[rule])
})

Vue.component('ValidationProvider', ValidationProvider)
Vue.component('ValidationObserver', ValidationObserver)

nuxt.config.js

module.exports = {
  ...
  plugins: [
    { src: '~/plugins/veeValidate', ssr: false },
  ],
  ...
}

ValidationObserver & ValidationProviderの作成

ValidationObserver

ValidationObserverはこの内部で定義されるValidationProviderコンポーネント(バリデーション対象フォーム要素)の状態を監視します。

ValidationProviderでは、バリデーション対象となるinput, select, checkbox, radioなどのフォーム要素を定義し、そこにルール、エラーメッセージ等を適用させます。

今回は、 InputForm.vue, SelectForm.vue, CheckForm.vue, RadioForm.vue コンポーネントを用意しそれぞれで ValidationProviderを利用します。

ValidationObserverで管理される invalid dataを利用してボタンの活性化をコントロールします。

pages/validate.vue

<template>
  <div class="container">
    <validation-observer v-slot="{ invalid }" tag="div">
      <section class="mb-20">
        <div class="mb-10">
          <h2 class="mb-4 text-xl font-bold text-gray-700">
            <fa icon="leaf" aria-hidden="true" class="mr-2" />Input forms
          </h2>

          <div class="w-full mb-6">
            <input-form
              v-model="text"
              :error-messages="{
                required: '必須項目です',
                alpha_spaces: '半角英字で入力してください',
              }"
              placeholder="半角英字"
              field-name="Input Alpha"
              rules="required|alpha_spaces"
              type="text"
            />
          </div>

          <div class="w-full mb-6">
            <input-form
              v-model="email"
              :error-messages="{
                required: '必須項目です',
                email: '不正なメールアドレスです',
              }"
              placeholder="メールアドレス"
              field-name="Input email"
              rules="required|email"
              type="email"
            />
          </div>

          <div class="w-full mb-6">
            <input-form
              v-model="number"
              :error-messages="{
                required: '必須項目です',
                numeric: '数値で入力してください',
              }"
              placeholder="数値"
              field-name="Input number"
              rules="required|numeric"
              type="number"
              pattern="\d*"
            />
          </div>
        </div>

        <div class="mb-10">
          <h2 class="mb-4 text-xl font-bold text-gray-700">
            <fa icon="leaf" aria-hidden="true" class="mr-2" />Select form
          </h2>

          <div class="w-full mb-6">
            <select-form
              v-model="option1"
              :options="options1"
              :error-messages="{
                required: '必須項目です',
              }"
              placeholder="選択してください"
              field-name="Selectbox"
              rules="required"
            />
          </div>
        </div>

        <div class="mb-10">
          <h2 class="mb-4 text-xl font-bold text-gray-700">
            <fa icon="leaf" aria-hidden="true" class="mr-2" />Radio form
          </h2>

          <div class="w-full mb-6">
            <radio-form
              v-model="option2"
              :options="options2"
              :error-messages="{
                required: '必須項目です',
              }"
              placeholder="選択してください"
              field-name="Radio"
              rules="required"
            />
          </div>
        </div>

        <div class="mb-10">
          <h2 :class="['mb-4 text-xl font-bold text-gray-700']">
            <fa icon="leaf" aria-hidden="true" class="mr-2" />Checkbox form
          </h2>

          <div class="w-full mb-6">
            <check-form
              v-model="option3"
              :options="options3"
              :error-messages="{
                required: '必須項目です',
              }"
              placeholder="選択してください"
              field-name="Checkbox"
              rules="required"
            />
          </div>
        </div>

        <h2 class="mb-4 text-xl font-bold text-gray-700">
          <fa icon="leaf" aria-hidden="true" class="mr-2" />Password forms
        </h2>

        <div class="w-full mb-6">
          <input-form
            v-model="password"
            vid="confirmation"
            :error-messages="{
              required: '必須項目です',
              alpha_num: '半角英数字で入力してください',
              min: '8-20文字で入力してください',
              max: '8-20文字で入力してください',
            }"
            placeholder="パスワード"
            field-name="Input password"
            rules="required|alpha_num|min:8|max:20"
            type="password"
          />
        </div>

        <div class="w-full mb-6">
          <input-form
            v-model="passwordConfirmation"
            :error-messages="{
              required: '必須項目です',
              confirmed: 'パスワードが一致しません',
            }"
            placeholder="パスワード(確認)"
            field-name="Input password (confirmation)"
            rules="required|confirmed:confirmation"
            type="password"
          />
        </div>
      </section>

      <button
        :class="{ 'cursor-not-allowed opacity-50': invalid }"
        class="bg-teal-600 hover:bg-teal-500 text-white font-bold rounded w-full py-3"
        @click="onClickSubmit"
      >
        Submit
      </button>
    </validation-observer>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
import InputForm from '~/components/form/InputForm.vue'
import SelectForm from '~/components/form/SelectForm.vue'
import RadioForm from '~/components/form/RadioForm.vue'
import CheckForm from '~/components/form/CheckForm.vue'

type formOption = {
  label: string
  value: string
}

@Component({
  name: 'ValidateIndex',
  components: {
    InputForm,
    SelectForm,
    RadioForm,
    CheckForm,
  },
})
export default class Validate extends Vue {
  private text: string = ''
  private email: string = ''
  private number: string = ''
  private option1: string = ''
  private option2: string = ''
  private option3: string[] = []
  private password: string = ''
  private passwordConfirmation: string = ''

  private options1: formOption[] = [
    { label: 'Option 1', value: '1' },
    { label: 'Option 2', value: '2' },
    { label: 'Option 3', value: '3' },
    { label: 'Option 4', value: '4' },
    { label: 'Option 5', value: '5' },
  ]

  private options2: formOption[] = [
    { label: 'Option 1', value: '1' },
    { label: 'Option 2', value: '2' },
    { label: 'Option 3', value: '3' },
    { label: 'Option 4', value: '4' },
    { label: 'Option 5', value: '5' },
    { label: 'Option 6', value: '6' },
    { label: 'Option 7', value: '7' },
    { label: 'Option 8', value: '8' },
  ]

  private options3: formOption[] = [
    { label: 'Option 1', value: '1' },
    { label: 'Option 2', value: '2' },
    { label: 'Option 3', value: '3' },
    { label: 'Option 4', value: '4' },
    { label: 'Option 5', value: '5' },
    { label: 'Option 6', value: '6' },
    { label: 'Option 7', value: '7' },
    { label: 'Option 8', value: '8' },
  ]

  private onClickSubmit() {
    alert('SUBMIT')
  }
}
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  max-width: 600px;
  padding: 10vh 0;
}
</style>

ValidationProvider

components/InputForm.vue

<template>
  <validation-provider
    v-slot="{ valid, errors }"
    :rules="rules"
    :name="fieldName"
    :custom-messages="errorMessages"
    :vid="vid"
    :mode="mode"
  >
    <label
      class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
      for="grid-password"
    >
      {{ fieldName }}
    </label>
    <input
      v-model="innerValue"
      :name="fieldName"
      :placeholder="placeholder"
      :class="[
        'form-input mt-1 block w-full',
        { 'border-red-600': errors.length },
        { 'border-teal-600': valid },
      ]"
      :type="type"
      :pattern="pattern"
    />
    <p v-show="errors.length" class="absolute text-red-600 text-xs">
      {{ errors[0] }}
    </p>
  </validation-provider>
</template>

<script lang="ts">
import { Component, Vue, Prop } from 'nuxt-property-decorator'

@Component({
  name: 'InputForm',
})
export default class InputForm extends Vue {
  @Prop({ type: String, required: true })
  private rules!: string

  @Prop({ type: Object, required: true })
  private errorMessages!: object

  @Prop({ type: String, required: true })
  private value!: string

  @Prop({ type: String, required: true })
  private fieldName!: string

  @Prop({ type: String, default: '' })
  private placeholder!: string

  @Prop({ type: String, default: 'text' })
  private type!: string

  @Prop({ type: String, default: '' })
  private vid!: string

  @Prop({ type: String, default: 'eager' })
  private mode!: string

  @Prop({ type: String, default: '' })
  private pattern!: string

  get innerValue(): string {
    return this.$props.value
  }

  set innerValue(val) {
    this.$emit('input', val)
  }
}
</script>

components/SelectForm.vue

<template>
  <validation-provider
    v-slot="{ valid, errors }"
    :rules="rules"
    :name="fieldName"
    :custom-messages="errorMessages"
    :vid="vid"
    :mode="mode"
  >
    <label
      class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
      for="grid-password"
    >
      {{ fieldName }}
    </label>

    <select
      v-model="innerValue"
      :name="fieldName"
      :class="[
        'form-select mt-1 block w-full',
        { 'border-red-600': errors.length },
        { 'border-teal-600': valid },
        { 'text-gray-700': selected },
        { 'text-gray-500': !selected },
      ]"
      @change="selected = true"
    >
      <option value selected disabled hidden>
        {{ placeholder }}
      </option>
      <option v-for="(item, i) in options" :key="i" :value="item.value">
        {{ item.label }}
      </option>
    </select>

    <p v-show="errors.length" class="absolute text-red-600 text-xs">
      {{ errors[0] }}
    </p>
  </validation-provider>
</template>

<script lang="ts">
import { Component, Vue, Prop } from 'nuxt-property-decorator'

type option = {
  label: string
  value: string
}

@Component({
  name: 'SelectForm',
})
export default class SelectForm extends Vue {
  private selected: boolean = false

  @Prop({ type: String, required: true })
  private rules!: string

  @Prop({ type: Object, required: true })
  private errorMessages!: object

  @Prop({ type: String, required: true })
  private value!: string

  @Prop({ type: String, required: true })
  private fieldName!: string

  @Prop({ type: Array, required: true })
  private options!: option[]

  @Prop({ type: String, default: '' })
  private placeholder!: string

  @Prop({ type: String, default: '' })
  private vid!: string

  @Prop({ type: String, default: 'aggressive' })
  private mode!: string

  @Prop({ type: String, default: '' })
  private pattern!: string

  get innerValue(): string {
    return this.$props.value
  }

  set innerValue(val) {
    this.$emit('input', val)
  }
}
</script>

components/CheckForm.vue

<template>
  <validation-provider
    v-slot="{ valid, errors }"
    :rules="rules"
    :name="fieldName"
    :custom-messages="errorMessages"
    :vid="vid"
    :mode="mode"
  >
    <label
      class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
      for="grid-password"
    >
      {{ fieldName }}
    </label>

    <div class="flex flex-wrap">
      <div v-for="(item, i) in options" :key="i" class="py-1 px-2">
        <label>
          <input
            v-model="innerValue"
            :value="item.value"
            :class="[
              'form-checkbox h-6 w-6 text-teal-600',
              { 'border-red-600': errors.length },
            ]"
            type="checkbox"
          />
          <span class="ml-2">{{ item.label }}</span>
        </label>
      </div>
    </div>

    <p v-show="errors.length" class="absolute text-red-600 text-xs">
      {{ errors[0] }}
    </p>
  </validation-provider>
</template>

<script lang="ts">
import { Component, Vue, Prop } from 'nuxt-property-decorator'

type option = {
  label: string
  value: string
}

@Component({
  name: 'RadioForm',
})
export default class RadioForm extends Vue {
  @Prop({ type: String, required: true })
  private rules!: string

  @Prop({ type: Object, required: true })
  private errorMessages!: object

  @Prop({ type: Array, required: true })
  private value!: string[]

  @Prop({ type: String, required: true })
  private fieldName!: string

  @Prop({ type: Array, required: true })
  private options!: option[]

  @Prop({ type: String, default: '' })
  private placeholder!: string

  @Prop({ type: String, default: '' })
  private vid!: string

  @Prop({ type: String, default: 'aggressive' })
  private mode!: string

  @Prop({ type: String, default: '' })
  private pattern!: string

  get innerValue(): string {
    return this.$props.value
  }

  set innerValue(val) {
    this.$emit('input', val)
  }
}
</script>

components/RadioForm.vue

<template>
  <validation-provider
    v-slot="{ valid, errors }"
    :rules="rules"
    :name="fieldName"
    :custom-messages="errorMessages"
    :vid="vid"
    :mode="mode"
  >
    <label
      class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
      for="grid-password"
    >
      {{ fieldName }}
    </label>

    <div class="flex flex-wrap">
      <div v-for="(item, i) in options" :key="i" class="py-1 px-2">
        <label>
          <input
            :id="`option-${item.value}`"
            v-model="innerValue"
            :name="fieldName"
            :value="item.value"
            type="radio"
            :class="[
              'form-radio h-6 w-6 text-teal-600',
              { 'border-red-600': errors.length },
            ]"
          />
          <span class="ml-2">{{ item.label }}</span>
        </label>
      </div>
    </div>

    <p v-show="errors.length" class="absolute text-red-600 text-xs">
      {{ errors[0] }}
    </p>
  </validation-provider>
</template>

<script lang="ts">
import { Component, Vue, Prop } from 'nuxt-property-decorator'

type option = {
  label: string
  value: string
}

@Component({
  name: 'RadioForm',
})
export default class RadioForm extends Vue {
  @Prop({ type: String, required: true })
  private rules!: string

  @Prop({ type: Object, required: true })
  private errorMessages!: object

  @Prop({ type: String, required: true })
  private value!: string

  @Prop({ type: String, required: true })
  private fieldName!: string

  @Prop({ type: Array, required: true })
  private options!: option[]

  @Prop({ type: String, default: '' })
  private placeholder!: string

  @Prop({ type: String, default: '' })
  private vid!: string

  @Prop({ type: String, default: 'aggressive' })
  private mode!: string

  @Prop({ type: String, default: '' })
  private pattern!: string

  get innerValue(): string {
    return this.$props.value
  }

  set innerValue(val) {
    this.$emit('input', val)
  }
}
</script>

以上