こんにちは。ContractSでフロントエンドエンジニアをしている北原です。
弊社では、UIのバリデーションとしてVeeValidate v4
を採用しています。
この記事では、複数のインプットを1つのバリデーションフィールドとして扱う方法について解決策を提示したいと思います。
目次
前提知識
今回の記事の前提知識です。VeeValidate v4についてご存知の方はスルーしてください。
VeeValidateとは?
Vue.jsにおけるフォームバリデーションを簡素化する人気のライブラリです。
日本語ドキュメントも充実しており、大変扱いやすいものになっています。
vee-validate.logaretm.com
VeeValidate v4 の Composition関数
1. useField
const { value, errorMessage } = useField(() => 'email', yup.string().email().required(), )
useField
は、コンポーネントをフィールドとして扱うことができる関数です。
v-model
にbindして使う実際の入力値value
や、バリデーションルールにそぐわない場合に生成されるerrorMeesage
などを有しています。
vee-validate.logaretm.com
2. useForm
const { handleSubmit } = useForm()
useForm
は、記述したコンポーネントをフォームとして扱うことができる関数です。
前述したuseField
を有するコンポーネントを子コンポーネント以下に持つ場合に用いることで、その値を取得して送信できるhandleSubmit
などを有しています。
vee-validate.logaretm.com
なぜ必要なのか?
弊社のプロダクトでは、契約書を管理するに当たって契約項目
と呼ばれる関連情報を、各契約書が有しています。
それらをユーザの方へ入力していただく場合に必須項目としたいパターンが存在します。
このとき、以下のような別々のフィールドを、エラーメッセージを1箇所に集約してバリデーションをしたいパターンが出てきました。
このフィールドは、契約書に対する自動更新機能を有効にしたとき、契約書の契約期間終了後に新しく引き延ばす契約期間の長さを入力するフィールドです。
左側は値(半角数字)のインプット、右側は単位(日、ヶ月、年から選択)のプルダウンとなっており、2つで1つです。
しかしVeeValidateの仕様上、このようなパターンでは左右それぞれのフィールドが、個々のバリデーションを持つ別のフィールド値として扱われてしまうため、上の画像のようにエラーメッセージの表示箇所を集約できません。
なんとか解決する手段を考えた結果がこの記事になります。
TL;DR
- エラーメッセージを自身の管理対象として加える関数を
provide
するラッパーコンポーネントを作成 useField
を持つコンポーネントが、1. のコンポーネントからprovide
された関数をinject
してエラーメッセージを管理対象に加える
以上の流れで実現します。
やってみる
参考
VeeValidate v4開発者、Abdelrahman AwadさんのCodeSandboxにあった以下の内容を参考にしました。
実装例
1. ラッパーコンポーネントの作成
まず以下のように、動的にuseFieldの値を複数管理するための関数を提供するラッパーコンポーネントを実装します。
script
// Types interface Injection { errorMessage: Ref<string> // 他に管理したい対象があれば追加する } export type ProvideAddField = (injection: Injection) => void // Variables const errorMessages = ref<Ref<string>[]>([]) // Computed const allErrorMessages = computed<string[]>(() => (errorMessages.value.map(it => it.value).filter(it => !!it))) /** * エラーメッセージを登録する関数をprovideする */ onBeforeMount(() => { provide(ProvideKey.INCLUDE_FIELD_ERROR_MESSAGE, (injection: Injection) => { errorMessages.value.push(injection.errorMessage) }, ) })
onBeforeMount
のライフサイクルで、このコンポーネントへエラーメッセージを登録する関数をprovide
しておき、それによって追加されるエラーメッセージ群のerrorMessages
も持っておきます。
また、実際にエラーが発生しているものだけに絞り込んだallErrorMessages
も合わせて持っておきます。
template
<template> <div> <slot /> <!-- 複数のメッセージを表示したい場合はv-forすれば良い --> <AtomsErrorMessage v-show="allErrorMessages.length !== 0" class="mt-1" :message="allErrorMessages[0]" /> </div> </template>
template
部には、provide
した関数により追加されたエラーメッセージの表示箇所と、当該コンポーネントへエラーメッセージを追加したいフィールドコンポーネントを入れ込むためのslot
を記述します。
2. フィールドコンポーネントから関数をinject
script
const { value, errorMessage } = useField(() => 'email', yup.string().email().required(), ) // (中略) /** * エラーメッセージを、ラッパーコンポーネントの管理対象として追加します ※ */ const includeField = (errorMessage: Ref<string>) => { const injection: ProvideAddField | undefined = inject(ProvideKey.INCLUDE_FIELD_ERROR_MESSAGE) if (injection) injection({ errorMessage }) } onMounted(() => { includeField('email', errorMessage) })
フィールドコンポーネントへ、onBeforeMounted
でprovide
されている関数を、次のonMounted
ライフサイクルで呼出す処理を追加しておきます。
※ ここでは便宜上素で定義していますが、composablesにしておくのが良いです
3. 実際に使ってみる
ラッパーコンポーネントのslotへ、実際にバリデーションしたいフィールドコンポーネントを入れこみます。
これにより、エラーが発生された場合に1箇所へ集約して表示できるようになります。
まとめ
実装自体に手間はかかりますが、一度仕組み化してしまえば汎用的に利用できるため利便性は高いと思います。
もう少し簡素に実現できる方法がありましたら、ぜひご教授ください。
今回の記事が、少しでも同じような課題を抱えている方の助けになれば幸いです。
最後に
お読みいただきありがとうございました。
ContractSでは一緒にプロダクトを進化させていくエンジニアを募集中です。