【Vue】VeeValidateでフォームバリデーションを実装する

JavaScript

こんにちは、かつコーチです。

前回は、VueUseを使ってブラウザAPIまわりの処理を効率化する方法を解説しました。

今回は、Vueアプリで避けて通れない「フォームバリデーション」を、専用ライブラリのVeeValidateで実装する方法を扱います。

データ連携編の最後のテーマです。

なぜバリデーションを自前で書くと辛くなるのか

自作バリデーションが複雑化しやすい理由

会員登録フォームを例に考えてみましょう。

「必須入力」「メール形式」「パスワードの一致確認」といった項目が増えるほど、refif文だけで管理するコードは複雑になっていきます。

  • 入力中に検証するのか、送信時だけ検証するのか
  • エラーメッセージをどこに、どのタイミングで表示するか
  • 項目同士が関係するバリデーション(パスワード確認欄など)をどう表現するか

これらを自前で管理すると、コンポーネントが「表示のコード」と「バリデーションのロジック」でどんどん肥大化してしまいます。

VeeValidateが解決してくれること

VeeValidateは、Vue向けに作られたフォームバリデーション専用ライブラリです。

入力値の検証ルール、エラーメッセージの管理、送信タイミングの制御といった、フォーム特有の面倒な処理をまとめて引き受けてくれます。

バリデーションルール自体は、後述するZodYupといったスキーマライブラリと組み合わせて定義するのが定番です。

VeeValidateの導入

インストール

npm install vee-validate zod @vee-validate/zod

ここでは、TypeScriptとの相性が良いZodをスキーマ定義に使います。

基本のフォームを作る

シンプルなお問い合わせフォームを例に見てみましょう。

<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { z } from 'zod'

const schema = toTypedSchema(
  z.object({
    name: z.string().min(1, '名前を入力してください'),
    email: z.string().min(1, 'メールアドレスを入力してください').email('メールアドレスの形式が正しくありません'),
  })
)

const { handleSubmit, defineField, errors } = useForm({
  validationSchema: schema,
})

const [name, nameAttrs] = defineField('name')
const [email, emailAttrs] = defineField('email')

const onSubmit = handleSubmit((values) => {
  console.log('送信された値:', values)
})
</script>

<template>
  <form @submit="onSubmit">
    <div>
      <label for="name">名前</label>
      <input id="name" v-model="name" v-bind="nameAttrs" />
      <p v-if="errors.name" class="error">{{ errors.name }}</p>
    </div>
    <div>
      <label for="email">メールアドレス</label>
      <input id="email" v-model="email" v-bind="emailAttrs" />
      <p v-if="errors.email" class="error">{{ errors.email }}</p>
    </div>
    <button type="submit">送信</button>
  </form>
</template>

z.object()でルールを定義し、toTypedSchemaでVeeValidate向けの型付きスキーマに変換します。

defineFieldで各項目をv-modelに紐付けると、errors.nameのように項目ごとのエラーメッセージが自動的に管理されます。

よくあるつまずきポイント:送信タイミングとエラー表示

私が実際に困った「入力中にいきなりエラーが出る」問題

VeeValidateを初めて導入したとき、初期設定のまま使うと「入力欄をクリックしただけ(フォーカスして離れただけ)」でもエラーメッセージが表示されてしまい、ユーザーからは「何も入力していないのに怒られる」と感じられる見た目になってしまったことがあります。

原因は、validateOnBlur(フォーカスが外れたタイミングで検証する設定)がデフォルトで有効になっていたことでした。

初回表示や未入力の状態で赤いエラーが並んでいると、フォームを開いた瞬間から圧迫感を与えてしまいます。

❌ Before:初期表示から全項目のバリデーションが走ってしまう

<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { z } from 'zod'

const schema = toTypedSchema(
  z.object({
    name: z.string().min(1, '名前を入力してください'),
  })
)

// 設定を何も変えずに使うと、意図せず早いタイミングでエラーが出てしまう
const { handleSubmit, defineField, errors } = useForm({
  validationSchema: schema,
})

const [name, nameAttrs] = defineField('name')
</script>

✅ After:入力が一度も行われるまではエラーを表示しないよう制御する

<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { z } from 'zod'

const schema = toTypedSchema(
  z.object({
    name: z.string().min(1, '名前を入力してください'),
  })
)

const { handleSubmit, defineField, errors } = useForm({
  validationSchema: schema,
})

// validateOnModelUpdateをfalseにし、入力途中では検証しないようにする
const [name, nameAttrs] = defineField('name', {
  validateOnModelUpdate: false,
})

const onSubmit = handleSubmit((values) => {
  console.log('送信された値:', values)
})
</script>

<template>
  <form @submit="onSubmit">
    <input v-model="name" v-bind="nameAttrs" />
    <!-- 送信ボタンを押した後、または入力欄からフォーカスが外れた後に表示される -->
    <p v-if="errors.name" class="error">{{ errors.name }}</p>
  </form>
</template>

defineFieldの第2引数でバリデーションのタイミングを細かく制御できます。

「初回は静かに、送信時やフォーカスアウト時にしっかり検証する」というバランスが、ユーザー体験としては扱いやすいです。

応用:項目間の関係を検証する

パスワード確認欄のような相互チェック

パスワードと確認用パスワードが一致しているかのような、複数項目にまたがるチェックはZodのrefineで表現します。

<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { z } from 'zod'

const schema = toTypedSchema(
  z
    .object({
      password: z.string().min(8, 'パスワードは8文字以上で入力してください'),
      passwordConfirm: z.string(),
    })
    .refine((data) => data.password === data.passwordConfirm, {
      message: 'パスワードが一致しません',
      path: ['passwordConfirm'], // エラーをどの項目に紐付けるか指定する
    })
)

const { handleSubmit, defineField, errors } = useForm({
  validationSchema: schema,
})

const [password, passwordAttrs] = defineField('password')
const [passwordConfirm, passwordConfirmAttrs] = defineField('passwordConfirm')

const onSubmit = handleSubmit((values) => {
  console.log('登録処理を実行:', values)
})
</script>

<template>
  <form @submit="onSubmit">
    <input v-model="password" v-bind="passwordAttrs" type="password" />
    <p v-if="errors.password" class="error">{{ errors.password }}</p>

    <input v-model="passwordConfirm" v-bind="passwordConfirmAttrs" type="password" />
    <p v-if="errors.passwordConfirm" class="error">{{ errors.passwordConfirm }}</p>
  </form>
</template>

pathオプションで、エラーメッセージをどちらの入力欄に表示するかを指定できるのがポイントです。

こうした項目間の相互チェックも、スキーマの中に集約できるのがVeeValidate+Zodの強みです。

次回は認証・セキュリティ編に入り、こうしたフォームで入力された情報をどう安全に扱うかを見ていきます。

まとめ

この記事のポイント

  • フォームバリデーションを自前のrefif文だけで管理すると、コンポーネントが肥大化しやすい
  • VeeValidateはZod・Yupなどのスキーマと組み合わせて、ルール定義とエラー管理を一元化できる
  • validateOnModelUpdateなどの設定で、エラー表示のタイミングをユーザー体験に合わせて調整できる
  • 初期設定のままだと、入力直後からエラーが表示されて圧迫感を与えることがある
  • パスワード確認欄のような項目間チェックは、Zodのrefinepathオプションで表現する

次に読むべき記事

次回からは認証・セキュリティ編に入り、「Vueアプリに認証機能を実装するパターン」を解説します。

→ 次の記事:Vueアプリに認証機能を実装するパターン

タイトルとURLをコピーしました