大家好!今天我们来聊一聊如何封装 Vue 中的表单控件,并以 Element Plus 为例,探讨其表单组件的实现方式。相信使用过 Vue 的开发者对 Element Plus 并不陌生,它封装了许多优雅且易用的表单控件,例如 Input、Checkbox、Checkbox-Group、Radio、Radio-Group 等。这些组件不仅功能强大,还能极大地提升开发效率。那么,这些控件是如何封装出来的呢?接下来,我们将从基础到进阶,逐步实现这些组件。
封装UIInput
<template>
<div class="input-wrap">
<input v-model="model"/>
</div>
</template>
<script setup>
const model = defineModel()
</script>
<style lang="scss" scoped>
.input-wrap {
box-shadow: 0 0 0 1px #dcdfe6 inset;
border-radius: 4px;
padding: 1px 11px;
input {
border: none;
outline: none;
color: #606266;
width: 100%;
box-sizing: border-box;
line-height: 28px;
}
}
</style>
使用UiInput
<template>
<div>
姓名: <UiInput v-model="name" />
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import UiInput from '@/components/UiInput/index.vue'
const name = ref('')
watch(name, (newVal) => {
console.log(newVal)
})
</script>
<style scoped>
</style>
在这个例子中:
- 在
UiInput组件中我们主要使用了v-model和defineModel两个关键知识点。v-model是vue提供的专门用来做数据双向绑定的。defineModel 是V3.4.0 以后新增的一个语法糖 - 在使用的时候,我在
UiInput上使用v-model绑定了一个name变量。这个变量是响应式的,因为他使用ref声明。 - 我们使用
watch监听name的变化,变化之后输出新的值。 这时候我在输入框中输入值就能在控制台中看到变化的值。
运行结果:
上面提到defineModel 是3.4.0 以后才有的,如果使用的版本是3.4.0以前的版本改怎么办呢?
3.4.0 版本以前的UiInput 封装。
<template>
<div class="input-wrap">
<input @input="emitValue" />
</div>
</template>
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const emitValue = ($event) => {
emit('update:modelValue', $event.target.value)
}
</script>
<style lang="scss" scoped>
.input-wrap {
box-shadow: 0 0 0 1px #dcdfe6 inset;
border-radius: 4px;
padding: 1px 11px;
input {
border: none;
outline: none;
color: #606266;
width: 100%;
box-sizing: border-box;
line-height: 28px;
}
}
</style>
可以看到在3.4.0 以前的版本中我们封装UiInput 和3.4.0 以后的区别,主要在于:
- 将
defineModel换成了defineProps中得出来的modelValue和defineEmits中得出来的update:modelValue - 将
input上的v-model换成了@input事件
来看看效果:
UiCheckbox 封装
<template>
<label class="check-wrap">
<input type="checkbox" v-model="model" />
<span class="check-span iconfont"
:class="{
'icon-checkbox-on': model,
'icon-checkbox': !model
}"
>{{ label }}</span>
</label>
</template>
<script setup>
const props = defineProps({
label: {
type: String,
default: ''
}
})
const model = defineModel()
</script>
<style lang="scss" scoped>
.check-wrap {
position: relative;
cursor: pointer;
input {
width: 0;
height: 0;
opacity: 0;
position: absolute
}
.check-span {
color: #606266;
&::before{
margin-right: 6px;
}
&.icon-checkbox-on {
color: #409eff;
}
}
}
</style>
使用UiCheckbox
<template>
<div class="form-wrap">
<div class="form-item">
<UiCheckbox label="是否记住密码" v-model="isRememberPwd" />
</div>
<p>{{ isRememberPwd }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import UiCheckbox from '@/components/UiCheckbox/index.vue'
const isRememberPwd = ref(false)
</script>
<style lang="scss" scoped>
.form-wrap {
width: 500px;
.form-item {
display: flex;
align-items: center;
}
}
</style>
在这个例子中:
- 我们使用
props传值的方式来传递label的值,用于多选框显示的文案内容 - 我们使用了
label标签包裹了type为checkbox的input标签和一个span标签。选中的时候无论是点击span和input都能切换选中状态。 - 我们将
input标签的样式宽度设置为0,透明度设置为0 , 因为我们不需要checkbox默认的样式,我只需要他的值。我们使用v-model绑定了他的值。 - 我们通过
model的值来判断显示选中状态还是非选中状态。这里使用了iconfonts 阿里字体图标字体图标库。如果不会使用iconfonts 的可以百度搜索下,跟着操作即可,很简单的。 - 使用时我们直接在UiChecbox上使用
v-model绑定值。
来看看运行效果
非选中状态:
选中状态:
UiCheckGroup 封装
<template>
<div class="check-group">
<slot></slot>
</div>
</template>
<script setup>
import { provide, ref } from 'vue'
const emit = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: {
type: Array,
default: () => []
}
})
const selectdValue = ref([...props.modelValue])
provide('checkGroup', {
selectdValue,
toggleOption (value) {
const index = selectdValue.value.indexOf(value)
if (index !== -1){
selectdValue.value.splice(index, 1)
} else {
selectdValue.value.push(value)
}
emit('update:modelValue', selectdValue.value)
}
})
</script>
<style lang="scss" scoped>
.check-group {
:deep(.check-wrap) {
margin-right: 20px;
}
}
</style>
修改UiCheckBox 适应UiCheckboxGroup
<template>
<label class="check-wrap">
<input type="checkbox" v-model="model" :true-value="checkGroup ? label : true" />
<span class="check-span iconfont"
:class="{
'icon-checkbox-on': model,
'icon-checkbox': !model
}"
>{{ label }}</span>
</label>
</template>
<script setup>
import { inject, watch } from 'vue';
const props = defineProps({
label: {
type: String,
default: ''
}
})
const model = defineModel()
const checkGroup = inject('checkGroup')
watch(model, () => {
if (checkGroup) {
checkGroup.toggleOption(props.label)
}
})
</script>
<style lang="scss" scoped>
.check-wrap {
position: relative;
cursor: pointer;
input {
width: 0;
height: 0;
opacity: 0;
position: absolute
}
.check-span {
color: #606266;
&::before{
margin-right: 6px;
}
&.icon-checkbox-on {
color: #409eff;
}
}
}
</style>
使用UiCheckboxGroup
<template>
<div class="form-wrap">
<div class="form-item">
<UiCheckbox label="是否同意隐私政策" v-model="isArgee" /> {{ isArgee }}
</div>
<div class="form-item">
爱好:
<UiCheckboxGroup v-model="hobby">
<UiCheckbox label="打乒乓球" />
<UiCheckbox label="打篮球" />
<UiCheckbox label="跑步" />
</UiCheckboxGroup>
</div>
<p>{{ hobby }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
import UiCheckbox from '@/components/UiCheckbox/index.vue'
import UiCheckboxGroup from '@/components/UiCheckboxGroup/index.vue'
const hobby = ref([])
const isArgee = ref(false)
</script>
<style lang="scss" scoped>
.form-wrap {
width: 500px;
.form-item {
display: flex;
align-items: center;
margin-top: 10px;
}
}
</style>
在这个例子中:
- 我们
UiCheckGroup使用slot占位,使用时将单个UiCheckbox传过来 - 使用
provide定义了一个checkGroup对象,对象里面主要包含了,选中的数据和改变选中数据的方法,提提供给单个的UiCheckbox组件使用 - 修改
UiCheckbox使得适应UiCheckboxGroup, 使用inject接收UiCheckboxGroup传过来的checkGroup UiCheckbox true-value属性用来定义选中的值,因为一般一组选中我们都需要一个具体值,而不是只需要false和true, 这里做了个判断,只有checkGrou存在时才使用label的值,不存在时还是使用true组件作为选中的值。- 监听选中状态的变化,判断如果
checkGroup存在时,调用checkGroup.toggleOption方法更改选中的数组 - 使用时我们直接将在
UiCheckbox上用v-model绑定选中的数据,在UiCheckboxGroup之间添加单个的UiCheckbox
查看效果 未选中状态:
选中状态:
可以看到我们的单个选择,和一组选择都能正常使用。
UiRadio 封装
<template>
<label class="radio-wrap">
<input type="radio" v-model="model" :value="label" />
<span class="radio-span iconfont"
:class="{
'icon-radio-on': model,
'icon-radio': !model
}"
><slot></slot></span>
</label>
</template>
<script setup>
const props = defineProps({
label: {
type: String,
default: ''
}
})
const model = defineModel()
</script>
<style lang="scss" scoped>
.radio-wrap {
position: relative;
cursor: pointer;
input {
width: 0;
height: 0;
opacity: 0;
position: absolute
}
.radio-span {
color: #606266;
&::before{
margin-right: 6px;
}
&.icon-radio-on {
color: #409eff;
}
}
}
</style>
UiRadio 的使用
<template>
<div class="form-wrap">
<div class="form-item">
<UiRadio label="1" v-model="isArgee">是否同意注册</UiRadio> {{isArgee}}
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import UiRadio from '@/components/UiRadio/index.vue'
const isArgee = ref('')
</script>
<style lang="scss" scoped>
.form-wrap {
width: 500px;
.form-item {
display: flex;
align-items: center;
margin-top: 10px;
}
}
</style>
在这个例子中:
- 我们使用
slot占位符定义单选框文本值显示位置 :value定义选中的值显示- 其他流程和
UiCheckbox封装流程差不多,不再做多的解释
UiRadioGroup 封装
<template>
<div class="radio-group">
<slot></slot>
</div>
</template>
<script setup>
import { provide, ref } from 'vue'
const emit = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: {
type: String,
default: ''
}
})
const selectdValue = ref(props.modelValue)
provide('radioGroup', {
selectdValue,
toggleOption (value) {
selectdValue.value = value
emit('update:modelValue', selectdValue.value)
}
})
</script>
<style lang="scss" scoped>
.radio-group {
:deep(.radio-wrap) {
margin-right: 20px;
}
}
</style>
修改UiRadio 适应UiRadioGroup
<template>
<label class="radio-wrap">
<input type="radio" v-model="model" :value="label" />
<span class="radio-span iconfont"
:class="{
'icon-radio-on': isCheck,
'icon-radio': !isCheck
}"
><slot></slot></span>
</label>
</template>
<script setup>
import { watch, inject, computed } from 'vue'
const props = defineProps({
label: {
type: String,
default: ''
}
})
const model = defineModel()
const radioGroup = inject('radioGroup')
if (model.value === undefined) {
model.value = radioGroup.selectdValue
}
const isCheck = computed(() => radioGroup ? radioGroup.selectdValue.value === props.label : model.value)
watch(model, () => {
if (radioGroup) {
radioGroup.toggleOption(props.label)
}
})
</script>
<style lang="scss" scoped>
.radio-wrap {
position: relative;
cursor: pointer;
input {
width: 0;
height: 0;
opacity: 0;
position: absolute
}
.radio-span {
color: #606266;
&::before{
margin-right: 6px;
}
&.icon-radio-on {
color: #409eff;
}
}
}
</style>
使用UiRadioGroup
<template>
<div class="form-wrap">
<div class="form-item">
<UiRadio label="1" v-model="isArgee">是否同意注册</UiRadio> {{ isArgee }}
</div>
<div class="form-item">
性别:
<UiRadioGroup v-model="gender">
<UiRadio label="1">男</UiRadio>
<UiRadio label="2">女</UiRadio>
<UiRadio label="0">未知</UiRadio>
</UiRadioGroup>
{{ gender }}
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import UiRadio from '@/components/UiRadio/index.vue'
import UiRadioGroup from '@/components/UiRadioGroup/index.vue'
const isArgee = ref('')
const gender = ref('0')
</script>
<style lang="scss" scoped>
.form-wrap {
width: 500px;
.form-item {
display: flex;
align-items: center;
margin-top: 10px;
}
}
</style>
UiRadioGroup 封装流程和UiCheckboxGroup 封装流程差不多,不做过多解释。不同的地方主要在于,单选框只能选择一个,所以选中的值用不再使用数组,而是用一个String型的值。
查看运行效果:
总结
通过以上实现,我们完成了 UiInput、UiCheckbox、UiCheckboxGroup、UiRadio 和 UiRadioGroup 的封装。这些组件的设计思路与 Element Plus 类似,遵循了 Vue 的组件化开发原则,充分利用了 v-model、provide/inject 等特性,使得组件的使用更加直观和便捷。
希望这篇文章能帮助大家更好地理解表单控件的封装逻辑,并在实际开发中灵活运用这些技巧!