VeeValidateをV4へアップグレード ~Fieldのカスタムコンポーネント~
今回Nuxt2からNuxt3への移行作業に携わる機会がありました。VueのバリデーションライブラリであるVeeValidateも、移行に伴って大きく仕様変更がなされています。
この記事ではバージョンアップによる移行の方針や注意事項の共有ができればと思っています。
※記事中のコードは本記事用に記述したものですので、実際のコードとは異なります。
VeeValidate V3との違い
単体のバリデーションから打って変わり、フォームのバリデーションへ
あくまで個人の感想ですが、V3は他のフォームライブラリに比べると独特な記法でした。
ReactでポピュラーなフォームライブラリであるFormikでは、htmlの form
に対応する Form
コンポーネントをルートとして <Field/>
コンポーネントが束ねられます。バリデーションスキーマはルートである Form
で定義する感じです。VeeValidate V4においても概ね同じような構造となります。
対するV3では、フォームというひとまとまりのバリデーションというよりは、一つの input
に対してのバリデーションを定義(ValidationProvider
)し、ついでにフォームとしてまとめあげる機能(ValidationObserver
)もあるという印象です。
これは複数ページにわたって必要事項を記入していく申込サイトやフォームと呼ぶほどでもない入力欄のバリデーションといったユースケースに向いていますよね。
Vue3でコンポジションAPIが導入されたこともV4のコンセプトに影響を与えたのだろうと思いますが、別物感は否めないですね・・・。
各種バリデーションライブラリを使用できるようになった
V3までのルールの記法に加えて、yupやzodなどサードパーティーのバリデーションライブラリが利用できるようになっています。したがって、V4からVeeValidateを使い始める場合は使い勝手がいいと思います。
V4でも従来の記法はサポートされていますので、移行に際してはGlobal Validatorの登録手順が変わった以外は大きな修正は不要です。
他にもいろいろ
<ErrorMessage/>
コンポーネントが追加されました。これはFormikと同様に、対象の<Field/>
を指定しておくことでエラーメッセージを表示してくれます。今回の記事では扱いませんが、手軽にフォームを作成できて便利ですね。
また、<FieldArray/>
コンポーネントも追加されました。これは複数の<Field/>
をまとめて管理できるコンテナのようなもので、Todoリストなど可変であってもバリデーションでき、リスト内のアイテムの移動、入れ替えや追加・削除もできます。
移行の方針
移行作業をするにあたって、機能の開発と並行して取り組む必要があったため、定期的に変更をNuxt3移行ブランチに取り込まなければなりませんでした。
したがって、V4の機能を最大限活用するというよりは、なるべくコードの変更量を最小限にして移行できるようにしています。
移行作業と注意点
まずは関連パッケージのインストールを行います。
npm i -D vee-validate @vee-validate/i18n @vee-validate/rules
プラグイン定義
プロジェクトではNuxtを使用していますので、以下のコードはすべて/plugins/veeValidate.ts
の以下関数内に記述しています。
export default defineNuxtPlugin((nuxtApp) => {
// ここに記述
})
組み込みルール
extendをdefineRuleに書き換えればOKです。
// before
import * as rules from 'vee-validate/dist/rules';
import { extend } from "vee-validate";
Object.keys(rules).forEach(rule => {
extend(rule, rules[rule]);
});
// after
import { defineRule } from 'vee-validate';
import * as AllRules from '@vee-validate/rules';
Object.keys(AllRules).forEach(rule => {
defineRule(rule, AllRules[rule]);
});
カスタムルール
組み込みルールと基本は同じですが、一部記述方法が変わっていますので、書き直す必要があります。
- バリデーションルールは関数のみになりました。
- フィールド名はコンテクストオブジェクトから取得するようになりました。
- params(Cross-Field Validationなどで使われる、
:rules="is_not:foo"
のfoo
の部分)がオブジェクトではなく配列になりました。
Global Validators (logaretm.com)
extendに関数を渡していた場合は簡単:
// before
extend("phoneNumber", (value, params) => {
return /^(050|070|080|090)\d{8}$/.test(value) ? true : `正しい携帯電話番号を入力してください。`;
})
// after
defineRule("phoneNumber", (value, params) => {
return /^(050|070|080|090)\d{8}$/.test(value) ? true : `正しい携帯電話番号を入力してください。`;
})
extendにオブジェクトを渡していた場合:
// before
extend("password-confirm", {
params: ["password"],
validate: (value, { password }) => {
if(value !== password){
return "{_field_}は同じ値を入力してください。"
}
return true;
},
})
// after
defineRule<string, { password: string }>("passwordConfirm", (value, [ password ], ctx) => {
if(value !== params[0]){
return `${ctx.field}は同じ値を入力してください。`
}
return true;
})
params
の型もオブジェクトから配列に変更されていますので、注意が必要です。詳しいrules
の記法は公式ドキュメントで確認してください。
ctx
は以下の型となっています。
interface FieldValidationMetaInfo {
field: string;
name: string;
label?: string;
value: unknown;
form: Record<string, unknown>;
rule?: {
name: string;
params?: Record<string, unknown> | unknown[];
};
}
上記のpasswordConfirm
の場合、ctx
は以下の値が入ります。form
はForm全体の値を保持しています。field
とlabel
の違いは分かりませんが、V3から移行する場合はfield
を見ておけばよさそうです。
{
"field": "パスワード確認",
"name": "passwordConfirm",
"label": "パスワード確認",
"value": "pass0123",
"form": {
"password": "pass0123",
"passwordConfirm": "pass4567"
},
"rule": {
"name": "passwordConfirm",
"params": [
"pass0123"
]
}
}
グローバルコンポーネントの登録
この記事では移行負荷を下げることを目的としていますので、コンポーネント名の差異を吸収するようにしています。どちらにしろ名称は一括置換ですぐに終わりますのでご自由に。ValidationProviderは後述しますが、カスタムコンポーネントを作成して内部でFieldを作るようにしています。
import { Form, ErrorMessage } from "vee-validate";
import { ValidationProvider } from "~/components/ValidationProvider"
declare module '@vue/runtime-core' {
export interface GlobalComponents {
ValidationObserver: typeof Form
ValidationProvider: typeof ValidationProvider
ValidationErrorMessage: typeof ErrorMessage
}
}
export default defineNuxtPlugin( (nuxtApp) => {
nuxtApp.vueApp.component('ValidationObserver', Form);
nuxtApp.vueApp.component('ValidationProvider', ValidationProvider);
nuxtApp.vueApp.component('ValidationErrorMessage', ErrorMessage);
// ...
ローカライズ
ローカライズはこの記事をそのまま利用します。
Form(ValidationObserver)
VeeValidate V4では、コンポーネントで記述できる<Form>
と、Vue3で登場したコンポーザブルで記述できるuseForm()
のどちらかを使うことができます。内部的には、<Form>
もuseForm()
を使っています。 V3のValidationObserver
から比較するといくつか変更はありますが、いずれも大きな労力とはならないと思います。
V3でのValidationObserverは以下のようなものです:
<ValidationObserver slim v-slot="{ invalid }" />
<button :disabled="invalid">申し込む</button>
<form>
タグの抑止
HTMLのV4の<Form>
では、名前の通り<form>
を描画するようになりました。V3ではデフォルトでは<span>
タグで、tag="form"
のようにタグを指定していました。V4ではas
プロパティを使って描画するHTMLタグを指定できます。as=""
と指定することで、タグを描画しないようにもできます。これはV3ではslim
プロパティに該当します。
今回のプロジェクトでは複数ページにわたる申込サイトであるため、あまり好ましい変更ではありません。したがって、以下のように書き換えます。
<ValidationObserver as="" v-slot="{ invalid }" />
<button :disabled="invalid">申し込む</button>
v-slotの変更
v-slotも変更があります。dirty
やvalid
といったフォームのメタ情報は、meta
オブジェクトの子へ変更されました。
<ValidationObserver as="" v-slot="{ meta: { valid } }" />
<button :disabled="!valid">申し込む</button>
また、invalid
は削除されました。代替は以下になります:
<ValidationObserver as="" v-slot="{ meta: { valid, validated, dirty } }" />
<span v-show="!valid && validated">フォームを入力してください。</span>
<span v-show="!valid && dirty">フォームを入力してください。</span>
dirty
は値が変更されるとtrue
になります。申し込みが複数ページに渡るなど、フィールドに初期値がセットされる場合ではdirty
がfalse
になるため、validated
を見た方がいいです。
fields
プロパティの代替
V3でのfields
オブジェクトは公式ドキュメントには存在しませんが、実際にはスコープドスロットとして各フィールドの情報を取得することができていました。
V3でのfields
オブジェクトの型は以下です:
// fields: Record<string, ObserverField>;
interface ObserverField {
id: string;
name: string;
failedRules: Record<string, string>;
pristine: boolean;
dirty: boolean;
touched: boolean;
untouched: boolean;
valid: boolean;
invalid: boolean;
pending: boolean;
validated: boolean;
changed: boolean;
passed: boolean;
failed: boolean;
}
これはフォームの無効な項目をページ下部に一覧表示する際や、記入したフィールドの数に応じてシークバーを進めたりするのに便利でしたが、V4ではこれに値する機能はなくなってしまいました。
今回は代替案として、ValidationProviderでField作成時にVuexストアにメタ情報を保存するような方策を取りました。詳細は後述します。
ValidationObserverのref
ValidationObserverのrefも上記の変更を受けていますので注意が必要です。
validやdirtyなどフォームのmeta情報やフォームのリセット等は、V4より追加されたコンポーザブルを利用できます。
import { useIsFormValid } from 'vee-validate';
const isValid = useIsFormValid();
isValid.value; // true or false
注意点として、上記のコンポーザブルはValidationObserver
を呼び出しているコンポーネント、およびそれより先祖のコンポーネントでは呼び出せません。その場合はValidationObserver
のref
からアクセスするしかなさそうです。
Field(ValidationProvider)
<Field/>
でもコンポーネントとuseField()
コンポーザブルのどちらかを利用できます。
公式のFieldコンポーネントを使うパターン
まずは公式で用意されている<Field/>
をそのまま使うケースについて説明します。
V3
V4主な変更は以下です:
- rulesの指定が不要な場合、nullの代わりに空文字を指定する必要があります。
- ValidationObserverと同様、
slim
,tag
はas
へ変更になりました。 - vidはnameに変更されました。
- nameはlabelに変更されました。
- modeは廃止されました。
特にmodeことインタラクションモードがV4では廃止されたことが大きな変更となります。この挙動を再現する手法はドキュメントに例がありますが、<Field/>
では再現ができません。必ずuseField()
を使う必要があります。
次のセクションでは上のドキュメントを拡張したカスタムコンポーネントを作成していきます。
useFieldを用いたカスタムコンポーネント
動作例やコードについてはCodeSandboxを用意しました。
以下は概要です。
- ドキュメントのコード例は
input
を内包したカスタムフィールドですが、より汎用的に利用したいため、<slot/>
に置き換えました。 -
<ValidationProvider/>
の呼び出し元ではv-slot
でhandlers
を取り出して<input/>
のイベントを購読しています。-
<Field/>
を使う場合と同様に値の変更は<ValidationProvider/>
で行います。 -
<input/>
にv-model
で渡してしまうと、値を変更した際に二重でpassword
が更新されてしまいます。:value
を渡してください。
-
-
vid
,name
はV3との互換性を維持しています。 - ValidationProviderのrefからは
useField()
の返り値を取得できるようにしています。 - 各modeのバリデーションイベントのハンドリング処理は記事のものを一部変更して、blurイベントもバリデーションの対象にしています。
-
<ValidationProvider/>
内のuseField
のオプションでsyncVModel: true
を指定することで、自動で変更をemitしてくれます。defineEmits
も忘れずに。- mode=
validateOnUpdate
は<input/>
以外から値を変更する場合に必要になります。本来はuseField
のvalidateOnValueUpdate: true
とするだけで動作するはずなのですが、動作しないのでwatchを追加しています。
- mode=
- ValidationProviderのref、v-slotからFieldの情報にアクセスできるようにするためにuseFieldの返り値をすべて
defineExpose()
,<slot />
に渡しています。 - ページの下部にバリデーションエラーの項目をまとめて表示するために
Pinia
でvalidかどうかや日本語名を保持するようにしています。これによって<ValidationObserver/>
のfields
の代替の役割を果たします。
Cleave.jsとの併用
Cleave.jsを使っている場合は、<ValidationProvider/>
で値を変更するように変わっているため、結構手が込んだ対応が必要となります。
ポイントとしては、
-
<ValidationProvider/>
でCleave.jsのインスタンスを持ち、Cleave.jsのonValueChange
を購読して生の値で更新・バリデーションします。 -
<ValidationProvider/>
からv-slotでCleave.jsのインスタンス登録用の関数をエクスポートし、Cleave.jsインスタンスを呼び出します。
また時間があればサンプルを更新したい・・・
引用・参考
Discussion