ContractS開発者ブログ

契約マネジメントシステム「ContractS CLM」の開発者ブログです。株式会社HolmesはContractS株式会社に社名変更しました。

複数フィールドのバリデーションエラーを集約して表示するVeeValidate v4の活用法

こんにちは。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箇所に集約してバリデーションをしたいパターンが出てきました。

エラーメッセージを1箇所に集約したい例

このフィールドは、契約書に対する自動更新機能を有効にしたとき、契約書の契約期間終了後に新しく引き延ばす契約期間の長さを入力するフィールドです。
左側は値(半角数字)のインプット、右側は単位(日、ヶ月、年から選択)のプルダウンとなっており、2つで1つです。

しかしVeeValidateの仕様上、このようなパターンでは左右それぞれのフィールドが、個々のバリデーションを持つ別のフィールド値として扱われてしまうため、上の画像のようにエラーメッセージの表示箇所を集約できません。

なんとか解決する手段を考えた結果がこの記事になります。

TL;DR

  1. エラーメッセージを自身の管理対象として加える関数をprovideするラッパーコンポーネントを作成
  2. 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

実装例

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)
})

フィールドコンポーネントへ、onBeforeMountedprovideされている関数を、次のonMountedライフサイクルで呼出す処理を追加しておきます。

※ ここでは便宜上素で定義していますが、composablesにしておくのが良いです

3. 実際に使ってみる

ラッパーコンポーネントのslotへ、実際にバリデーションしたいフィールドコンポーネントを入れこみます。

これにより、エラーが発生された場合に1箇所へ集約して表示できるようになります。

テンプレート記述例

まとめ

実装自体に手間はかかりますが、一度仕組み化してしまえば汎用的に利用できるため利便性は高いと思います。
もう少し簡素に実現できる方法がありましたら、ぜひご教授ください。

今回の記事が、少しでも同じような課題を抱えている方の助けになれば幸いです。

最後に

お読みいただきありがとうございました。
ContractSでは一緒にプロダクトを進化させていくエンジニアを募集中です。

recruit.jobcan.jp

recruit.jobcan.jp

recruit.jobcan.jp