こんにちは。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
const { handleSubmit } = useForm()
useFormは、記述したコンポーネント をフォームとして扱うことができる関数です。
前述したuseFieldを有するコンポーネント を子コンポーネント 以下に持つ場合に用いることで、その値を取得して送信できるhandleSubmitなどを有しています。
vee-validate.logaretm.com
なぜ必要なのか?
弊社のプロダクトでは、契約書を管理するに当たって契約項目と呼ばれる関連情報を、各契約書が有しています。
それらをユーザの方へ入力していただく場合に必須項目としたいパターンが存在します。
このとき、以下のような別々のフィールドを、エラーメッセージを1箇所に集約してバリデーションをしたいパターンが出てきました。
エラーメッセージを1箇所に集約したい例
このフィールドは、契約書に対する自動更新機能を有効にしたとき、契約書の契約期間終了後に新しく引き延ばす契約期間の長さを入力するフィールドです。
左側は値(半角数字)のインプット、右側は単位(日、ヶ月、年から選択)のプルダウンとなっており、2つで1つです。
しかしVeeValidateの仕様上、このようなパターンでは左右それぞれのフィールドが、個々のバリデーションを持つ別のフィールド値として扱われてしまうため、上の画像のようにエラーメッセージの表示箇所を集約できません。
なんとか解決する手段を考えた結果がこの記事になります。
TL;DR
エラーメッセージを自身の管理対象として加える関数をprovideするラッパーコンポーネント を作成
useFieldを持つコンポーネント が、1. のコンポーネント からprovideされた関数をinjectしてエラーメッセージを管理対象に加える
以上の流れで実現します。
やってみる
参考
VeeValidate v4開発者、Abdelrahman Awad さんのCodeSandboxにあった以下の内容を参考にしました。
https://codesandbox.io/p/sandbox/handling-nested-forms-in-vee-validate-v4-p45up?file=%2Fsrc%2Fcomponents%2FChildForm.vue%3A30%2C3-53%2C3
実装例
まず以下のように、動的にuseFieldの値を複数管理するための関数を提供するラッパーコンポーネント を実装します。
script
interface Injection {
errorMessage : Ref <string >
}
export type ProvideAddField = (injection : Injection ) => void
const errorMessages = ref<Ref <string >[]>([] )
const allErrorMessages = computed<string []>(() => (errorMessages.value.map (it => it .value).filter (it => !!it )))
onBeforeMount(() => {
provide(ProvideKey.INCLUDE_FIELD_ERROR_MESSAGE,
(injection : Injection ) => {
errorMessages.value.push (injection.errorMessage)
} ,
)
} )
onBeforeMountのライフサイクルで、このコンポーネント へエラーメッセージを登録する関数をprovideしておき、それによって追加されるエラーメッセージ群のerrorMessagesも持っておきます。
また、実際にエラーが発生しているものだけに絞り込んだallErrorMessagesも合わせて持っておきます。
template
< template >
< div >
< slot />
< 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では一緒にプロダクトを進化させていくエンジニアを募集中です。
recruit.jobcan.jp
recruit.jobcan.jp
recruit.jobcan.jp