Vue3.0で使えるようになるComposition APIでのフォームバリデーションを実装します。一般的なユーザーの登録フォームをもとに見ていくので、割と網羅的な内容になっていると思います。

実装完了したコードはこちら

必要なライブラリの導入

今回使うのはこれらです。

利用するVueは2.6.11です。Vue3.0でなくても、@composition-apiをインストールすればComposition APIが利用できます。 バリデーションに利用するライブラリは、vue-composableです。VuelidateやVeeValidateはVue2.0系向けで、まだ3には対応していないようでした。

CLIインストール

まずは最新のCLIを入れます。古いものが入っている場合、削除してしまいましょう。

$ npm install @vue/cli@4.4.1 -g

プロジェクト作成・ライブラリ追加

Vueプロジェクトを作成し、Composition APIとvue-composableを導入します。プロジェクト作成時のオプションはお好みで問題ないです。なお、今回はTypeScriptを利用しています。

$ vue create validate-sample
$ cd validate-sample
$ yarn add @vue/composition-api vue-composable

ここまでできたら一旦起動してみましょう。

$ yarn run serve

http://localhost:8080 で立ち上がります。このような画面が表示されればOKです。

vue-startup

CompositionAPIの設定

プラグインとしてComposition APIを利用することを設定します。src/main.tsでimportとVue.useをしましょう。

import Vue from 'vue'
import App from './App.vue'
import VueCompositionApi from "@vue/composition-api"; // 追記

Vue.config.productionTip = false
Vue.use(VueCompositionApi); // 追記

new Vue({
  render: h => h(App),
}).$mount('#app')

補足: vue-composableについて

vue-composableは、Composition APIで利用するためのコンポーネント群です。日時を扱うものや、文字列のフォーマット、ページネーション機能等も備えています。 非常に汎用性の高いものになっていますので、バリデーション以外の機能にも興味がある方はぜひご覧ください。

vue-composable

フォームの作成

バリデーションを試すために、まずは一般的なフォームを作成します。

項目は一般的なユーザーの登録フォームを想定しています。

  • 氏名
  • 年齢
  • 生年月日
  • プロフィール
  • 電話番号
  • 住所(省力化のため都道府県選択のみ)

以下のコードを /src/App.vueにコピペすればOKです。

<template>
  <div id="app">
    <form @submit.prevent="submit">
      <div class="mt">
        <label for="name-input">氏名</label>
        <input
          type="text"
          v-model="state.name"
          id="name-input"
        />
      </div>

      <div class="mt">
        <label for="age-input">年齢</label>
        <input
          type="number"
          v-model="state.age"
          id="age-input"
        />
      </div>

      <div class="mt">
        <label for="birth-input">生年月日</label>
        <input
          type="date"
          v-model="state.birthDay"
          id="birth-input"
        />
      </div>

      <div class="mt">
        <label for="profile-input">プロフィール</label>
        <textarea
          v-model="state.profile"
          rows="4"
          cols="40"
          id="profile-input"
        ></textarea>
      </div>

      <div class="mt">
        <label for="tel-input">電話番号</label>
        <input
          type="text"
          v-model="state.telNumber"
          id="tel-input"
        />
      </div>

      <div class="mt">
        <label for="address-input">都道府県</label>
        <select
          v-model="state.address"
          id="address-input"
        >
          <option>都</option>
          <option>道</option>
          <option>府</option>
          <option>県</option>
        </select>
      </div>

      <div class="mt">
        <input type="submit"/>
      </div>
    </form>
  </div>
</template>

<script lang="ts">
import {defineComponent, reactive} from '@vue/composition-api'

type State = {
  name: string;
  age: number;
  birthDay: Date;
  profile: string;
  telNumber: string;
  address: string;
}

export default defineComponent({
  setup() {
    const state = reactive<State>({
      name: "",
      age: 20,
      birthDay: new Date(),
      profile: "",
      telNumber: "",
      address: "都"
    });

    function submit() {
      alert('called submit')
    }

    return {
      state,
      submit
    }
  }
})
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}

.mt {
  margin-top: 20px;
}
</style>

画面はこのような表示になります。不格好ですが、十分でしょう。

form-completed

各入力欄の基本形はこちらです。ラベルとinputのセットですね。これを項目の分だけ並べた形です。

<div class="mt">
  <label for="name-input">氏名</label>
  <input
    type="text"
    v-model="state.name"
    id="name-input"
  />
</div>

では、バリデーションの実装をしていきましょう。

バリデーションの実装

公式ドキュメントはこちらです。

基本形の実装

vue-composableuseValidationを利用し、バリデーションの定義をしていきます。nameの必須バリデーションを設定したscriptがこちら。

<script lang="ts">
import {defineComponent, reactive} from '@vue/composition-api'
import {useValidation} from 'vue-composable' // 追加

type State = {
  name: string;
  age: number;
  birthDay: Date;
  profile: string;
  telNumber: string;
  address: string;
}

const required = x => !!x // 追加

export default defineComponent({
  setup() {
    const state = reactive<State>({
      name: "",
      age: 20,
      birthDay: new Date(),
      profile: "",
      telNumber: "",
      address: "都"
    });

    // 追加
    const form = useValidation({
      name: {
        $value: state.name,
        required
      }
    })

    function submit() {
      alert('called submit')
    }

    return {
      state,
      form, // 追加
      submit
    }
  }
})
</script>

useValidationで、バリデーションの設定を生成しています。構文は下記の通りです。

useValidation({
  <プロパティ名>: {
    $value: <監視する対象の値>,
    <バリデータ>
  }
})

監視する対象にはリアクティブな値を入れましょう。reactiveで生成しても、refで生成してもOKです。バリデータは、$valuecontextを受け取る関数を指定します。先ほどの例では、x => !!x を指定しました。受け取った値を真偽値に変換しています。

一点補足で、現時点では公式トップにCurrently there's no exported validators.との文言が表示されています。将来的に標準のバリデータが提供されるということでしょうか。現時点ではライブラリからバリデータが提供されていないので、今回はrequiredバリデータを自前実装しています。

さて、これが正常に動作しているか確認してみましょう。先程のフォームの氏名の部分をこのコードに置き換えます。

<div class="mt">
  <label for="name-input">氏名</label>
  <input
    type="text"
    v-model="form.name.$value"
    id="name-input"
  />

  <p>any invalid?: {{form.name.$anyInvalid}}</p>
</div>

form.name.$valueでリアクティブな値にアクセスできるようになっています。これをv-modelにセットしました。pタグの中に書いてあるform.name.$anyInvalidは、そのプロパティが有効か無効かを返します。氏名が空の場合はrequiredがfalseを返すため、$anyInvalidはtrueになります。実際にフォームを触って確かめてみてください。

エラー時メッセージの設定

不正な値の場合、その旨をユーザーに伝える必要があります。先程のバリデーションに、エラーメッセージを追加しましょう。

export default defineComponent({
  setup() {
    const state = reactive<State>({
      name: "",
      age: 20,
      birthDay: new Date(),
      profile: "",
      telNumber: "",
      address: "都"
    });

    const form = useValidation({
      name: {
        $value: state.name,
        required,
        $message: "必須項目です"
      },

$messageを追加します。この値はform.name.$messageでアクセスすることが可能です。

下記のように氏名の部分を書き換えると、不正な値があった場合メッセージが表示されるようになります。一番下にpタグを追加した形ですね。

<div class="mt">
  <label for="name-input">氏名</label>
  <input
    type="text"
    v-model="form.name.$value"
    id="name-input"
  />
  <p v-if="form.name.$anyInvalid">{{form.name.$message}}</p>
</div>

validated

数値チェックの実装

先ほどは氏名の必須チェックを実装しましたが、次は年齢の数値チェックをします。

const required = x => !!x
// 追加
const numerically = v => {
  return v.match(/\d+/)
};

export default defineComponent({
  setup() {
    const state = reactive<State>({
      name: "",
      age: 20,
      birthDay: new Date(),
      profile: "",
      telNumber: "",
      address: "都"
    });

    const form = useValidation({
      name: {
        $value: state.name,
        required,
        $message: "必須項目です"
      },
      // 追加
      age: {
        $value: state.age,
        numerically,
        $message: "数値を入力してください"
      },

validatorに、新たに定義したnumericallyを設定します。templateのv-modelform.age.$valueを利用するよう更新しておきましょう。

これでname同様バリデーションが効くようになっているはずです。最後に電話番号のバリデーションを実装しましょう。

フォーマットチェックの実装

useValidationに、telNumberについてのバリデーションを追加します。

const required = x => !!x
const numerically = v => {
  return v.match(/\d+/)
};
// 追加
const telNumberFormat = v => {
  return v.match(/\d{2,3}-\d{1,4}-\d{4}$/)
}

export default defineComponent({
  setup() {
    const state = reactive<State>({
      name: "",
      age: 20,
      birthDay: new Date(),
      profile: "",
      telNumber: "",
      address: "都"
    });

    const form = useValidation({
      name: {
        $value: state.name,
        required,
        $message: "必須項目です"
      },
      age: {
        $value: state.age,
        numerically,
        $message: "数値を入力してください"
      },
      // 追加
      telNumber: {
        $value: state.telNumber,
        required: {
          $validator: required,
          $message: "必須項目です"
        },
        format: {
          $validator: telNumberFormat,
          $message: "電話番号の形式が不正です"
        }
      }

ネストさせ、複数のバリデーションを行うことができます。電話番号では必須チェックとフォーマットチェックを行うようにしました。それぞれ、form.telNumber.required.プロパティ名 / form.telNumber.format.プロパティ名でアクセスできます。

完成形

最後にsubmitボタンの活性制御を追加すれば完成です。

<template>
  <div id="app">
    <form @submit.prevent="submit">
      <div class="mt">
        <label for="name-input">氏名</label>
        <input
          type="text"
          v-model="form.name.$value"
          id="name-input"
        />
      </div>

      <div class="mt">
        <label for="age-input">年齢</label>
        <input
          type="number"
          v-model="form.age.$value"
          id="age-input"
        />
      </div>

      <div class="mt">
        <label for="birth-input">生年月日</label>
        <input
          type="date"
          v-model="state.birthDay"
          id="birth-input"
        />
      </div>

      <div class="mt">
        <label for="profile-input">プロフィール</label>
        <textarea
          v-model="form.profile.$value"
          rows="4"
          cols="40"
          id="profile-input"
        ></textarea>
      </div>

      <div class="mt">
        <label for="tel-input">電話番号(ハイフン含)</label>
        <input
          type="text"
          v-model="form.telNumber.$value"
          id="tel-input"
        />
      </div>

      <div class="mt">
        <label for="address-input">都道府県</label>
        <select
          v-model="state.address"
          id="address-input"
        >
          <option>都</option>
          <option>道</option>
          <option>府</option>
          <option>県</option>
        </select>
      </div>

      <div class="mt">
        <input
          :disabled="form.$anyInvalid"
          type="submit"
        />
      </div>

      <div>
        <p v-if="form.name.$anyInvalid">氏名: {{form.name.$message}}</p>
        <p v-if="form.age.$anyInvalid">年齢: {{form.age.$message}}</p>
        <p v-if="form.birthday.$anyInvalid">生年月日: {{form.birthday.$message}}</p>
        <p v-if="form.profile.$anyInvalid">プロフィール: {{form.profile.$message}}</p>
        <p v-if="form.telNumber.$anyInvalid">電話番号: {{form.telNumber.required.$message}} {{form.telNumber.format.$message}}</p>
      </div>
    </form>
  </div>
</template>

<script lang="ts">
import {defineComponent, reactive} from '@vue/composition-api'
import {useValidation} from 'vue-composable'

type State = {
  name: string;
  age: number;
  birthDay: Date;
  profile: string;
  telNumber: string;
  address: string;
}

const required = x => !!x
const numerically = v => {
  return v.match(/\d+/)
};
const telNumberFormat = v => {
  return v.match(/\d{2,3}-\d{1,4}-\d{4}$/)
}

export default defineComponent({
  setup() {
    const state = reactive<State>({
      name: "",
      age: 20,
      birthDay: new Date(),
      profile: "",
      telNumber: "",
      address: "都"
    });

    const form = useValidation({
      name: {
        $value: state.name,
        required,
        $message: "必須項目です"
      },
      age: {
        $value: state.age,
        numerically,
        $message: "数値を入力してください"
      },
      birthday: {
        $value: state.birthDay,
        required,
        $message: "必須項目です"
      },
      profile: {
        $value: state.profile,
        required,
        $message: "必須項目です"
      },
      telNumber: {
        $value: state.telNumber,
        required: {
          $validator: required,
          $message: "必須項目です"
        },
        format: {
          $validator: telNumberFormat,
          $message: "電話番号の形式が不正です"
        }
      }
    })

    function submit() {
      alert('called submit')
    }

    return {
      state,
      form,
      submit
    }
  }
})
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}

.mt {
  margin-top: 20px;
}
</style>

このコードは多少雑な作りになっていますが、うまくアレンジして利用してみてください。

まとめ

基本形からちょっとした応用まで紹介したので、これをベースに拡張していけば大抵の要件には対応できるかと思います。非常に柔軟性が高く使い勝手の良い機能ですし、そこまで癖のない馴染みある構文ですね。一部触れなかった構文もありますので、興味のある方はぜひ公式ドキュメントも読んでみてください。 https://pikax.me/vue-composable/composable/validation/validation.html

以上、vue-composableを利用したバリデーション実装でした。