Skip to content

表单输入绑定

在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript 中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦:

template
<input
  :value="text"
  @input="event => text = event.target.value">

v-model 指令帮我们简化了这一步骤:

template
<input v-model="text">
```guide/components/v-model 

另外,`v-model` 还可以用于各种不同的输入元素,`<textarea>`、`<select>` 元素。它会根据所使用的元素自动使用对应的 DOM 属性和事件组合:

- 文本类型的 `<input>` 和 `<textarea>` 元素会绑定 `value` property 并侦听 `input` 事件;
- `<input type="checkbox">` 和 `<input type="radio">` 会绑定 `checked` property 并侦听 `change` 事件;
- `<select>` 会绑定 `value` property 并侦听 `change` 事件。

::: tip 注意
`v-model` 会忽略任何表单元素上初始的 `value`、`checked` 或 `selected` attribute。它将始终将当前绑定的 JavaScript 状态视为数据的正确来源。你应该在 JavaScript 中使用响应式系统的 API来声明该初始值。
:::

## 基本用法

### 文本

```vue
<script setup>
import { ref } from 'vue'

const message = ref('')
</script>

<template>
  <p>Message is: {{ message }}</p>
  <input v-model="message" placeholder="edit me" />
</template>

Message is:

注意

对于需要使用 IME 的语言 (中文,日文和韩文等),你会发现 v-model 不会在 IME 输入还在拼字阶段时触发更新。如果你的确想在拼字阶段也触发更新,请直接使用自己的 input 事件监听器和 value 绑定而不要使用 v-model

多行文本

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

const message = ref('')
</script>

<template>
  <span>Multiline message is:</span>
  <p style="white-space: pre-line;">{{ message }}</p>
  <textarea v-model="message" placeholder="add multiple lines"></textarea>
</template>

注意在 <textarea> 中是不支持插值表达式的。请使用 v-model 来替代:

template
<!-- 错误 -->
<textarea>{{ text }}</textarea>

<!-- 正确 -->
<textarea v-model="text"></textarea>

复选框

单一的复选框,绑定布尔类型值:

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

const checked = ref(true)
</script>

<template>
  <input id="checkbox" type="checkbox" v-model="checked" />
  <label for="checkbox">{{ checked }}</label>
</template>

我们也可以将多个复选框绑定到同一个数组或集合的值:

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

const checkedNames = ref([])
</script>

<template>
  <div>Checked names: {{ checkedNames }}</div>

  <input id="jack" type="checkbox" value="Jack" v-model="checkedNames" />
  <label for="jack">Jack</label>

  <input id="john" type="checkbox" value="John" v-model="checkedNames" />
  <label for="john">John</label>

  <input id="mike" type="checkbox" value="Mike" v-model="checkedNames" />
  <label for="mike">Mike</label>
</template>

在这个例子中,checkedNames 数组将始终包含所有当前被选中的框的值。

单选按钮

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

const picked = ref('One')
</script>

<template>
  <div>Picked: {{ picked }}</div>

  <input id="one" type="radio" value="One" v-model="picked" />
  <label for="one">One</label>

  <input id="two" type="radio" value="Two" v-model="picked" />
  <label for="two">Two</label>
</template>

选择器

单个选择器的示例如下:

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

const selected = ref('')
</script>

<template>
  <div>Selected: {{ selected }}</div>

  <select v-model="selected">
    <option disabled value="">Please select one</option>
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>
</template>

注意

如果 v-model 表达式的初始值不匹配任何一个选择项,<select> 元素会渲染成一个"未选择"的状态。在 iOS 上,这将导致用户无法选择第一项,因为 iOS 在这种情况下不会触发一个 change 事件。因此,我们建议提供一个空值的禁用选项,如上面的例子所示。

多选 (值绑定到一个数组):

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

const selected = ref([])
</script>

<template>
  <div>Selected: {{ selected }}</div>

  <select v-model="selected" multiple>
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>
</template>

选择器的选项可以使用 v-for 动态渲染:

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

const selected = ref('A')

const options = ref([
  { text: 'One', value: 'A' },
  { text: 'Two', value: 'B' },
  { text: 'Three', value: 'C' }
])
</script>

<template>
  <select v-model="selected">
    <option v-for="option in options" :value="option.value">
      {{ option.text }}
    </option>
  </select>

  <div>Selected: {{ selected }}</div>
</template>

值绑定

对于单选按钮,复选框和选择器选项,v-model 绑定的值通常是静态的字符串 (或者对复选框是布尔值):

template
<!-- `picked` 在被选择时是字符串 "a" -->
<input type="radio" v-model="picked" value="a" />

<!-- `toggle` 只会是 true 或 false -->
<input type="checkbox" v-model="toggle" />

<!-- `selected` 在第一个选项被选中时是字符串 "abc" -->
<select v-model="selected">
  <option value="abc">ABC</option>
</select>

但有时我们可能希望将该值绑定到当前组件实例上的一个动态数据。这时可以用 v-bind 来实现。此外,使用 v-bind 还使我们可以将选项值绑定为非字符串的数据类型。

复选框

template
<input
  type="checkbox"
  v-model="toggle"
  true-value="yes"
  false-value="no" />

true-valuefalse-value 是 Vue 特有的 attributes,仅支持和 v-model 配套使用。这里 toggle 属性的值会在选中时被设为 'yes',取消选择时设为 'no'。你同样可以通过 v-bind 将其绑定为其他动态值:

template
<input
  type="checkbox"
  v-model="toggle"
  :true-value="dynamicTrueValue"
  :false-value="dynamicFalseValue" />

提示

true-valuefalse-value attributes 不会影响 value attribute,因为浏览器在表单提交时,并不会包含未选择的复选框。为了保证这两个值 (例如:"yes"和"no") 的其中之一被表单提交,请使用单选按钮作为替代。

单选按钮

template
<input type="radio" v-model="pick" :value="first" />
<input type="radio" v-model="pick" :value="second" />

pick 会在第一个按钮选中时被设为 first,在第二个按钮选中时被设为 second

选择器选项

template
<select v-model="selected">
  <!-- 内联对象字面量 -->
  <option :value="{ number: 123 }">123</option>
</select>

v-model 同样也支持非字符串类型的值绑定!在上面这个例子中,当某个选项被选中,selected 会被设为该对象字面量值 { number: 123 }

修饰符

.lazy

默认情况下,v-model 会在每次 input 事件后更新数据 (IME 拼字阶段的状态例外)。你可以添加 lazy 修饰符来改为在每次 change 事件后更新数据:

template
<!-- 在 "change" 事件后同步更新而不是 "input" -->
<input v-model.lazy="msg" />

.number

如果你想让用户输入自动转换为数字,你可以在 v-model 后添加 .number 修饰符来管理输入:

template
<input v-model.number="age" />

如果该值无法被 parseFloat() 处理,那么将返回原始值。

number 修饰符会在输入框有 type="number" 时自动启用。

.trim

如果你想要默认自动去除用户输入内容中两端的空格,你可以在 v-model 后添加 .trim 修饰符:

template
<input v-model.trim="msg" />

组件上的 v-model

如果你还不熟悉 Vue 的组件,可以先跳过这一小节。

HTML 的内置表单输入类型并不总能满足所有需求。幸运的是,Vue 的组件系统允许你创建具有自定义行为的可复用输入组件。这些输入组件甚至可以和 v-model 一起使用!

实际应用示例

用户注册表单

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

const form = reactive({
  username: '',
  email: '',
  password: '',
  confirmPassword: '',
  gender: '',
  interests: [],
  country: '',
  agreeTerms: false
})

const countries = ['中国', '美国', '英国', '日本', '韩国']
const interestOptions = ['编程', '音乐', '运动', '阅读', '旅行']

const submitForm = () => {
  if (form.password !== form.confirmPassword) {
    alert('密码不匹配!')
    return
  }
  
  if (!form.agreeTerms) {
    alert('请同意服务条款!')
    return
  }
  
  console.log('表单数据:', form)
  alert('注册成功!')
}
</script>

<template>
  <form @submit.prevent="submitForm" class="registration-form">
    <h2>用户注册</h2>
    
    <!-- 用户名 -->
    <div class="form-group">
      <label for="username">用户名:</label>
      <input 
        id="username" 
        v-model.trim="form.username" 
        type="text" 
        required 
        placeholder="请输入用户名"
      />
    </div>
    
    <!-- 邮箱 -->
    <div class="form-group">
      <label for="email">邮箱:</label>
      <input 
        id="email" 
        v-model.trim="form.email" 
        type="email" 
        required 
        placeholder="请输入邮箱"
      />
    </div>
    
    <!-- 密码 -->
    <div class="form-group">
      <label for="password">密码:</label>
      <input 
        id="password" 
        v-model="form.password" 
        type="password" 
        required 
        placeholder="请输入密码"
      />
    </div>
    
    <!-- 确认密码 -->
    <div class="form-group">
      <label for="confirmPassword">确认密码:</label>
      <input 
        id="confirmPassword" 
        v-model="form.confirmPassword" 
        type="password" 
        required 
        placeholder="请再次输入密码"
      />
    </div>
    
    <!-- 性别 -->
    <div class="form-group">
      <label>性别:</label>
      <div class="radio-group">
        <input id="male" type="radio" value="male" v-model="form.gender" />
        <label for="male">男</label>
        
        <input id="female" type="radio" value="female" v-model="form.gender" />
        <label for="female">女</label>
        
        <input id="other" type="radio" value="other" v-model="form.gender" />
        <label for="other">其他</label>
      </div>
    </div>
    
    <!-- 兴趣爱好 -->
    <div class="form-group">
      <label>兴趣爱好:</label>
      <div class="checkbox-group">
        <div v-for="interest in interestOptions" :key="interest">
          <input 
            :id="interest" 
            type="checkbox" 
            :value="interest" 
            v-model="form.interests" 
          />
          <label :for="interest">{{ interest }}</label>
        </div>
      </div>
    </div>
    
    <!-- 国家 -->
    <div class="form-group">
      <label for="country">国家:</label>
      <select id="country" v-model="form.country" required>
        <option value="">请选择国家</option>
        <option v-for="country in countries" :key="country" :value="country">
          {{ country }}
        </option>
      </select>
    </div>
    
    <!-- 同意条款 -->
    <div class="form-group">
      <input id="terms" type="checkbox" v-model="form.agreeTerms" />
      <label for="terms">我同意服务条款和隐私政策</label>
    </div>
    
    <!-- 提交按钮 -->
    <button type="submit" :disabled="!form.agreeTerms">注册</button>
    
    <!-- 表单数据预览 -->
    <div class="form-preview">
      <h3>表单数据预览:</h3>
      <pre>{{ JSON.stringify(form, null, 2) }}</pre>
    </div>
  </form>
</template>

<style scoped>
.registration-form {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
}

.form-group {
  margin-bottom: 15px;
}

.form-group label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
}

.form-group input[type="text"],
.form-group input[type="email"],
.form-group input[type="password"],
.form-group select {
  width: 100%;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 14px;
}

.radio-group,
.checkbox-group {
  display: flex;
  gap: 15px;
  flex-wrap: wrap;
}

.checkbox-group {
  flex-direction: column;
}

.checkbox-group > div {
  display: flex;
  align-items: center;
  gap: 5px;
}

button {
  width: 100%;
  padding: 10px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
}

button:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}

.form-preview {
  margin-top: 20px;
  padding: 15px;
  background-color: #f8f9fa;
  border-radius: 4px;
}

.form-preview pre {
  font-size: 12px;
  overflow-x: auto;
}
</style>

动态表单验证

vue
<script setup>
import { ref, computed, watch } from 'vue'

const email = ref('')
const password = ref('')
const confirmPassword = ref('')

// 邮箱验证
const emailError = computed(() => {
  if (!email.value) return ''
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  return emailRegex.test(email.value) ? '' : '请输入有效的邮箱地址'
})

// 密码验证
const passwordError = computed(() => {
  if (!password.value) return ''
  if (password.value.length < 6) return '密码至少需要6个字符'
  if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(password.value)) {
    return '密码必须包含大小写字母和数字'
  }
  return ''
})

// 确认密码验证
const confirmPasswordError = computed(() => {
  if (!confirmPassword.value) return ''
  return password.value === confirmPassword.value ? '' : '两次输入的密码不一致'
})

// 表单是否有效
const isFormValid = computed(() => {
  return email.value && 
         password.value && 
         confirmPassword.value &&
         !emailError.value && 
         !passwordError.value && 
         !confirmPasswordError.value
})

// 监听密码变化,清空确认密码
watch(password, () => {
  if (confirmPassword.value) {
    confirmPassword.value = ''
  }
})
</script>

<template>
  <form class="validation-form">
    <h2>表单验证示例</h2>
    
    <div class="form-group">
      <label for="email">邮箱:</label>
      <input 
        id="email"
        v-model="email"
        type="email"
        :class="{ error: emailError }"
        placeholder="请输入邮箱"
      />
      <span v-if="emailError" class="error-message">{{ emailError }}</span>
    </div>
    
    <div class="form-group">
      <label for="password">密码:</label>
      <input 
        id="password"
        v-model="password"
        type="password"
        :class="{ error: passwordError }"
        placeholder="请输入密码"
      />
      <span v-if="passwordError" class="error-message">{{ passwordError }}</span>
    </div>
    
    <div class="form-group">
      <label for="confirmPassword">确认密码:</label>
      <input 
        id="confirmPassword"
        v-model="confirmPassword"
        type="password"
        :class="{ error: confirmPasswordError }"
        placeholder="请再次输入密码"
      />
      <span v-if="confirmPasswordError" class="error-message">{{ confirmPasswordError }}</span>
    </div>
    
    <button type="submit" :disabled="!isFormValid">
      {{ isFormValid ? '提交' : '请完善表单信息' }}
    </button>
  </form>
</template>

<style scoped>
.validation-form {
  max-width: 400px;
  margin: 0 auto;
  padding: 20px;
}

.form-group {
  margin-bottom: 20px;
}

.form-group label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
}

.form-group input {
  width: 100%;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 14px;
  transition: border-color 0.3s;
}

.form-group input:focus {
  outline: none;
  border-color: #007bff;
}

.form-group input.error {
  border-color: #dc3545;
}

.error-message {
  display: block;
  color: #dc3545;
  font-size: 12px;
  margin-top: 5px;
}

button {
  width: 100%;
  padding: 10px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
  transition: background-color 0.3s;
}

button:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}

button:not(:disabled):hover {
  background-color: #0056b3;
}
</style>

选项式 API

如果你使用的是选项式 API,可以这样使用表单绑定:

vue
<script>
export default {
  data() {
    return {
      message: '',
      checked: false,
      picked: '',
      selected: '',
      multiSelected: []
    }
  },
  methods: {
    onSubmit() {
      console.log({
        message: this.message,
        checked: this.checked,
        picked: this.picked,
        selected: this.selected,
        multiSelected: this.multiSelected
      })
    }
  }
}
</script>

<template>
  <form @submit.prevent="onSubmit">
    <!-- 文本输入 -->
    <input v-model="message" placeholder="输入消息" />
    
    <!-- 复选框 -->
    <input type="checkbox" v-model="checked" />
    
    <!-- 单选按钮 -->
    <input type="radio" value="A" v-model="picked" />
    <input type="radio" value="B" v-model="picked" />
    
    <!-- 选择器 -->
    <select v-model="selected">
      <option>A</option>
      <option>B</option>
      <option>C</option>
    </select>
    
    <!-- 多选 -->
    <select v-model="multiSelected" multiple>
      <option>A</option>
      <option>B</option>
      <option>C</option>
    </select>
    
    <button type="submit">提交</button>
  </form>
</template>

性能优化

使用 v-model.lazy

对于不需要实时更新的表单字段,使用 .lazy 修饰符可以减少不必要的更新:

template
<!-- 只在失去焦点时更新 -->
<input v-model.lazy="searchQuery" placeholder="搜索..." />

防抖处理

对于搜索框等需要频繁更新的输入,可以使用防抖:

vue
<script setup>
import { ref, watch } from 'vue'
import { debounce } from 'lodash-es'

const searchQuery = ref('')
const searchResults = ref([])

const performSearch = debounce(async (query) => {
  if (query) {
    // 执行搜索逻辑
    searchResults.value = await searchAPI(query)
  } else {
    searchResults.value = []
  }
}, 300)

watch(searchQuery, performSearch)
</script>

大型表单优化

对于包含大量字段的表单,考虑使用 shallowRef 或分组管理:

vue
<script setup>
import { shallowRef } from 'vue'

// 使用 shallowRef 减少深层响应式开销
const formData = shallowRef({
  personalInfo: {
    name: '',
    email: '',
    phone: ''
  },
  address: {
    street: '',
    city: '',
    zipCode: ''
  }
})

const updateField = (section, field, value) => {
  formData.value = {
    ...formData.value,
    [section]: {
      ...formData.value[section],
      [field]: value
    }
  }
}
</script>

最佳实践

  1. 使用合适的修饰符

    • .trim 用于去除空格
    • .number 用于数字输入
    • .lazy 用于减少更新频率
  2. 表单验证

    • 使用计算属性进行实时验证
    • 提供清晰的错误提示
    • 在提交前进行最终验证
  3. 用户体验

    • 提供占位符文本
    • 使用适当的输入类型
    • 添加加载状态和禁用状态
  4. 性能考虑

    • 对于复杂表单使用防抖
    • 避免在 v-model 中使用复杂计算
    • 合理使用 .lazy 修饰符

下一步

vue study guide - 专业的 Vue.js 学习平台