Vue3.0で使えるようになるComposition APIでのフォームバリデーションを実装します。一般的なユーザーの登録フォームをもとに見ていくので、割と網羅的な内容になっていると思います。
実装完了したコードはこちら。
必要なライブラリの導入
今回使うのはこれらです。
- @vue/cli@4.4.1
- vue2.6.11
- composition-api
- vue-composable
利用する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です。

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で利用するためのコンポーネント群です。日時を扱うものや、文字列のフォーマット、ページネーション機能等も備えています。
非常に汎用性の高いものになっていますので、バリデーション以外の機能にも興味がある方はぜひご覧ください。
フォームの作成
バリデーションを試すために、まずは一般的なフォームを作成します。
項目は一般的なユーザーの登録フォームを想定しています。
- 氏名
- 年齢
- 生年月日
- プロフィール
- 電話番号
- 住所(省力化のため都道府県選択のみ)
以下のコードを /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>
画面はこのような表示になります。不格好ですが、十分でしょう。

各入力欄の基本形はこちらです。ラベルとinputのセットですね。これを項目の分だけ並べた形です。
<div class="mt">
<label for="name-input">氏名</label>
<input
type="text"
v-model="state.name"
id="name-input"
/>
</div>
では、バリデーションの実装をしていきましょう。
バリデーションの実装
公式ドキュメントはこちらです。
基本形の実装
vue-composableのuseValidationを利用し、バリデーションの定義をしていきます。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です。バリデータは、$valueとcontextを受け取る関数を指定します。先ほどの例では、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>

数値チェックの実装
先ほどは氏名の必須チェックを実装しましたが、次は年齢の数値チェックをします。
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-modelもform.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を利用したバリデーション実装でした。