フォームパターン
15.1.1. v-modelバインディング
フォーム入力値はrefを使用したv-model双方向バインディングで管理します。フォームフィールドが2つ以上の場合、オブジェクトrefで状態をまとめて管理します。
vue
<script setup lang="ts">
import { ref } from 'vue'
interface SignUpForm {
name: string
email: string
password: string
agreeTerms: boolean
}
const form = ref<SignUpForm>({
name: '',
email: '',
password: '',
agreeTerms: false,
})
</script>
<template>
<form>
<input v-model="form.name" type="text" placeholder="名前" />
<input v-model="form.email" type="email" placeholder="メールアドレス" />
<input v-model="form.password" type="password" placeholder="パスワード" />
<label>
<input v-model="form.agreeTerms" type="checkbox" />
利用規約に同意
</label>
</form>
</template>- フォーム状態の型は
interfaceで明示します。 - 初期値は空文字列(
'')、false、nullなど型に合ったデフォルト値を割り当てます。
15.1.2. フォーム状態管理
フォームの非同期処理状態は以下の4種類に区分して管理します。
| 状態 | 型 | 用途 |
|---|---|---|
isLoading | boolean | 初期データの読み込み(編集フォームなど) |
isSubmitting | boolean | 送信中かどうか |
isSuccess | boolean | 送信成功完了かどうか |
errors | Record<string, string> | フィールドごとのエラーメッセージ |
typescript
const isLoading = ref(false)
const isSubmitting = ref(false)
const isSuccess = ref(false)
const errors = ref<Record<string, string>>({})isLoadingとisSubmittingは同時にtrueにならないように管理します。errorsはフィールド名をキーとしてエラーメッセージをマッピングします。
15.1.3. 制御コンポーネントパターン
Vue 3.4以上ではdefineModelを使用してカスタムInputコンポーネントを作成します。
vue
<script setup lang="ts">
// src/components/BaseInput.vue
const model = defineModel<string>({ required: true })
defineProps<{
label: string
error?: string
}>()
</script>
<template>
<div class="base-input">
<label>{{ label }}</label>
<input v-model="model" />
<span v-if="error" class="error">{{ error }}</span>
</div>
</template>親コンポーネントではv-modelで直接バインディングします。
vue
<template>
<BaseInput v-model="form.name" label="名前" :error="errors.name" />
<BaseInput v-model="form.email" label="メールアドレス" :error="errors.email" />
</template>defineModelはmodelValuePropsとupdate:modelValueEmitを自動生成します。- Vue 3.4未満の環境では
defineProps+defineEmitsの組み合わせを使用します。
15.1.4. フォーム送信処理
フォーム送信は@submit.preventでデフォルト動作をブロックし、非同期関数で処理します。重複送信を必ず防止しなければなりません。
vue
<script setup lang="ts">
import { ref } from 'vue'
import { createUser } from '@/api/user'
const form = ref({ name: '', email: '' })
const isSubmitting = ref(false)
const errors = ref<Record<string, string>>({})
async function handleSubmit() {
if (isSubmitting.value) return
isSubmitting.value = true
errors.value = {}
try {
await createUser(form.value)
form.value = { name: '', email: '' }
} catch (e) {
// エラー処理は15.3節を参照
} finally {
isSubmitting.value = false
}
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<input v-model="form.name" type="text" />
<input v-model="form.email" type="email" />
<button type="submit" :disabled="isSubmitting">
{{ isSubmitting ? '処理中...' : '登録' }}
</button>
</form>
</template>@submit.preventはすべてのフォームに必須で適用します。- 重複送信防止は
isSubmittingガードとボタンのdisabled属性を併用します。 - 送信成功後、フォーム状態を初期値にリセットします。