Vue 3（Composition API）+ Vuelidateでにおいて、配列の各要素に対してバリデーションを実装する

こんばんは。

今日はVue 3（Composition API）+ Vuelidate において、配列の各要素に対してバリデーションを行う場合の実装例をご紹介します。

今回のポイントは以下の通りです。

- 配列を持つ親コンポーネントと、
- 配列の要素（オブジェクト）に対するフォームを扱う子コンポーネント

を分けることで、要素ごとにバリデーションを実行しやすくする構成です。

# 親コンポーネント

## 親コンポーネントの概要

- 親コンポーネントで「配列のデータ」と「Vuelidate のバリデーションルール（rules）」を定義し、`useVuelidate`を呼び出します。
- 各要素を描画するときに、`:vuelidate="$v.items[index]"`という形で要素ごとの Vuelidate 状態を子コンポーネントへ渡します。

```ParentForm.vue 
<template>
  <div>
    <h1>親コンポーネント</h1>

    <!-- 配列データ(items)をループして、子コンポーネントに渡す -->
    <div v-for="(item, index) in items" :key="index" style="margin-bottom: 16px;">
      <ItemForm
        :item="item"
        :vuelidate="$v.items[index]"
      />
    </div>

    <!-- バリデーション全体のチェックを行う例 -->
    <button @click="onSubmit">送信</button>
    <div v-if="submitErrorMessage" style="color: red;">
      {{ submitErrorMessage }}
    </div>
  </div>
</template>

<script>
import { ref, computed } from 'vue'
import useVuelidate from '@vuelidate/core'
import { required } from '@vuelidate/validators'
import ItemForm from './ItemForm.vue'

export default {
  name: 'ParentForm',
  components: {
    ItemForm,
  },
  setup() {
    // バリデーション対象となるデータ
    const items = ref([
      { name: '', age: '' },
      { name: '', age: '' },
    ])

    // Vuelidate で使用するルール定義
    // 配列の各要素に対して $each でルールを定義できる
    const rules = computed(() => ({
      items: {
        $each: {
          name: { required },
          age: { required },
        },
      },
    }))

    // useVuelidate の呼び出し（第二引数は実際のデータ）
    const v$ = useVuelidate(rules, { items })

    const submitErrorMessage = ref('')

    // 送信時のバリデーション例
    const onSubmit = async () => {
      // 全体のバリデーションチェック
      const isValid = await v$.value.$validate()
      if (!isValid) {
        submitErrorMessage.value = '入力に不備があります。'
      } else {
        submitErrorMessage.value = ''
        // 送信処理 ...
        alert('送信成功！')
      }
    }

    return {
      items,
      v$,
      onSubmit,
      submitErrorMessage,
    }
  },
}
</script>
```

上記の例では、`items`配列に対して、`$each`キーを使い、
`name`と`age`それぞれに`required`バリデーションを付与しています。

- `v$.value.items[index]`には、該当インデックスの要素の`Vuelidate`状態が格納されます。

# 子コンポーネント

## 子コンポーネントの概要

- 親から受け取った「配列の要素（`item`）」と「要素ごとの Vuelidate インスタンス（`vuelidate`）」
- 入力フォームとバリデーションエラー表示
- 自分自身の内部では`useVuelidate`を呼ばず、親コンポーネントから既に生成されたインスタンスを受け取る形にする

```ItemForm.vue
<template>
  <div>
    <h2>子コンポーネント (ItemForm)</h2>

    <div>
      <label>名前:</label>
      <input v-model="item.name" />
      <!-- name フィールドのバリデーションエラー表示例 -->
      <span v-if="vuelidate?.name?.$error" style="color: red;">
        ※名前は必須です
      </span>
    </div>

    <div>
      <label>年齢:</label>
      <input type="number" v-model="item.age" />
      <!-- age フィールドのバリデーションエラー表示例 -->
      <span v-if="vuelidate?.age?.$error" style="color: red;">
        ※年齢は必須です
      </span>
    </div>
  </div>
</template>

<script>
export default {
  name: 'ItemForm',
  props: {
    // 親から受け取る、配列の要素
    item: {
      type: Object,
      required: true,
    },
    // 親から受け取る、要素ごとの Vuelidate インスタンス
    vuelidate: {
      type: Object,
      required: true,
    },
  },
}
</script>
```

- `v-model="item.name"`のように、直接親のデータを変更しています。
- バリデーションエラーの表示に`vuelidate?.name?.$error`を使用しています。
    - `?.`はオプショナルチェーン演算子で、`vuelidate`や`vuelidate.name`が未定義の場合でもエラーにならないようにするためのものです。

# 配列要素の追加・削除を行う場合

もし配列要素の追加や削除を行う場合は、以下のように親コンポーネントで`items`を変更してあげれば動的にバリデーションが適用されます。

```vue
<template>
  <div>
    <button @click="addItem">要素を追加</button>

    <div v-for="(item, index) in items" :key="index">
      <!-- 要素を削除するボタン例 -->
      <button @click="removeItem(index)">削除</button>
      <ItemForm :item="item" :vuelidate="$v.items[index]" />
    </div>
  </div>
</template>

<script>
import { ref, computed } from 'vue'
import useVuelidate from '@vuelidate/core'
import { required } from '@vuelidate/validators'
import ItemForm from './ItemForm.vue'

export default {
  components: { ItemForm },
  setup() {
    const items = ref([
      { name: '', age: '' },
    ])

    const rules = computed(() => ({
      items: {
        $each: {
          name: { required },
          age: { required },
        },
      },
    }))

    const v$ = useVuelidate(rules, { items })

    const addItem = () => {
      items.value.push({ name: '', age: '' })
    }

    const removeItem = (index) => {
      items.value.splice(index, 1)
    }

    return {
      items,
      v$,
      addItem,
      removeItem,
    }
  },
}
</script>
```

このようにすると、要素を追加するたびに配列に新しいオブジェクトが追加され、そのオブジェクトにもバリデーションが動的に適用されるようになります。

# まとめ

- 親コンポーネントで配列データ (`items`) と`Vuelidate`のルール定義・インスタンス生成を行う。
- 子コンポーネントでは親から渡された単一要素（item）と対応する`Vuelidate`インスタンス（`vuelidate`）を使ってフォーム入力とバリデーションエラー表示を行う。
- 追加・削除など動的に要素を操作しても、`Vuelidate`が`$each`を通じて自動的に管理してくれる。

以上の手順で、配列に対して要素ごとのバリデーションを行う構成が実装可能です。

ぜひ参考にしてみてください。

今日は以上です。

ありがとうございました。
よろしくお願いいたします。