複数の連絡先、参加者、明細行のように、ユーザーが入力項目を自分で増やしたり減らしたりするフォームを、動的フォームと呼びます。項目の数が決まっていないため、配列でデータを持ち、v-forで描画するのが基本です。この記事では、Vue 3(<script setup>)で、追加・削除、項目ごとのエラー表示、送信までを動くコードで整理します。
配列と v-for で項目を追加・削除する
入力欄を、配列の要素に1対1で対応させます。pushで追加し、filterで削除します。ここで大事なのがkeyです。各項目に固有のidを持たせ、:key="field.id"と指定します。インデックスをkeyにすると、途中の項目を削除したときに、要素とデータの対応がずれる原因になります。理由はv-forで配列やオブジェクトをループして表示する方法で解説しています。入力欄はv-modelで、配列の要素に結びつけます。
<script setup>
import { ref } from 'vue'
const MAX = 5 // 追加できる項目の上限
let nextId = 2 // 次に付ける id
// 各項目に、固有の id を持たせる
const fields = ref([{ id: 1, name: '' }])
function addField() {
if (fields.value.length >= MAX) return
fields.value.push({ id: nextId++, name: '' })
}
function removeField(id) {
if (fields.value.length <= 1) return // 最後の1件は消さない
fields.value = fields.value.filter(field => field.id !== id)
}
</script>
<template>
<div v-for="field in fields" :key="field.id">
<input v-model="field.name" placeholder="名前を入力">
<button
type="button"
:disabled="fields.length <= 1"
@click="removeField(field.id)"
>
削除
</button>
</div>
<button type="button" :disabled="fields.length >= MAX" @click="addField">
項目を追加
</button>
</template>
上のコードでは、運用でよく困る2点にも備えています。項目数の上限(MAX)を決め、最後の1件は削除できないようにしています。
項目ごとのバリデーションと送信
動的フォームのバリデーションで大切なのは、「どの項目が間違っているか」を、その項目の近くに出すことです。computedで、項目のidごとにエラーメッセージを作り、入力欄の下に表示します。送信ボタンを押す前から赤く表示すると、入力中に煩わしいので、submittedのフラグで、送信後だけ見せています。
<script setup>
import { ref, computed } from 'vue'
const MAX = 5
let nextId = 2
const fields = ref([{ id: 1, name: '' }])
const submitted = ref(false) // 送信ボタンを押したあとだけ、エラーを見せる
function addField() {
if (fields.value.length >= MAX) return
fields.value.push({ id: nextId++, name: '' })
}
function removeField(id) {
if (fields.value.length <= 1) return
fields.value = fields.value.filter(field => field.id !== id)
}
// 項目ごとのエラーメッセージ({ id: メッセージ })
const errors = computed(() =>
Object.fromEntries(
fields.value.map(field => [
field.id,
field.name.trim() === '' ? '名前を入力してください' : ''
])
)
)
const hasError = computed(() => Object.values(errors.value).some(msg => msg !== ''))
function onSubmit() {
submitted.value = true
if (hasError.value) return
// 送信するデータの形を整える(前後の空白を除く)
const payload = fields.value.map(field => field.name.trim())
console.log(payload)
}
</script>
<template>
<form @submit.prevent="onSubmit">
<div v-for="field in fields" :key="field.id">
<input v-model="field.name" placeholder="名前を入力">
<button type="button" @click="removeField(field.id)">削除</button>
<p v-if="submitted && errors[field.id]" class="error">
{{ errors[field.id] }}
</p>
</div>
<button type="button" @click="addField">項目を追加</button>
<button type="submit">送信</button>
</form>
</template>
送信するデータは、画面の状態そのままではなく、送信用に整えます。上の例では、前後の空白を取り除いた名前の配列にしています。computedの使い方はcomputedプロパティで動的なデータ処理を行う方法、フォーム全般のバリデーションは入力フォームのバリデーション機能を実装する方法をご覧ください。
VeeValidateで動的フォームを作る
項目の種類やルールが増えてきたら、バリデーションライブラリのVeeValidateが便利です。配列のフォームには、useFieldArrayを使います。まずパッケージを入れます。VeeValidateの導入の詳細はバリデーションライブラリvee-validateの導入と使い方をご覧ください。
npm install vee-validate yup @vee-validate/yup
Yupのスキーマは、@vee-validate/yupのtoTypedSchemaで包んで渡します。公式は、この書き方を推奨しています。生のYupスキーマも渡せますが、フォームの値に型が付かず、必須の項目でも、送信された値がnull許容のままになるためです。
<script setup>
import { useForm, useFieldArray, Field, ErrorMessage } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/yup'
import * as yup from 'yup'
// スキーマ:people という配列の各要素に、name が必須というルール
const schema = toTypedSchema(
yup.object({
people: yup
.array()
.of(
yup.object({
name: yup.string().required('名前は必須です')
})
)
.min(1)
})
)
const { handleSubmit } = useForm({
validationSchema: schema,
initialValues: { people: [{ name: '' }] }
})
// fields は { key, value, isFirst, isLast } の配列。push / remove で増減できる
const { fields, push, remove } = useFieldArray('people')
const onSubmit = handleSubmit(values => {
console.log(values.people)
})
</script>
<template>
<form @submit="onSubmit">
<!-- key には、自動で付く field.key を使う -->
<div v-for="(field, idx) in fields" :key="field.key">
<Field :name="`people[${idx}].name`" placeholder="名前を入力" />
<button type="button" @click="remove(idx)">削除</button>
<ErrorMessage :name="`people[${idx}].name`" />
</div>
<button type="button" @click="push({ name: '' })">項目を追加</button>
<button type="submit">送信</button>
</form>
</template>
useFieldArray('people')が返すfieldsは、key、value、isFirst、isLastを持つ要素の配列です。v-forのkeyには、自動で付くfield.keyを使います。- 各入力欄は、
people[0].nameのような名前で、フォームの値に結びつけます。 push、removeのほか、insert、swap、moveなどで、並べ替えもできます。
実用シーンと拡張
- 明細付きの請求フォーム(項目、金額、数量)。金額と数量から合計を
computedで計算する - 複数のファイル名やURLの登録欄
- 質問と回答のセットを増やす入力画面
- 項目の中に、さらに配列を持つ入れ子の構造(
useFieldArrayを入れ子で使う)
関連記事
v-modelの仕組みはv-modelの仕組みを完全理解- ボタンのイベントはv-onでDOMイベントを購読する方法
よくある質問(FAQ)
idを持たせて、keyにしてください。VeeValidateではfield.keyです。@vee-validate/yupのtoTypedSchemaで包む書き方を推奨しています。型が付き、必須項目の値がnull許容にならないためです。まとめ
動的フォームは、固有のidを持つ配列と、v-forで作ります。バリデーションは、どの項目が間違っているかを項目ごとに示し、送信時には送信用の形に整えます。ルールが増えたら、VeeValidateのuseFieldArrayとtoTypedSchemaに任せると、見通しよく管理できます。

