🌟

Vuelidate 2の悩んだところのメモ

2023/10/24に公開

前提

create-vueで作成したVue3 + Vuelidate 2の組み合わせで開発するにあたり
公式ドキュメントを読んでもわかりにくかったところ、迷ったところの解決方法を記載する

i18nはエラーメッセージを上書きする

Vuelidate公式のi18nサポートはバリデーターとi18nメッセージを関連付ける
同じバリデーターでも異なるi18nメッセージを表示したいことがあった

helpers.withMessageの第1引数を文字列を返す関数にする
vue-i18n-nextでi18nメッセージを返す

サンプルコード


const { t } = useI18n()
const rules = computed(() => ({
  name: {
    required: helpers.withMessage(() => t('required_key'), required)
  }
}))

useVuelidateのrule(第1引数のcomputed)は再評価されないようにする

再評価時に初期化を行うようで、$clearExternalResultsが正常に動作しないことがあった

再評価されないようにコードを記述する
(不具合がなくても余計な処理を行われないようにする)

サンプルコード

type Props = {
  isEnabled: boolean
}
const props = defineProps<Props>()

const { isEnabled: isEnabledRef } = toRef(props, 'isEnabled')

const rules = computed(() => ({
  name: {
    // NG: props.isEnabledが変更されるたびにcomputedが再評価される
    requiredIf: helpers.withMessage('必須入力です', requiredIf(props.isEnabled))
    // OK1: 関数内でpropsを参照する
    requiredIf: helpers.withMessage('必須入力です', requiredIf(() => props.isEnabled))
    // OK2: propsをrefに変換する
    requiredIf: helpers.withMessage('必須入力です', requiredIf(isEnabledRef))
  }
}))

標準バリデーター(Built-in Validator)のコール方法

required, numeric, maxLengthなどの用意されているバリデーターをコードにより呼び出す方法
$validatorメソッドをコールする、第2,3の引数は内部で使用している値を渡す

サンプルコード

const maxLength10 = maxLength(10)

const rules = computed(() => ({
  name: {
    maxLength: helpers.withMessage('数値かつ10文字以内で入力してください', (value: string, siblingState: unknown, vm: unknown) => {
      // propsにより検証を行わないことを想定したガード節
      if (!props.isEnabled) return true

      // 標準バリデーターを呼び出す(数値と文字数の検証)
      return numeric.$validator(value, siblingState, vm) && maxLength10.$validator(value, siblingState, vm)      
    })
  },
}))

増減する入力フォーム + APIの検証エラー

ドキュメントから下記は理解できたが、組み合わせ時がわからなかった
・増減する入力フォームの検証はNested Validationsを使用する
・APIの検証エラーはexternalResultsを使用する

「増減する入力フォーム + APIの検証エラー」の対応
APIの検証エラーはexternalResultsに変換し、子コンポーネントに渡す
子コンポーネントに渡したexternalResultsは再検証時に親コンポーネントに渡す

サンプルコード

VueUseのuseVModelsを使用しています

// UserFormType.ts
export type UserForm = {
  users: {
    name: string
    email: string
    externalResults?: {
      name: string[]
      email: string[]
    }
  }[]
}
// UserForm.vue(親コンポーネント)
<template>
  <form @submit.prevent="submitHandler">
    <template v-for="(user, index) in form.users" :key="index">
      <UserFormItem
        v-model:name="user.name"
        v-model:email="user.email"
        :external-results="user.externalResults"
      ></UserFormItem>
      <button>Send API</button>
    </template>
  </form>
</template>

<script setup lang="ts">
import { reactive } from 'vue'
import { useVuelidate } from '@vuelidate/core'
import UserFormItem from '@/components/UserFormItem.vue'
import type { UserForm } from '@/components/UserFormType'

const form = reactive<UserForm>({
  users: [
    { name: 'yamada', email: 'yamada@example.com' },
    { name: 'taro', email: 'taro@example.com' },
  ]
})

const v$ = useVuelidate()

const submitHandler = async () => {
  // 検証前に子コンポーネントのexternalResultsをクリアする
  // v$.value.$clearExternalResultsは子コンポーネントに使用できない
  form.users = form.users.map((user) => ({...user, externalResults: undefined }))

  // 検証(Nested Validationで子コンポーネントの検証を実行)
  const hasError = !(await v$.value.$validate())
  if (hasError) return

  // APIをコール
  // await fetch('https://example.com/users')

  // APIで検証エラーがある場合は、レスポンスをexternalResultsに変換し、フォームに入れる
  // 2つめのユーザの入力欄でエラーがある想定のコード
  form.users[1].externalResults = {
    name: ['重複しています', '不正な値です'],
    email: ['重複しています'],
  }
}
</script>
// UserFormItem.vue(子コンポーネント)
<template>
  <label>名前<input type="text" v-model="name" /></label>
  <p v-for="error in v$.name.$errors" :key="error.$uid">{{ error.$message }}</p>
  <label>メール<input type="text" v-model="email" /></label>
  <p v-for="error in v$.email.$errors" :key="error.$uid">{{ error.$message }}</p>
</template>

<script setup lang="ts">
import { computed, toRef } from 'vue'
import { helpers, required, email as emailValidate } from '@vuelidate/validators'
import { useVModels } from '@vueuse/core'
import { useVuelidate } from '@vuelidate/core'
import type { UserForm } from '@/components/UserFormType'

type Props = {
  name: UserForm['users']['0']['name']
  email: UserForm['users']['0']['email']
  externalResults?: UserForm['users']['0']['externalResults']
}
const props = withDefaults(defineProps<Props>(), {
  // Props未指定時(undefined時)はエラーなし(空配列)とする
  externalResults: () => ({
    name: [],
    email: [],
  })
})

type Emit = {
  (e: 'update:name', value: UserForm['users']['0']['name']): void
  (e: 'update:email', value: UserForm['users']['0']['email']): void
}
const emit = defineEmits<Emit>()

const { name, email } = useVModels(props, emit)
const rules = computed(() => ({
  name: {
    required: helpers.withMessage('必須入力です', required)
  },
  email: {
    required: helpers.withMessage('email形式で入力してください', emailValidate)
  },
}))

// propsのexternalResultsをrefに変換する
const externalResults = toRef(props, 'externalResults')
const v$ = useVuelidate(rules, { name, email }, { $externalResults: externalResults })
</script>

Discussion