【Nuxt3】yupとvee-validateを使ったバリデーション
バリデーションの実装って地味に面倒なんですよね。
yupとvee-validateを使うと、すごくシンプルかつわかりやすく書くことができます。
色々な書き方があるがゆえに最初は少しとっつきにくいですが、順を追って学習するとわかりやすいと思います。
なお、今回のコードはこちらに公開しています。
ライブラリなしの場合
まずはライブラリなしではどんな感じの実装になるか見てみましょう。
今回は「名前」と「メールアドレス」をフォームに書いて送信するようなアプリケーションを実装します。
名前は何か文字が入力されていること、メールアドレスは何か文字が入っていることに加えてメールアドレス形式になっている必要があるとしましょう。
不備がある場合はサーバーに送る前に修正を促す、いわゆるバリデーションを実装します。
もしバリデーションが通らない時は、名前欄とメールアドレス欄それぞれにエラーメッセージを出したいとします。
スクリプト部分は以下のような書き方になると思います。
const formData = reactive({
名前:'',
メールアドレス:''
})
const nameErrorMessage = computed(()=>{
if(!formData.名前){
return '名前を入力してください'
}
})
const emailErrorMessage = computed(()=>{
if(!formData.メールアドレス){
return 'メールアドレスを入力してください'
}
if(!formData.メールアドレス.match(/^[a-zA-Z0-9_.+-]+@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.)+[a-zA-Z]{2,}$/)){
return '正しいメールアドレスを入力してください'
}
})
function onSubmit(){
// ここでもバリデーションが必要
alert(JSON.stringify(formData))
}
うーん、nameErrorMessageやemailErrorMessageがかなり長ったらしいです。
今回は省略していますが、入力した内容をPOSTするとき(onSubmitの部分)はさらにバリデーションかける必要があります。
実際にアプリケーション作る場合、名前は4文字以上などのようにもっと正確にバリデーションかける必要がありますし、パスワードなど項目が増えたら、さらに長くてわかりにくいコードになってしまいそうです。
yupを導入する
次はバリデーション用の有名なライブラリであるyupを導入してみましょう。
const formData = reactive({
名前:'',
メールアドレス:''
})
+const userSchema = yup.object().shape({
+ 名前: yup.string().required('名前を入力してください'),
+ メールアドレス:yup.string().email('正しいメールアドレスを入力してください').required('メールアドレスを入力してください')
+});
function onSubmit(){
userSchema.validate(formData)
.then(res=>{
alert('OK')
})
.catch(err=>{
alert(err)
})
}
yupの使い方としては、schemaというバリデーションをかけるためのオブジェクトを用意します。
そして、schema.validate()にバリデーションをかけたい項目(今回は名前とメールアドレス)をぶち込むことで、バリデーションが通るか、通らない場合は例外を発生させることができます。
見ての通り、yupを使うと入力必須項目はrequired()
と一言書くだけで済みますし、メールアドレスの形式についてもemail()
の一言だけで済みますので、先ほどのライブラリ使わない方法と比べるとずいぶんと楽かつ読みやすく実装できたのではないでしょうか。
一方で欠点としては、submitするタイミングでしかバリデーションをかけられないので、フォームの各項目を入力したタイミングでリアルタイムにエラーを表示させてほしい時は困ります。
(そういうサイト多いですよね。私だとすごくイラッとするんですが、そういうサイトの制作はクソザコエンジニアにしか発注できなかったんでしょう)
まあwatchを使うとリアルタイムにエラーメッセージを表示させれなくはないのですが、結構大変なので素直に次に紹介するvee-validateを使いましょう。
vee-validate useFieldを使う
今回の記事の主題であるvee-validateです。
先ほどまでの書き方では、フォームの項目である「名前」と「メールアドレス」を普通の変数(formData)に格納していましたが、vee-validateは状態変数(use〇〇)に格納します。
ちょっと扱いがこれまでと異なるので最初は戸惑うかもしれませんが、慣れれば素晴らしい書き方だと思うので、ぜひマスターしてください。
vee-validateには色々な書き方があって正直ややこしいのですが、まずは一番簡単なuseFieldを使った書き方です。
const 名前field = useField('名前',yup.string().required('名前を入力してください'))
const メールアドレスfield = useField('メールアドレス',yup.string().email('正しいメールアドレスを入力してください').required('メールアドレスを入力してください'))
function onSubmit(){
// ここは実装しづらい
}
useFieldの第一引数はuseFieldを区別するための名称を指定します。(NuxtのuseState()につける名称と同じように考えれば良いです)
第二引数はバリデーションをかけるためのスキーマを入れます。
スキーマはベタ書きする方法もあるのですが、上記のようにyupを使うとシンプルかつ読みやすく書けます。
今回は「名前」と「メールアドレス」ですので、それぞれについてuseField()を使ってFieldContext(上記の例では名前fieldとメールアドレスfield)を作成しました。
さて、このFieldContextには何が入っているのでしょうか。
よく使うのはvalue、errorMessage、そしてsetValue()です。
const {value, errorMessage, setValue} = useField('someName',yup.string().required())
読んでの通り、valueは現在セットされている値、errorMessageはバリデーションかけた結果のエラーメッセージです。
これらはリアルタイムに更新されますので、入力欄の近くにerrorMessageを表示させといたら、ユーザーはすぐに不備に気づくでしょう。
(yup単独で使った時のようにonsubmitして初めて入力不備に気づかされてイラっとすることがなくなります。)
ひとつ気をつけなければならないのは、inputでv-modelを使ったバインディングができないことです。
fieldContextの値を更新するには、valueを直接更新するのではなく、setValue()で更新する必要があります。
<input class="input input-sm input-bordered" :value="名前field.value.value" @blur="名前field.setValue($event.target.value)">
<!--これはダメ-->
<input class="input input-sm input-bordered" v-model="名前field.value">
※この例では@blurを使っていますが、@changeや@inputも使えます。
useField()で非常に書きやすくなったのは良いのですが、onSubmitで送信する際に名前fieldとメールアドレスfieldを個別にバリデーションかけたりvalueを取り出すのは面倒です。
それにフォームとしてサーバーに送るデータである名前fieldとメールアドレスfieldが別個になっているのも、なんだか気持ち悪いです。
これらを解決するには、useFormを使います
vee-validate useFormを使う
先の説明では名前とメールアドレスに対して別々にuseField()を使いました。
useFormを使うと、ひとかたまりのオブジェクトとして扱うことができます。
以下のようにuseFormにvalidationSchemaとして名前、メールアドレスそれぞれのschemaを与えます。
const {handleSubmit,values, errors, setValues} = useForm({
validationSchema:{
名前:yup.string().required('名前を入力してください'),
メールアドレス:yup.string().email('正しいメールアドレスを入力してください').required('メールアドレスを入力してください')
}
})
const onSubmit = handleSubmit(values=>{
alert(JSON.stringify(values))
})
見ての通り、返り値にvalues、errors、setValuesがあります。
だいたい想像がつくと思いますが、これらは先ほどuseField()の返り値のvalue、error、setValueの複数版です。
「名前」のvalueを見たかったらvalues['名前']
とすれば良いですし、「名前」にsetValueしたかったら、setValues({名前:'something'})
とすれば良いです。
useFieldで別個で書いた時と比べて、ものすごくシンプルになりました。
そして、何よりもonSubmitするときの最終バリデーションがhandleSubmit
を使って書くことができます。
普段vee-validateを使うときは、このuseFormを使って実装すると良いでしょう。
inputを別コンポーネントにする
最後にinputを別コンポーネントにする時の書き方です。
先ほど解説した通り、vee-validateではフォームの項目(名前やメールアドレスなど)を状態変数に入れますので、普通の変数に入れる時とかなり違った書き方になります。
まず親側です。
<form @submit="onSubmit">
<FormInput name="名前" />
・・・
</form>
inputやselectなどフォームの項目は別コンポーネントに切り出している場合も含めて、必ず<form>
タグの中に入れます。
興味深いことに、子コンポーネントにはnameだけを渡しており、普段使うようなv-bindやv-onは使いません。
次は子側です。
<script setup lang="ts">
import { useField } from 'vee-validate';
const {name} = defineProps<{name:string}>();
const { handleChange, value, handleBlur, errorMessage, meta } = useField(() => name,undefined ); //第二引数にundefinedを渡さないと、無限再帰ループしてしまう
</script>
<template>
<label class="form-control w-full max-w-xs">
<div class="label">
<span class="label-text">{{ name }}</span>
<span v-if="meta.required" class="text-red-500">*</span>
<span class="label-text-alt text-red-500">{{ errorMessage }}</span>
</div>
<input type="text" :value="value" @change="handleChange" placeholder="Type here" class="input input-bordered w-full max-w-xs" />
</label>
</template>
ここにuseFieldが使われており、値などはuseFieldを通して渡されていることがわかります。
親側コンポーネントでuseFormを使って設定されたフォームの項目(「名前」や「メールアドレス」など)は、グローバルな状態変数に格納されているので、useFieldを使ってグローバル状態変数からデータを取り出すことになります。
(useStateに似た使い方ですね。もっともvee-validateはnuxtに限らずvueでも使えます)
親子コンポーネント間でデータを受け渡しを明示しなくても良いので、かなりシンプルに実装できるのではないでしょうか。
最後に
今回はごく基本的な書き方を通して、vee-validateの書き方を学びました。
ここで示した以外にも、yupには色々なスキーマを構築する機能があるので、ぜひ公式ドキュメントを読んでみてください。
Discussion