摘要:1.2 自定义Input组件 方式一:使用defineModel宏(Vue3.4+推荐) &model.capitalize="inputValue" /> <p class="mt-2">处理后的值:{{ inputValue }}</p> </div> </template>
二、复合表单组件的封装(如带验证的输入框、日期选择器) 2.1 带验证的输入框
往期文章归档
Vue 3表单验证如何从基础规则到异步交互构建完整验证体系? 应用商店 | 免费好用的在线工具 文件隐写工具 - 应用商店 | 免费好用的在线工具 IPTV 频道探索器 - 应用商店 | 免费好用的在线工具 快传 - 应用商店 | 免费好用的在线工具 随机抽奖工具 - 应用商店 | 免费好用的在线工具 动漫场景查找器 - 应用商店 |
Vue3中组件的双向绑定本质是props与emit的语法糖。在Vue3.4+版本,官方推荐使用defineModel()宏简化实现,而低版本则需要手动处理属性与事件的传递。
props
emit
defineModel()
<!-- CustomInput.vue --> <script setup> // defineModel自动处理props和emit的双向绑定 const model = defineModel() </script> <template> <input v-model="model" placeholder="请输入内容" class="custom-input" /> </template> <style scoped> .custom-input { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; } </style>
父组件使用:
<!-- Parent.vue --> <script setup> import { ref } from 'vue' import CustomInput from './CustomInput.vue' const inputValue = ref('') </script> <template> <div> <CustomInput v-model="inputValue" /> <p class="mt-2">输入结果:{{ inputValue }}</p> </div> </template>
<!-- CustomInputLegacy.vue --> <script setup> // 接收父组件传递的value const props = defineProps(['modelValue']) // 定义更新事件 const emit = defineEmits(['update:modelValue']) </script> <template> <input :value="props.modelValue" @input="emit('update:modelValue', $event.target.value)" placeholder="请输入内容" class="custom-input" /> </template>
父组件使用方式与defineModel版本完全一致。
<!-- CustomSelect.vue --> <script setup> const model = defineModel() // 接收选项配置 const props = defineProps({ options: { type: Array, required: true, default: () => [] }, placeholder: { type: String, default: '请选择' } }) </script> <template> <select v-model="model" class="custom-select"> <option value="" disabled>{{ props.placeholder }}</option> <option v-for="option in props.options" :key="option.value" :value="option.value" > {{ option.label }} </option> </select> </template> <style scoped> .custom-select { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; background-color: white; } </style>
<!-- Parent.vue --> <script setup> import { ref } from 'vue' import CustomSelect from './CustomSelect.vue' const selectedValue = ref('') const selectOptions = [ { value: 'vue', label: 'Vue.js' }, { value: 'react', label: 'React' }, { value: 'angular', label: 'Angular' } ] </script> <template> <div> <CustomSelect v-model="selectedValue" :options="selectOptions" placeholder="选择前端框架" /> <p class="mt-2">选中值:{{ selectedValue }}</p> </div> </template>
Vue3支持在单个组件上绑定多个v-model,通过指定参数区分:
<!-- UserForm.vue --> <script setup> const firstName = defineModel('firstName') const lastName = defineModel('lastName') </script> <template> <div class="flex gap-2"> <input v-model="firstName" placeholder="姓" class="custom-input" /> <input v-model="lastName" placeholder="名" class="custom-input" /> </div> </template>
<!-- Parent.vue --> <script setup> import { ref } from 'vue' import UserForm from './UserForm.vue' const userFirstName = ref('') const userLastName = ref('') </script> <template> <div> <UserForm v-model:first-name="userFirstName" v-model:last-name="userLastName" /> <p class="mt-2">姓名:{{ userFirstName }} {{ userLastName }}</p> </div> </template>
自定义组件也可以支持v-model修饰符,比如实现首字母大写:
<!-- CustomInputWithModifier.vue --> <script setup> const [model, modifiers] = defineModel({ set(value) { // 处理capitalize修饰符 if (modifiers.capitalize && value) { return value.charAt(0).toUpperCase() + value.slice(1) } return value } }) </script> <template> <input v-model="model" placeholder="请输入内容" class="custom-input" /> </template>
<!-- Parent.vue --> <script setup> import { ref } from 'vue' import CustomInputWithModifier from './CustomInputWithModifier.vue' const inputValue = ref('') </script> <template> <div> <CustomInputWithModifier v-model.capitalize="inputValue" /> <p class="mt-2">处理后的值:{{ inputValue }}</p> </div> </template>
封装一个集成验证逻辑的输入框组件,支持多种验证规则:
<!-- ValidatedInput.vue --> <script setup> import { ref, computed } from 'vue' const model = defineModel() const props = defineProps({ rules: { type: Object, default: () => ({}) }, label: { type: String, default: '' } }) const showError = ref(false) const errorMessage = ref('') // 验证输入值 const validate = (value) => { showError.value = false errorMessage.value = '' // 必填验证 if (props.rules.required && !value) { showError.value = true errorMessage.value = props.rules.requiredMessage || '此字段为必填项' return false } // 最小长度验证 if (props.rules.minLength && value.length < props.rules.minLength) { showError.value = true errorMessage.value = props.rules.minLengthMessage || `最少需要输入${props.rules.minLength}个字符` return false } // 邮箱格式验证 if (props.rules.email && value) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ if (!emailRegex.test(value)) { showError.value = true errorMessage.value = props.rules.emailMessage || '请输入有效的邮箱地址' return false } } return true } // 失去焦点时触发验证 const handleBlur = () => { validate(model.value) } // 输入时清除错误提示 const handleInput = () => { showError.value = false errorMessage.value = '' } </script> <template> <div class="validated-input"> <label v-if="props.label" class="input-label">{{ props.label }}</label> <input v-model="model" @blur="handleBlur" @input="handleInput" :class="{ 'input-error': showError }" class="custom-input" :placeholder="props.label || '请输入内容'" /> <div v-if="showError" class="error-message">{{ errorMessage }}</div> </div> </template> <style scoped> .validated-input { margin-bottom: 16px; } .input-label { display: block; margin-bottom: 4px; font-size: 14px; font-weight: 500; } .input-error { border-color: #ff4d4f; } .error-message { margin-top: 4px; font-size: 12px; color: #ff4d4f; } </style>
<!-- Parent.vue --> <script setup> import { ref } from 'vue' import ValidatedInput from './ValidatedInput.vue' const email = ref('') const emailRules = { required: true, requiredMessage: '邮箱不能为空', email: true, emailMessage: '请输入有效的邮箱地址' } </script> <template> <ValidatedInput v-model="email" label="邮箱地址" :rules="emailRules" /> </template>
封装一个支持格式化和范围选择的日期选择器:
<!-- DatePicker.vue --> <script setup> import { ref, computed } from 'vue' const model = defineModel() const props = defineProps({ format: { type: String, default: 'YYYY-MM-DD' }, placeholder: { type: String, default: '选择日期' } }) // 格式化显示的日期 const formattedDate = computed(() => { if (!model.value) return '' const date = new Date(model.value) const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') return `${year}-${month}-${day}` }) // 处理日期变化 const handleDateChange = (e) => { model.value = e.target.value } </script> <template> <div class="date-picker"> <input type="date" :value="formattedDate" @change="handleDateChange" :placeholder="props.placeholder" class="custom-input" /> <p v-if="model.value" class="mt-2">选中日期:{{ formattedDate }}</p> </div> </template>
<!-- Parent.vue --> <script setup> import { ref } from 'vue' import DatePicker from './DatePicker.vue' const selectedDate = ref('') </script> <template> <DatePicker v-model="selectedDate" /> </template>
placeholder
disabled
size
通过插槽增强组件的扩展性:
<!-- CustomInputWithSlot.vue --> <script setup> const model = defineModel() </script> <template> <div class="input-group"> <slot name="prefix"></slot> <input v-model="model" class="custom-input" /> <slot name="suffix"></slot> </div> </template>
父组件使用插槽:
<CustomInputWithSlot v-model="value"> <template #prefix> <span class="prefix-icon">📧</span> </template> <template #suffix> <button @click="clearInput">清除</button> </template> </CustomInputWithSlot>
:root { --input-border-color: #ddd; --input-focus-color: #409eff; --input-error-color: #ff4d4f; }
class
v-bind="$attrs"
validate-success
validate-fail
kebab-case
update:model-value
provide
inject
答案解析:
<script setup> const model = defineModel() </script> <template> <input v-model="model" /> </template>
<script setup> const props = defineProps(['modelValue']) const emit = defineEmits(['update:modelValue']) </script> <template> <input :value="props.modelValue" @input="emit('update:modelValue', $event.target.value)" /> </template>
父组件统一使用v-model="value"绑定。
v-model="value"
答案解析: 通过为defineModel()指定参数实现多v-model绑定:
<!-- 子组件 --> <script setup> const firstName = defineModel('firstName') const lastName = defineModel('lastName') </script> <template> <input v-model="firstName" placeholder="姓" /> <input v-model="lastName" placeholder="名" /> </template>
<CustomComponent v-model:first-name="userFirstName" v-model:last-name="userLastName" />
产生原因:自定义组件使用了v-model,但父组件未绑定值,或子组件未正确定义props。 解决办法:
modelValue
产生原因:v-model绑定的变量类型与子组件期望的prop类型不匹配。 解决办法:
.number
产生原因:子组件未声明update:modelValue事件,或使用了片段根节点导致事件无法自动继承。 解决办法:
update:modelValue
defineEmits(['update:modelValue'])
二、复合表单组件的封装(如带验证的输入框、日期选择器) 2.1 带验证的输入框
往期文章归档
Vue 3表单验证如何从基础规则到异步交互构建完整验证体系? 应用商店 | 免费好用的在线工具 文件隐写工具 - 应用商店 | 免费好用的在线工具 IPTV 频道探索器 - 应用商店 | 免费好用的在线工具 快传 - 应用商店 | 免费好用的在线工具 随机抽奖工具 - 应用商店 | 免费好用的在线工具 动漫场景查找器 - 应用商店 |
一、自定义input/select等基础表单组件(v-model配合props/emit)
1.1 双向绑定的核心原理
Vue3中组件的双向绑定本质是
props与emit的语法糖。在Vue3.4+版本,官方推荐使用defineModel()宏简化实现,而低版本则需要手动处理属性与事件的传递。1.2 自定义Input组件
方式一:使用defineModel宏(Vue3.4+推荐)
父组件使用:
方式二:手动处理props与emit(兼容低版本)
父组件使用方式与defineModel版本完全一致。
1.3 自定义Select组件
父组件使用:
1.4 多v-model绑定
Vue3支持在单个组件上绑定多个v-model,通过指定参数区分:
父组件使用:
1.5 处理v-model修饰符
自定义组件也可以支持v-model修饰符,比如实现首字母大写:
父组件使用:
二、复合表单组件的封装(如带验证的输入框、日期选择器)
2.1 带验证的输入框
封装一个集成验证逻辑的输入框组件,支持多种验证规则:
父组件使用:
2.2 日期选择器组件
封装一个支持格式化和范围选择的日期选择器:
父组件使用:
三、表单组件库的设计思路(扩展性与通用性)
3.1 可配置化设计原则
placeholder、disabled、size等3.2 插槽的灵活运用
通过插槽增强组件的扩展性:
父组件使用插槽:
3.3 样式定制方案
:root { --input-border-color: #ddd; --input-focus-color: #409eff; --input-error-color: #ff4d4f; }classprops传递自定义样式类3.4 事件系统设计
v-bind="$attrs"透传原生事件validate-success、validate-failkebab-case命名,如update:model-value3.5 组件组合策略
provide和inject实现跨组件通信,如表单验证状态的共享课后Quiz
问题1:如何在Vue3中实现组件的双向绑定?请分别写出Vue3.4+和低版本的实现方式。
答案解析:
defineModel()宏:父组件统一使用
v-model="value"绑定。问题2:如何让自定义组件支持多个v-model绑定?请给出示例代码。
答案解析: 通过为
defineModel()指定参数实现多v-model绑定:父组件使用:
问题3:在设计表单组件库时,如何保证组件的扩展性和通用性?
答案解析:
常见报错解决方案
报错1:[Vue warn]: Missing required prop: "modelValue"
产生原因:自定义组件使用了v-model,但父组件未绑定值,或子组件未正确定义props。 解决办法:
v-model="value"绑定响应式变量defineModel()或声明modelValueprop报错2:[Vue warn]: Invalid prop: type check failed for prop "modelValue". Expected String, got Number
产生原因:v-model绑定的变量类型与子组件期望的prop类型不匹配。 解决办法:
.number修饰符或在defineModel()中指定类型报错3:[Vue warn]: Extraneous non-emits event listeners (update:modelValue) were passed to component
产生原因:子组件未声明
update:modelValue事件,或使用了片段根节点导致事件无法自动继承。 解决办法:defineModel()宏自动处理事件声明defineEmits(['update:modelValue'])声明事件参考链接