Vue 表單處理
表單是網頁應用程式中最常見的互動元素。Vue 提供了 v-model 指令,讓表單資料綁定變得簡單直覺。
基本表單綁定
文字輸入
<script setup>
import { ref } from 'vue'
const message = ref('')
</script>
<template>
<input v-model="message" type="text" placeholder="輸入訊息">
<p>訊息:{{ message }}</p>
</template>
多行文字
<script setup>
import { ref } from 'vue'
const content = ref('')
</script>
<template>
<textarea v-model="content" placeholder="輸入內容"></textarea>
<p style="white-space: pre-line;">{{ content }}</p>
</template>
在
<textarea> 中,{% raw %}{{ content }}{% endraw %} 的插值方式不會作用,必須使用 v-model。核取方塊(Checkbox)
單一核取方塊
綁定布林值:
<script setup>
import { ref } from 'vue'
const checked = ref(false)
</script>
<template>
<label>
<input type="checkbox" v-model="checked">
同意條款
</label>
<p>是否同意:{{ checked }}</p>
</template>
多選核取方塊
綁定陣列:
<script setup>
import { ref } from 'vue'
const selected = ref([])
</script>
<template>
<label><input type="checkbox" value="vue" v-model="selected"> Vue</label>
<label><input type="checkbox" value="react" v-model="selected"> React</label>
<label><input type="checkbox" value="angular" v-model="selected"> Angular</label>
<p>已選擇:{{ selected }}</p>
</template>
單選按鈕(Radio)
<script setup>
import { ref } from 'vue'
const picked = ref('')
</script>
<template>
<label><input type="radio" value="one" v-model="picked"> 選項一</label>
<label><input type="radio" value="two" v-model="picked"> 選項二</label>
<label><input type="radio" value="three" v-model="picked"> 選項三</label>
<p>選擇:{{ picked }}</p>
</template>
下拉選單(Select)
單選
<script setup>
import { ref } from 'vue'
const selected = ref('')
</script>
<template>
<select v-model="selected">
<option value="" disabled>請選擇</option>
<option value="a">選項 A</option>
<option value="b">選項 B</option>
<option value="c">選項 C</option>
</select>
<p>選擇:{{ selected }}</p>
</template>
多選
<script setup>
import { ref } from 'vue'
const selected = ref([])
</script>
<template>
<select v-model="selected" multiple>
<option value="a">選項 A</option>
<option value="b">選項 B</option>
<option value="c">選項 C</option>
</select>
<p>選擇:{{ selected }}</p>
</template>
動態選項
<script setup>
import { ref } from 'vue'
const selected = ref('')
const options = ref([
{ value: 'tw', label: '台灣' },
{ value: 'jp', label: '日本' },
{ value: 'us', label: '美國' }
])
</script>
<template>
<select v-model="selected">
<option value="" disabled>請選擇國家</option>
<option v-for="opt in options" :key="opt.value" :value="opt.value">
{{ opt.label }}
</option>
</select>
</template>
v-model 修飾符
.lazy
預設情況下,v-model 會在 input 事件後同步資料。.lazy 修飾符會改為在 change 事件後才同步:
<template>
<!-- 失去焦點或按 Enter 後才更新 -->
<input v-model.lazy="message">
</template>
.number
自動將輸入轉為數字:
<script setup>
import { ref } from 'vue'
const age = ref(0)
</script>
<template>
<input v-model.number="age" type="number">
<p>年齡:{{ age }}(型別:{{ typeof age }})</p>
</template>
.trim
自動去除首尾空白:
<template>
<input v-model.trim="message">
</template>
值綁定
自訂核取方塊的值
<script setup>
import { ref } from 'vue'
const toggle = ref('no')
</script>
<template>
<input
type="checkbox"
v-model="toggle"
true-value="yes"
false-value="no"
>
<p>{{ toggle }}</p>
</template>
綁定物件
<script setup>
import { ref } from 'vue'
const selected = ref(null)
const options = [
{ id: 1, name: '選項一' },
{ id: 2, name: '選項二' },
{ id: 3, name: '選項三' }
]
</script>
<template>
<select v-model="selected">
<option :value="null" disabled>請選擇</option>
<option v-for="opt in options" :key="opt.id" :value="opt">
{{ opt.name }}
</option>
</select>
<p v-if="selected">選擇的物件:{{ selected.name }}(ID: {{ selected.id }})</p>
</template>
表單驗證
基本驗證
<script setup>
import { ref, reactive, computed } from 'vue'
const form = reactive({
name: '',
email: '',
password: ''
})
const errors = reactive({
name: '',
email: '',
password: ''
})
const isValid = computed(() => {
return !errors.name && !errors.email && !errors.password &&
form.name && form.email && form.password
})
function validateName() {
if (!form.name) {
errors.name = '請輸入姓名'
} else if (form.name.length < 2) {
errors.name = '姓名至少 2 個字'
} else {
errors.name = ''
}
}
function validateEmail() {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!form.email) {
errors.email = '請輸入 Email'
} else if (!emailRegex.test(form.email)) {
errors.email = '請輸入有效的 Email'
} else {
errors.email = ''
}
}
function validatePassword() {
if (!form.password) {
errors.password = '請輸入密碼'
} else if (form.password.length < 6) {
errors.password = '密碼至少 6 個字元'
} else {
errors.password = ''
}
}
function handleSubmit() {
validateName()
validateEmail()
validatePassword()
if (isValid.value) {
console.log('提交表單:', form)
}
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<div class="field">
<label>姓名</label>
<input
v-model="form.name"
@blur="validateName"
:class="{ error: errors.name }"
>
<span class="error-msg">{{ errors.name }}</span>
</div>
<div class="field">
<label>Email</label>
<input
v-model="form.email"
type="email"
@blur="validateEmail"
:class="{ error: errors.email }"
>
<span class="error-msg">{{ errors.email }}</span>
</div>
<div class="field">
<label>密碼</label>
<input
v-model="form.password"
type="password"
@blur="validatePassword"
:class="{ error: errors.password }"
>
<span class="error-msg">{{ errors.password }}</span>
</div>
<button type="submit" :disabled="!isValid">送出</button>
</form>
</template>
<style scoped>
.field {
margin-bottom: 16px;
}
label {
display: block;
margin-bottom: 4px;
}
input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
input.error {
border-color: #dc3545;
}
.error-msg {
color: #dc3545;
font-size: 14px;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
完整表單範例
註冊表單
<script setup>
import { reactive, computed } from 'vue'
const form = reactive({
username: '',
email: '',
password: '',
confirmPassword: '',
gender: '',
interests: [],
country: '',
bio: '',
agree: false
})
const countries = [
{ value: 'tw', label: '台灣' },
{ value: 'jp', label: '日本' },
{ value: 'us', label: '美國' },
{ value: 'other', label: '其他' }
]
const interestOptions = ['程式設計', '設計', '音樂', '運動', '閱讀']
const passwordMatch = computed(() => {
return form.password === form.confirmPassword
})
const canSubmit = computed(() => {
return form.username &&
form.email &&
form.password &&
form.confirmPassword &&
passwordMatch.value &&
form.agree
})
function handleSubmit() {
if (!canSubmit.value) return
console.log('註冊資料:', { ...form })
alert('註冊成功!')
}
function handleReset() {
Object.assign(form, {
username: '',
email: '',
password: '',
confirmPassword: '',
gender: '',
interests: [],
country: '',
bio: '',
agree: false
})
}
</script>
<template>
<form @submit.prevent="handleSubmit" class="register-form">
<h2>會員註冊</h2>
<!-- 使用者名稱 -->
<div class="form-group">
<label for="username">使用者名稱 *</label>
<input
id="username"
v-model="form.username"
type="text"
required
>
</div>
<!-- Email -->
<div class="form-group">
<label for="email">Email *</label>
<input
id="email"
v-model="form.email"
type="email"
required
>
</div>
<!-- 密碼 -->
<div class="form-group">
<label for="password">密碼 *</label>
<input
id="password"
v-model="form.password"
type="password"
minlength="6"
required
>
</div>
<!-- 確認密碼 -->
<div class="form-group">
<label for="confirmPassword">確認密碼 *</label>
<input
id="confirmPassword"
v-model="form.confirmPassword"
type="password"
required
>
<span v-if="form.confirmPassword && !passwordMatch" class="error">
密碼不一致
</span>
</div>
<!-- 性別 -->
<div class="form-group">
<label>性別</label>
<div class="radio-group">
<label>
<input type="radio" v-model="form.gender" value="male"> 男
</label>
<label>
<input type="radio" v-model="form.gender" value="female"> 女
</label>
<label>
<input type="radio" v-model="form.gender" value="other"> 其他
</label>
</div>
</div>
<!-- 興趣 -->
<div class="form-group">
<label>興趣</label>
<div class="checkbox-group">
<label v-for="interest in interestOptions" :key="interest">
<input
type="checkbox"
v-model="form.interests"
:value="interest"
>
{{ interest }}
</label>
</div>
</div>
<!-- 國家 -->
<div class="form-group">
<label for="country">國家</label>
<select id="country" v-model="form.country">
<option value="">請選擇</option>
<option v-for="c in countries" :key="c.value" :value="c.value">
{{ c.label }}
</option>
</select>
</div>
<!-- 自我介紹 -->
<div class="form-group">
<label for="bio">自我介紹</label>
<textarea
id="bio"
v-model="form.bio"
rows="4"
placeholder="簡單介紹一下自己..."
></textarea>
</div>
<!-- 同意條款 -->
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" v-model="form.agree">
我同意服務條款和隱私政策 *
</label>
</div>
<!-- 按鈕 -->
<div class="form-actions">
<button type="button" @click="handleReset">重置</button>
<button type="submit" :disabled="!canSubmit">註冊</button>
</div>
<!-- 預覽 -->
<details class="preview">
<summary>預覽表單資料</summary>
<pre>{{ JSON.stringify(form, null, 2) }}</pre>
</details>
</form>
</template>
<style scoped>
.register-form {
max-width: 500px;
margin: 0 auto;
padding: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 4px;
font-weight: bold;
}
.form-group input[type="text"],
.form-group input[type="email"],
.form-group input[type="password"],
.form-group select,
.form-group textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.radio-group,
.checkbox-group {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.radio-group label,
.checkbox-group label,
.checkbox-label {
font-weight: normal;
display: flex;
align-items: center;
gap: 4px;
}
.form-actions {
display: flex;
gap: 10px;
margin-top: 20px;
}
.form-actions button {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.form-actions button[type="submit"] {
background-color: #42b883;
color: white;
}
.form-actions button[type="submit"]:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.form-actions button[type="button"] {
background-color: #ddd;
}
.error {
color: #dc3545;
font-size: 14px;
}
.preview {
margin-top: 20px;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
}
.preview pre {
margin: 10px 0 0;
font-size: 12px;
}
</style>
使用驗證函式庫
對於複雜的表單驗證,建議使用專門的驗證函式庫,如:
這些函式庫提供了更完整的驗證功能,包括非同步驗證、複雜的驗證規則等。