Form Patterns
15.1.1. v-model Binding
Form input values must be managed using two-way v-model binding with ref. When a form has two or more fields, the state must be grouped into an object 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="Name" />
<input v-model="form.email" type="email" placeholder="Email" />
<input v-model="form.password" type="password" placeholder="Password" />
<label>
<input v-model="form.agreeTerms" type="checkbox" />
Agree to Terms of Service
</label>
</form>
</template>- Form state types must be declared using an
interface. - Initial values must be assigned with type-appropriate defaults such as empty strings (
''),false, ornull.
15.1.2. Form State Management
Asynchronous processing states of a form must be managed using the following four categories.
| State | Type | Purpose |
|---|---|---|
isLoading | boolean | Initial data loading (e.g., edit forms) |
isSubmitting | boolean | Whether submission is in progress |
isSuccess | boolean | Whether submission completed successfully |
errors | Record<string, string> | Per-field error messages |
typescript
const isLoading = ref(false)
const isSubmitting = ref(false)
const isSuccess = ref(false)
const errors = ref<Record<string, string>>({})isLoadingandisSubmittingmust not betruesimultaneously.errorsmust use field names as keys to map error messages.
15.1.3. Controlled Component Pattern
In Vue 3.4 and above, defineModel must be used to create custom input components.
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>The parent component binds directly using v-model.
vue
<template>
<BaseInput v-model="form.name" label="Name" :error="errors.name" />
<BaseInput v-model="form.email" label="Email" :error="errors.email" />
</template>defineModelautomatically generates themodelValueprop and theupdate:modelValueemit.- For environments below Vue 3.4, the
defineProps+defineEmitscombination must be used.
15.1.4. Form Submission Handling
Form submission must use @submit.prevent to prevent the default behavior and handle it with an async function. Duplicate submissions must be prevented.
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) {
// See Section 15.3 for error handling
} 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 ? 'Processing...' : 'Submit' }}
</button>
</form>
</template>@submit.preventmust be applied to all forms.- Duplicate submission prevention must combine both the
isSubmittingguard and the button'sdisabledattribute. - After successful submission, the form state must be reset to its initial values.