Vuetify — フォームとバリデーション
テクノロジ系 / L19 vuetify — Material Design コンポーネントライブラリ
<style scoped>
section.title h1 { white-space: normal; font-size: 1.7em; line-height: 1.3; }
</style>
Vuetify — フォームとバリデーション
(ch01-setup-theme を先に進めていることが前提)
v-text-field の基本
<script setup>
import { ref } from 'vue'
const name = ref('')
</script>
<template>
<v-text-field v-model="name" label="お名前" />
</template>v-modelで入力値を双方向バインドlabelprop でラベル表示(プレースホルダーではなくフローティングラベル)
rules 関数を書く
<script setup> const requiredRule = (v) => !!v || '必須項目です' const emailRule = (v) => /.+@.+\..+/.test(v) || 'メール形式で入力してください' </script>
- rules は「値を受け取り、OK なら
true、NG ならエラーメッセージ(文字列)を返す関数」
なぜ rules は「ただの関数」でよいのか
requiredRuleもemailRuleも、Vuetify 専用の書き方は一切使っていない。
「値を受け取り、true かエラー文字列を返す」ただの JavaScript 関数
- 専用のバリデーションライブラリ(スキーマ定義など)を使わない利点:
- 覚える API が増えない。普通の関数の書き方・テストの仕方がそのまま使える
- 1つの rule は他のフォームや単体テストにもそのまま使い回せる
- トレードオフ: ここで扱う rules は「1つの値を見て即座に判定する」ものだけ。
「パスワード確認欄が本体と一致するか」のような複数項目をまたぐ判定や、
「メール重複を API で確認する」ような非同期判定はこの仕組みだけでは
書きにくく、別途 watch や外部ライブラリが必要になる
rules を v-text-field に渡す
<template> <v-text-field label="お名前" :rules="[requiredRule]" /> <v-text-field label="メール" :rules="[requiredRule, emailRule]" /> </template>
- 配列に複数渡すと 上から順に 評価され、最初に失敗したメッセージが表示される
- なぜ「最初の1件だけ」表示なのか: 全ての失敗メッセージを同時に出すと画面が
窮屈になり、利用者はどれから直せばいいか迷う。「まず必須を満たす→次に
形式を確認する」の順で1つずつ渡す方が親切
送信時に validate() を呼ぶ
<script setup>
import { ref } from 'vue'
const formRef = ref(null)
async function submit() {
const result = await formRef.value.validate()
if (!result.valid) return
}
</script>await formRef.value.validate()でv-form配下の rules をまとめて判定するresult.validがfalseならreturnで打ち切り、trueなら送信へ進む- なぜ各
v-text-fieldを1つずつ手動でチェックしないのか: 項目数が増える
たびに「全部の項目が valid か」を自分で集計するコードが必要になる。
v-form に任せれば、項目がいくつに増えてもこの1行のままでよい
v-form と v-text-field を組む
<template>
<v-form ref="formRef" @submit.prevent="submit">
<v-text-field v-model="name" label="お名前" :rules="[requiredRule]" />
<v-btn @click="submit">送信</v-btn>
</v-form>
</template>v-formにref="formRef"を張るとformRef.value.validate()が呼べるようになる@submit.prevent="submit"で Enter 送信時もデフォルト動作を止めてsubmit()を呼ぶv-btnの@click="submit"も同じ関数。クリックでも Enter でも検証ロジックは共通
validate() の流れ
graph LR
A[送信] --> B[検証実行]
B --> C{全rules<br/>OK?}
C -->|Yes| D[OK]
C -->|No| E[NG]- B =
formRef.value.validate()を呼ぶ - D(OK)/E(NG) =
result.validがtrue/false。awaitを忘れるとresultは Promise のままで、result.validは常にundefinedになる - 個別の
v-text-fieldを1つずつチェックする必要はない。v-formがまとめて実行してくれる
まとめ
v-text-fieldに:rules="[...]"を渡すだけで入力チェックの仕組みが手に入る- rules は「値 → true or エラー文字列」を返す普通の JS 関数でよい
v-formにrefを張り、formRef.value.validate()で全項目を一括検証するvalidate()は非同期。awaitしてresult.validを読む
考えてみよう
既定では正解・不正解の判定はしません。自分のペースで「答えを見る」を。採点してほしい場合だけ「採点してほしい」を押してください。
Lv1: 基本 — 必須チェック
問題
v-text-field に label="お名前" を設定し、未入力なら true を返さずに
「必須項目です」というエラーメッセージ(文字列)を返すルール関数を自作して :rules に渡せ。
未入力のままフィールドからフォーカスを外すと、そのエラーメッセージが表示されることを確認せよ。
合格基準
- 未入力時に「必須項目です」が表示される
- 何か入力すればエラーが消える
Lv2: 応用 — 複数ルールと v-form.validate()
問題
「お名前」(必須)と「メール」(必須 かつ メール形式)の2つの v-text-field を持つ v-form を作れ。
v-form に ref を張り、送信ボタン押下時に formRef.value.validate() を呼び出して、
その結果の valid をリアクティブな変数に保存すること。
以下の3パターンで送信し、それぞれの結果を確認せよ。
- 両方未入力で送信
- お名前だけ入力して送信
- お名前・メール(不正な形式)を入力して送信
- お名前・メール(正しい形式)を入力して送信
合格基準
- パターン1〜3では
validがfalseになる - パターン4では
validがtrueになる
Lv3: 応用 — 複数フィールドの送信ガード
問題
「名前」(必須)・「メールアドレス」(必須 かつ メール形式)の2つの v-text-field を持つフォームを作り、
フォーム全体が valid になるまで送信ボタンを押せないようにせよ(disabled にする)。
- フォーム全体の valid 状態を、リアクティブな変数として追跡できるようにすること
- 送信ボタンの
disabled状態は、その valid 状態と連動させること
合格基準
- 名前・メールアドレスのどちらか一方でも未入力・不正な形式の間は、送信ボタンが押せない(disabled)
- 両方とも正しく入力されると、送信ボタンが押せるようになる
Lv4: 発展 — カスタムルールを自作する
問題
「8文字以上」を要求するパスワード用のルールを自作し、type="password" の v-text-field に追加せよ。
さらに、「必須」と「8文字以上」の両方を満たすルール配列にせよ。
TODO
<script setup> // TODO: 「必須」ルールと「8文字以上」ルールをそれぞれ定義する </script> <template> <!-- TODO: :rules に上記2つのルールを配列で渡す --> <v-text-field label="パスワード" type="password" /> </template>
合格基準
- 7文字以下だとエラーになる
- 8文字以上だとエラーが消える
- rules は「値 → true or エラー文字列」という同じ形の関数を並べるだけで良いと説明できる
提出
submission.md に4問の回答を。
ヒント
まず第1段階だけ見て、自分で考えてみよう。行き詰まったら第2段階、それでも分からなければ第3段階を見る。
第1段階: 考え方
v-formの中身をまとめて検証するには、テンプレート側のそのv-form要素に JS からアクセスできる仕組みが要る。- バリデーションの実行は一瞬で終わるとは限らない処理として扱われている。実行が完了してから結果を使う必要がある。
- 検証結果は「フォーム全体として valid かどうか」を含むオブジェクトとして返ってくる。
第2段階: 使う API 名
v-formにref="formRef"を張っておくと、formRef.valueから JS 側でフォームを操作できる。- フォーム全体の検証は
formRef.value.validate()を呼ぶ。これはPromiseを返す非同期メソッドなのでawaitする。 - 戻り値は
{ valid: boolean, errors: [...] }という形のオブジェクト。validプロパティを取り出して使う。
第3段階: コード断片
async function submit() {
const result = await formRef.value.validate()
valid.value = result.valid
}解説
v-form + rules で「入力チェック」を宣言的に書く
Vuetify のフォーム部品(v-text-field など)は rules という prop に「値を受け取って
true(OK)か文字列(エラーメッセージ)を返す関数の配列」を渡すだけで、入力チェックの仕組みが手に入る。
<script setup>
import { ref } from 'vue'
const formRef = ref(null)
const name = ref('')
const valid = ref(null)
const requiredRule = (v) => !!v || '必須項目です'
async function submit() {
const result = await formRef.value.validate()
valid.value = result.valid
}
</script>
<template>
<v-form ref="formRef" @submit.prevent="submit">
<v-text-field v-model="name" label="お名前" :rules="[requiredRule]" />
<v-btn @click="submit">送信</v-btn>
</v-form>
</template>requiredRuleは「値があればtrue、なければエラーメッセージの文字列」を返すだけの、ただの関数。
Vuetify 固有の書き方を覚える必要はなく、素の JS 関数を書けばよい。
v-formにref="formRef"を張ることで、formRef.value.validate()からフォーム内の
すべての rules を一括実行できる。個々の v-text-field を 1 つずつチェックして回る必要はない。
validate()は非同期(Promise)。バリデーション結果が揃うまで待ってから
result.valid を読む必要があるため await を忘れると常に undefined になってしまう。
なぜ「1 個ずつ if 文でチェック」しないのか
素の JS でフォームバリデーションを書くと、入力欄が増えるたびに
if (!name.value) { ... } if (!email.value) { ... } のような分岐が積み上がり、
「どの欄が」「なぜ」エラーなのかの表示処理まで自分で書く必要が出てくる。
rules に判定関数を渡すだけで、エラー表示・色・アイコンまで Vuetify 側が面倒を見てくれるため、
入力欄が増えても書く量が線形にしか増えない。フォームの規模が大きくなるほど効果が大きい設計。
ヒント
まず第1段階だけ見て、自分で考えてみよう。行き詰まったら第2段階、それでも分からなければ第3段階を見る。
第1段階: 考え方
- メールアドレス欄のルールは、他のバリデーションルール(
requiredRuleなど)と同じく
「値を受け取って true かエラーメッセージの文字列を返す関数」でしかない。
「〇〇@〇〇.〇〇」のような形をしているかどうかを、自分で判定する処理を書く必要がある。
- 各入力欄に rules を設定しても、それは「その欄が単独で OK かどうか」でしかない。
「フォーム全体として送信して良いか」は別に追跡する必要がある。
- ボタン側は、送信されて初めて検証結果を知るのではなく、入力の変化を追いかけて
リアルタイムに「押せる/押せない」を切り替えたい。
- つまり、フォーム全体の valid 状態を
v-form側とリアルタイムに同期させておき、
それをボタンの disabled 条件にそのまま流し込めばよい。
第2段階: 使う API 名
v-formはrefを張ってvalidate()を呼ぶ以外に、v-modelで全体の valid 状態を
リアクティブに受け取ることもできる。<v-form v-model="valid"> のように書く。
- この
validは、まだ何も入力・検証されていない段階ではnull、検証済みなら
true / false になる。null も「valid ではない」として扱ってよい。
v-btnの:disabledに!validを渡せば、validがtrueになるまでボタンは押せない。- 各
v-text-fieldにはこれまでと同じく:rules="[...]"でルール関数の配列を渡す。
第3段階: コード断片
<script setup>
import { ref } from 'vue'
const valid = ref(null)
const requiredRule = (v) => !!v || '必須項目です'
const emailRule = (v) => !v || /.+@.+\..+/.test(v) || 'メール形式で入力してください'
</script>
<template>
<v-form v-model="valid">
<v-text-field label="名前" :rules="[requiredRule]" />
<v-text-field label="メールアドレス" :rules="[requiredRule, emailRule]" />
<v-btn :disabled="!valid">送信</v-btn>
</v-form>
</template>解説
v-form の v-model でフォーム全体の valid を追跡する
v-form は ref を張って formRef.value.validate() を呼ぶ(drill 01 のやり方)以外に、
v-model を使ってフォーム全体の valid 状態を常時受け取る方法もある。
<script setup>
import { ref } from 'vue'
const valid = ref(null)
const name = ref('')
const email = ref('')
const requiredRule = (v) => !!v || '必須項目です'
const emailRule = (v) => !v || /.+@.+\..+/.test(v) || 'メール形式で入力してください'
</script>
<template>
<v-form v-model="valid">
<v-text-field v-model="name" label="名前" :rules="[requiredRule]" />
<v-text-field v-model="email" label="メールアドレス" :rules="[requiredRule, emailRule]" />
<v-btn :disabled="!valid">送信</v-btn>
</v-form>
</template>v-formにv-model="valid"を張ると、中のv-text-fieldの入力や rules の評価結果が変わるたびに、
Vuetify がフォーム全体の valid 状態を自動的に valid へ反映してくれる。個々の v-text-field の
結果を自分で集計して回る必要はない。
validの値はtrue/falseだけでなく、まだどの項目も入力・検証されていない段階ではnullになる。
:disabled="!valid" と書けば、null の間も「valid ではない」として扱われ、ボタンは押せないままになる。
- 送信ボタンを押して初めて
validate()の結果を確認する drill 01 の設計と違い、こちらは
入力中からリアルタイムに ボタンの押せる/押せないが切り替わる。
なぜ「送信して初めてエラーに気づく」より良いのか
validate() を送信時にだけ呼ぶ設計だと、ユーザーは一度ボタンを押すまで自分の入力が
正しいかどうかに気づけない。フォーム全体の valid 状態を v-model で常時追跡しておけば、
「入力が全部揃うまでボタンを押せなくする」というガードをそのまま UI に表現でき、
ユーザーが未入力・不正な入力のまま送信を試みて後からエラーに気づく、という体験を
未然に防げる。
入力欄が増えても、各欄には rules を渡すだけでよく、「フォーム全体の valid をどう集計するか」を
自分で書く必要がない点は drill 01 と同じ設計思想である。