闽公网安备 35020302035485号
在日常开发的过程中通常我们会遇到一个填写表单信息的弹窗,这个时候新手会无所畏惧的直接就将这个弹框写到页面中,稍微有点追求的会将这个弹框封装成一个组件,但是还是会觉得自己的组件封装的不够好,今天我就带大家一起来封装一个相对好用的弹窗表单组件。
<template>
<el-dialog v-model="visible">
</el-dialog>
</template>
<script setup>
import { ref } from 'vue'
const visible = ref(false)
const open = () => {
visible.value = true
}
const close = () => {
visible.value = false
}
defineExpose({
open,
close
})
</script>
这种代码大家一看就能看明白,我就不写注释了;<template>
<el-dialog v-model="visible">
</el-dialog>
</template>
<script setup>
import {ref, watch} from 'vue'
// 堆代码 duidaima.com
const props = defineProps({
visible: {
type: Boolean,
default: false
}
})
const dialogVisible = ref(false)
watch(
() => props.visible,
(val) => {
dialogVisible.value = val
}
)
</script>
这种方案相对于上面哪一种要稍微优雅一点,可以通过父组件传入的visible进行控制弹窗组件的开启和关闭。但是上面这两种方案其实都有很大的缺陷,就是element-ui的Dialog组件的控制开启和关闭是通过v-model来控制的。这样就会导致一个问题,在组件内部关闭了Dialog无法通知给父组件,于是我看到了很多小伙伴又对Dialog组件加上了close事件的监听,同时还为组件加上了close的事件。<template>
<el-dialog v-model="visible" @close="close">
</el-dialog>
</template>
<script setup>
import {ref, watch} from 'vue'
const props = defineProps({
visible: {
type: Boolean,
default: false
}
})
const dialogVisible = ref(false)
watch(
() => props.visible,
(val) => {
dialogVisible.value = val
}
)
const emits = defineEmits(['close'])
const close = () => {
emits('close')
}
</script>
<!-- 父组件 -->
<template>
<div>
<MyDialog :visible="visible" @close="close"/>
</div>
</template>
<script setup>
import MyDialog from './MyDialog.vue'
import { ref } from 'vue'
const visible = ref(false)
const close = () => {
visible.value = false
}
</script>
真的是非常令人头大的代码,但是由于对Vue的理解不够,自己也不知道应该怎么优化这个代码才好,所以我今天站出来了;<template>
<el-dialog v-model="visible">
</el-dialog>
</template>
<script setup>
import {computed} from 'vue'
const props = defineProps({
modelValue: {
type: Boolean,
default: false
}
})
const emits = defineEmits(['update:modelValue'])
const visible = computed({
get: () => props.modelValue,
set: (val) => {
emits('update:modelValue', val)
}
})
</script>
<!-- 父组件 -->
<template>
<div>
<MyDialog v-model="visible"/>
</div>
</template>
<script setup>
import MyDialog from './MyDialog.vue'
import { ref } from 'vue'
const visible = ref(false)
</script>
这一个基础知识点,v-model在Vue3中就是两个东西的组合:有了这两个东西,组件就可以使用v-model来进行双向绑定了,而且Vue3还可以支持多个v-model,只需要在组件中定义多个props和emits就可以了。emits中的事件名称必须是以update:开头的,非modelValue的属性的双向绑定就通过v-model:propName来进行绑定,这个下面介绍。
表单的封装那可是大头,不仅是大头,还叫人头大,因为表单是需要数据支持的,这个数据来源肯定是需要父组件传入。二父组件不管传入的是数据ID让组件去请求数据,还是直接传入数据,然后对数据进行一个深拷贝,再给组件的表单来使用。这些方法都感觉怪怪的,因为修改的数据还是无法及时的回馈给父组件,都是一种掩耳盗铃的做法。
<template>
<el-dialog
title="Title"
v-model="dialogVisible"
>
<el-form ref="form" :model="formData" :rules="rules">
<el-form-item label="姓名" prop="name">
<el-input v-model="formData.name" placeholder="请输入姓名"/>
</el-form-item>
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="formData.gender">
<el-radio :label="1">男</el-radio>
<el-radio :label="0">女</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="年龄" prop="age">
<el-input-number v-model="formData.age"/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit">提交</el-button>
</template>
</el-dialog>
</template>
<script setup>
import {computed, ref} from 'vue'
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
formData: {
type: Object,
default: () => ({})
}
})
const emits = defineEmits([
'update:modelValue',
'update:formData',
'cancel',
'submit'
])
const dialogVisible = computed({
get: () => props.modelValue,
set: (value) => emits('update:modelValue', value),
})
const rules = {
name: {required: true, message: '请输入名称', trigger: 'blur'}
}
const formData = computed(() => {
return new Proxy(props.formData, {
set(target, prop, newValue) {
emits('update:formData', {
...target,
[prop]: newValue
})
return true;
}
})
})
const handleCancel = () => {
dialogVisible.value = false;
emits('cancel')
}
const form = ref(null)
const handleSubmit = () => {
form.value.validate(valid => {
if (valid) {
emits('submit')
}
})
}
</script>
<!-- 父组件 -->
<template>
<div>
<el-button @click="openDialog">打开弹框</el-button>
<DialogForm
v-model="dialogVisible"
v-model:form-data="formData"
@submit="handleSubmit"
/>
</div>
</template>
<script setup>
import {ref} from 'vue'
import DialogForm from "./components/DialogForm.vue";
const dialogVisible = ref(false)
const openDialog = () => {
dialogVisible.value = true
}
const formData = ref({
name: '田八',
age: 18,
gender: 1
})
const handleSubmit = () => {
console.log(formData.value)
}
</script>
这里核心还是在于计算属性,我使用了两个v-model,第一个就是控制弹框的,就不多说了;const formData = computed(() => {
// 使用计算属性返回一个代理对象
return new Proxy(props.formData, {
// 代理对象只需要拦截 set 操作即可
set(target, prop, newValue) {
// 直接提交 自定义 双向绑定事件
emits('update:formData', {
...target,
// 通过展开运算符解构所有对象,然后通过 自定义 属性名覆盖修改之后的属性
[prop]: newValue
})
return true;
}
})
})
核心还是使用了computed属性,但是这次没有使用computed属性的set属性,而是使用了Proxy的set拦截器;
<template>
<el-dialog
title="Title"
v-bind="$attrs"
v-model="dialogVisible"
>
<el-form ref="form" :model="formData" :rules="rules">
<el-form-item label="姓名" prop="name">
<el-input v-model="formData.name" placeholder="请输入姓名"/>
</el-form-item>
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="formData.gender">
<el-radio :label="1">男</el-radio>
<el-radio :label="0">女</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="年龄" prop="age">
<el-input-number v-model="formData.age"/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit">提交</el-button>
</template>
</el-dialog>
</template>
<script setup>
import {ref} from 'vue'
import {
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElRadioGroup,
ElRadio,
ElInputNumber,
ElButton
} from 'element-plus'
const props = defineProps({
formData: {
type: Object,
default: () => ({})
}
})
const emits = defineEmits([
'cancel',
'submit'
])
const dialogVisible = ref(false)
const rules = {
name: {required: true, message: '请输入名称', trigger: 'blur'}
}
const formData = ref({
...props.formData
});
const handleCancel = () => {
dialogVisible.value = false;
emits('cancel')
}
const form = ref(null)
const handleSubmit = () => {
form.value.validate(valid => {
if (valid) {
emits('submit', formData.value)
}
})
}
</script>
首先可以看到的是组件的代码所有的状态现在都是内部维护了,因为通过指令打开的方式,参数都是传入的,无法响应式,只能在组件内部进行响应式。import { h, render } from 'vue'
import Dialog from './DialogForm.vue'
const divDom = document.createElement('div')
document.body.appendChild(divDom);
const dialog = (option) => {
return new Promise((resolve, reject) => {
const onSubmit = (data) => {
render(null, divDom)
resolve(data)
}
const onCancel = () => {
render(null, divDom)
reject(new Error('取消'))
}
const vNode = h(Dialog, {
...option,
modelValue: true,
onSubmit,
onCancel,
onClose: onCancel
})
render(vNode, divDom)
})
}
export default dialog
这里使用了Vue内部提供的两个函数,一个是h函数,一个是render函数,然后还需要定义一个挂载的容器。然后内部返回一个Promise,对应的用于控制组件的开启关闭的回调,具体的可以看代码中写的注释,这里就不多说了。<template>
<div>
<el-button type="primary" @click="openDialog">打开弹框</el-button>
</div>
</template>
<script setup>
import {ref} from 'vue'
import dialog from "./components/DialogForm.js";
const formData = ref({
name: '田八',
age: 18,
gender: 1
})
const openDialog = () => {
dialog({
formData: formData.value,
}).then((data) => {
console.log(data)
})
}
</script>
来看看效果: