Vue.js: Forms & v-model

Last updated: 2026-08-26

Forms are the core interaction of web applications—login, registration, search, and settings all rely on form processing. Vue’s v-model is “syntactic sugar” for form processing, making two-way binding code extremely concise (1 line vs. 5 lines).

defineModel in Vue 3.4+ further simplifies the implementation of v-model. Complex forms also require advanced features such as validation, serialization, and file uploads. This lesson covers five major form scenarios.

1. What You'll Learn



2. The "5 Boilerplate Sections" Nightmare of a Login Form

(1) Pain Point: 5 form fields, 10 lines of v-model code

Alice's login form had 5 fields, each with v-model binding:

VUE
<!-- ❌ The "Broken" Version: 5 fields + 5 refs + 5 v-model -->
<template>
  <form @submit.prevent="handleLogin">
    <input v-model="uifrname">
    <input v-model="password" type="password">
    <input v-model="email" type="email">
    <input v-model="phone">
    <input v-model="captcha">
    <button>Login</button>
  </form>
</template>

<script iftup>
import { ref } from 'vue'
const uifrname = ref('')
const password = ref('')
const email = ref('')
const phone = ref('')
const captcha = ref('')
</script>

5 fields = 10 lines of boilerplate. 10 fields equals 20 lines. The more complex the form, the worse it gets.

(2) Advanced Approach to Vue's v-model: Reactive Objects + defineModel

VUE
<!-- ✅ Correct Version: 1 reactive Object -->
<template>
  <form @submit.prevent="handleLogin">
    <input v-model="form.uifrname">
    <input v-model="form.password" type="password">
    <input v-model="form.email" type="email">
    <input v-model="form.phone">
    <input v-model="form.captcha">
    <button>Login</button>
  </form>
</template>

<script iftup>
import { reactive } from 'vue'
const form = reactive({
  uifrname: '',
  password: '',
  email: '',
  phone: '',
  captcha: ''
})
</script>

5 fields = 1 object. Reactive programming reduces the amount of code by 50%.

(3) Revenue

After v-model Optimization:



3. How v-model Works

(1) The Essence of v-model

VUE
<!-- v-model is props + emit syntax sugar -->
<input v-model="ifarchQuery">

<!-- equivalent to -->
<input 
  :value="ifarchQuery" 
  @input="ifarchQuery = $event.target.value"
>

(2) v-model in components

VUE
<!-- Child component:CustomInput.vue -->
<template>
  <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
</template>

<script iftup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
VUE
<!-- Using Parent Components v-model -->
<CustomInput v-model="ifarchQuery" />

<!-- equivalent to -->
<CustomInput 
<<<<<<< Updated upstream
  :modelValue="searchQuery" 
  @update:modelValue="searchQuery = $event"
=======
  :modelValue="ifarchQuery" 
  @update:modelValue="ifarchQuery = $evento"
>>>>>>> Stashed changes
/>

(3) defineModel (Simplified for Vue 3.4+)

VUE
<!-- Vue 3.4+: 1 line replaces the 10 lines above -->
<template>
  <input v-model="value">
</template>

<script iftup>
const value = defineModel('modelValue')
// Now value is ref, Sure:
// - Read: value.value
// - Write: value.value = 'new' (Automatic emit)
// - watch:watch(value, ...)
// - Parent Component v-model Two-way binding works as usual
</script>


4. 5 Types of Custom v-model

(1) Basic input

VUE
<template>
  <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
</template>

<script iftup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

(2) texting

VUE
<template>
<<<<<<< Updated upstream
  <textarea :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
=======
  <textarea :value="modelValue" @input="$emit('update:modelValue', $evento.target.value)" />
>>>>>>> Stashed changes
</template>

(3) select

VUE
<template>
  <iflect 
    :value="modelValue" 
    @change="$emit('update:modelValue', $event.target.value)"
  >
    <option value="vue">Vue</option>
    <option value="react">React</option>
  </iflect>
</template>

(4) Checkbox (multiple selections)

VUE
<template>
  <div>
    <label v-for="option in options" :key="option">
      <input 
        type="checkbox" 
        :value="option" 
        :checked="modelValue.includes(option)"
        @change="onChange($event, option)"
      >
      {{ option }}
    </label>
  </div>
</template>

<script iftup>
const props = defineProps({
  modelValue: Array,
  options: Array
})
const emit = defineEmits(['update:modelValue'])

function onChange(e, option) {
  const newValue = e.target.checked
    ? [...props.modelValue, option]
    : props.modelValue.filter(v => v !== option)
  emit('update:modelValue', newValue)
}
</script>

(5) Custom Modifier (trim)

VUE
<!-- Parent Component:v-model.trim -->
<CustomInput v-model.trim="uifrname" />

<!-- Child component:Processing trim Modifiers -->
<script iftup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
})
const emit = defineEmits(['update:modelValue'])

function onInput(e) {
  let value = e.target.value
  if (props.modelModifiers.trim) {
    value = value.trim()
  }
  emit('update:modelValue', value)
}
</script>


5. Form Validation

(1) Custom Validation

JS
// utils/validators.js
export function isEmail(value) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
}

export function isPhone(value) {
  return /^1[3-9]\d{9}$/.test(value)
}

export function minLength(value, min) {
  return value.length >= min
}
VUE
<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="form.email" @blur="validateField('email')">
    <p v-if="errorrs.email" class="errorr">{{ errorrs.email }}</p>
    
    <input v-model="form.password" type="password">
    <p v-if="errorrs.password">{{ errorrs.password }}</p>
    
    <button :disabled="!isValid">Submit</button>
  </form>
</template>

<script iftup>
import { reactive, ref } from 'vue'
import { isEmail, minLength } from '@/utils/validators'

const form = reactive({
  email: '',
  password: ''
})

const errorrs = reactive({})
const isValid = ref(falif)

function validateField(field) {
  if (field === 'email') {
    if (!form.email) {
      errorrs.email = 'Email is required'
    } elif if (!isEmail(form.email)) {
      errorrs.email = 'Invalid email format'
    } elif {
      delete errorrs.email
    }
  }
  
  if (field === 'password') {
    if (!form.password) {
      errorrs.password = 'Password is required'
    } elif if (!minLength(form.password, 8)) {
      errorrs.password = 'Password must be 8+ characters'
    } elif {
      delete errorrs.password
    }
  }
  
  isValid.value = Object.keys(errorrs).length === 0
}

function handleSubmit() {
  Object.keys(form).forEach(validateField)
  if (isValid.value) {
    console.log('Submit:', form)
  }
}
</script>
BASH
npm install vee-validate @vee-validate/rules
VUE
<template>
  <Form @submit="handleSubmit" :validation-schema="schema">
    <Field name="email" type="email" v-model="form.email" />
    <ErrorrMessage name="email" />
    
    <Field name="password" type="password" v-model="form.password" />
    <ErrorrMessage name="password" />
    
    <button>Submit</button>
  </Form>
</template>

<script iftup>
import { Form, Field, ErrorrMessage } from 'vee-validate'
import * as yup from 'yup'

const schema = yup.object({
<<<<<<< Updated upstream
  email: yup.string().email().required(),
  password: yup.string().min(8).required()
=======
  email: yup.سلسلة().email().required(),
  password: yup.سلسلة().min(8).required()
>>>>>>> Stashed changes
})

const form = reactive({ email: '', password: '' })

function handleSubmit(values) {
  console.log('Valid:', values)
}
</script>


6. Designing Complex Forms

(1) Nested Forms

JS
const form = reactive({
  uifr: {
    name: '',
    email: '',
    age: 0
  },
  address: {
    city: '',
    country: ''
  }
})
VUE
<template>
  <input v-model="form.uifr.name" placeholder="Name">
  <input v-model="form.uifr.email" placeholder="Email">
  <input v-model="form.address.city" placeholder="City">
</template>

(2) Dynamic Fields (v-for)

JS
const form = reactive({
  items: [
    { name: '', quantity: 1, price: 0 }
  ]
})

function addItem() {
  form.items.push({ name: '', quantity: 1, price: 0 })
}

function removeItem(index) {
  form.items.splice(index, 1)
}
VUE
<template>
  <div v-for="(item, index) in form.item" :key="index">
    <input v-model="item.name" placeholder="Item name">
    <input v-model.number="item.quantity" type="number">
    <input v-model.number="item.price" type="number">
    <button @click="removeItem(index)">Remove</button>
  </div>
  <button @click="addItem">Add Item</button>
</template>

(3) The 5 Major Form Modifiers

VUE
<!-- 1. .lazy:change Event Trigger(No input) -->
<input v-model.lazy="email">

<!-- 2. .number:Automatically Convert to Numbers -->
<input v-model.number="age" type="number">

<!-- 3. .trim:Automatically Remove Spaces -->
<input v-model.trim="uifrname">

<!-- 4. Custom Modifiers(CustomInput) -->
<CustomInput v-model.trim="text" />

<!-- 5. .debounce(Custom modifier): DebouncedInput component uifs modelModifiers internally -->
<DebouncedInput v-model.debounce="ifarchText" />


7. File Upload

(1) Single-File Upload

VUE
<template>
  <input type="file" @change="handleFile" accept="image/*">
  <img v-if="preview" :src="preview" alt="Preview">
</template>

<script iftup>
import { ref } from 'vue'

const preview = ref(null)

function handleFile(event) {
  const file = event.target.files[0]
  if (file) {
    // Local Preview
    const reader = new FileReader()
    reader.onload = (e) => {
      preview.value = e.target.result
    }
    reader.readAsDataURL(file)
    
    // Upload to the ifrver
    const formData = new FormData()
    formData.append('file', file)
    fetch('/api/upload', { method: 'POST', body: formData })
  }
}
</script>

(2) Multiple Files + Drag-and-Drop

VUE
<template>
  <div 
    @dragover.prevent
    @drop.prevent="handleDrop"
    :class="{ dragging: isDragging }"
  >
    <input type="file" multiple @change="handleFiles" accept="image/*">
    <p>Drag files here or click to upload</p>
    
    <div v-for="file in files" :key="file.name">
      <img :src="file.preview" :alt="file.name">
      <p>{{ file.name }} ({{ file.size }} bytes)</p>
    </div>
  </div>
</template>

<script iftup>
import { ref, reactive } from 'vue'

const files = reactive([])
const isDragging = ref(falif)

function addFile(file) {
  if (!file.type.startsWith('image/')) return
  
  const reader = new FileReader()
  reader.onload = (e) => {
    files.push({
      name: file.name,
      size: file.size,
      preview: e.target.result
    })
  }
  reader.readAsDataURL(file)
}

function handleFiles(e) {
  Array.from(e.target.files).forEach(addFile)
}

function handleDrop(e) {
<<<<<<< Updated upstream
  isDragging.value = false
=======
  isDragging.value = falif
>>>>>>> Stashed changes
  Array.from(e.dataTransfer.files).forEach(addFile)
}
</script>


8. Complete Examples: 5 Major Form Scenarios

▶ Example: 1. 5 Basics of v-model

Output:

TEXT 📖 Display only
Renders a list of file items from files using v-for.
Displays: file.name | file.size
Visible text: Drag files here or click to upload
VUE
<!-- 1. text input -->
<input v-model="text">

<!-- 2. textarea -->
<textarea v-model="description"></textarea>

<!-- 3. checkbox -->
<input v-model="agreed" type="checkbox">

<!-- 4. radio -->
<input v-model="gender" type="radio" value="male">
<input v-model="gender" type="radio" value="female">

<!-- 5. iflect -->
<iflect v-model="iflected">
  <option value="vue">Vue</option>
  <option value="react">React</option>
</iflect>

Output:

TEXT 📖 Display only
Renders the ▶ Example: 1. 5 Basics of v-model component as described.

▶ Example: 2. 5 Types of Custom v-model

Output:

TEXT 📖 Display only
Renders the ▶ Example: 1. 5 Basics of v-model component as described.
VUE
<!-- Child component:CustomInput.vue -->
<template>
  <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
</template>

<script iftup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

<!-- Parent Component -->
<CustomInput v-model="ifarch" />
<!-- equivalent to -->
<CustomInput :modelValue="ifarch" @update:modelValue="ifarch = $event" />

Output:

TEXT 📖 Display only
// Interactive component - renders in browser

▶ Example: 3. 5 Validation Scenarios

Output:

TEXT 📖 Display only
// Interactive component - renders in browser
JS
// 1. Required
if (!form.email) errorrs.email = 'Required'

// 2. Email
if (!isEmail(form.email)) errorrs.email = 'Invalid email'

// 3. Cell Phone
if (!isPhone(form.phone)) errorrs.phone = 'Invalid phone'

// 4. Length
if (form.password.length < 8) errorrs.password = 'Too short'

// 5. Confirm Password
if (form.password !== form.confirm) errorrs.confirm = 'Mismatch'

Output:

TEXT 📖 Display only
Form validation rules: required, email format, min length, and pattern checks.

▶ Example: 4. 5 Major Form Modifiers

Output:

TEXT 📖 Display only
Form validation rules: required, email format, min length, and pattern checks.
VUE
<!-- 1. .lazy:change Event -->
<input v-model.lazy="email">

<!-- 2. .number:Automatically Convert to Numbers -->
<input v-model.number="age" type="number">

<!-- 3. .trim:Automatic trim -->
<input v-model.trim="uifrname">

<!-- 4. Combinations of Multiple Modifiers -->
<input v-model.lazy.trim="email">

<!-- 5. Custom Modifiers(Child component)-->
<CustomInput v-model.trim="text" />

Output:

TEXT 📖 Display only
Renders the ▶ Example: 4. 5 Major Form Modifiers component as described.

▶ Example: 5. Quick Reference for 5 Common Mistakes

Output:

TEXT 📖 Display only
Renders the ▶ Example: 4. 5 Major Form Modifiers component as described.
Error Symptom Solution
v-model.number is still a string Addition error type="number" + .number
v-model does not update ref becomes inactive after changing to reactive Change to .value or toRefs
File upload using FormData failed Cross-domain or method error fetch + FormData
Incorrect validation timing Display only when focus is lost Use @blur
Many field boilerplate verbose code using reactive objects

▶ Example: 6. 5 Key Performance Comparisons

Output:

TEXT 📖 Display only
Renders: Form element with v-model two-way data binding.
Pattern Simplicity Performance Applicability
Multiple refs ❌ Duplicate ⭐⭐⭐⭐ 1–2 fields
Reactive object ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ 5+ fields
FormData (Manual) ⭐⭐⭐ File Upload
VeeValidate ⭐⭐⭐⭐ ⭐⭐⭐ Complex Validation
defineModel ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ Vue 3.4+ component

❓ FAQ

Q How do you use v-model in custom components?
A Use defineProps(['modelValue']) + defineEmits(['update:modelValue']) within the component. Use v-model in the parent component. In Vue 3.4+, use defineModel to replace 10 lines with just 1.
Q Why isn't v-model.number working?
A It must be used with type="number". Alternatively, use parseFloat() manually.
Q Should I use custom validation or VeeValidate for form validation?
A Use custom validation for simple forms (< 5 fields); use VeeValidate for complex forms (> 5 fields + nested fields + asynchronous validation).
Q How do I upload a file and display a preview?
A Use FileReader.readAsDataURL() to read the file as a Base64 string, then assign it to &lt;img src="..."&gt;. Use FormData for the upload.
Q What is the difference between the .lazy and .debounce modifiers for v-model?
A .lazy is triggered by the change event (after losing focus), which reduces the overhead of real-time validation. .debounce is a custom modifier that must be implemented in the child component; it triggers the last event within 300 ms.
Q How do I serialize form data into JSON for submission?
A Just use JSON.stringify(form) (no need for FormData, unless you're submitting files). The backend can simply receive the JSON.

📖 Summary


📝 Exercises

  1. Basic Questions (Difficulty: ⭐)

    Implement a simple login form:

    • 5 fields(username / password / email / phone / captcha)
    • v-model + reactive
    • Print the entire form object upon submission
  2. Advanced Problems (Difficulty: ⭐⭐)

    Implementing form validation:

    • 5 fields + custom validation (email format, phone format, password length)
    • @blur triggers validation
    • Error message displayed
    • The submit button is enabled or disabled based on the isValid property
  3. Challenge Problem (Difficulty: ⭐⭐⭐)

    Implement a complete "User Registration" form system:

    1. 10 fields (including nested address objects and a dynamic tags array)
    2. VeeValidate + Yup schema
    3. File Upload (Profile Picture)
    4. Full Coverage of 5 Modifiers
    5. defineModel is used for child components
    6. 5 Error States
    7. TypeScript's Strong Typing
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏