在日常开发的过程中通常我们会遇到一个填写表单信息的弹窗,这个时候新手会无所畏惧的直接就将这个弹框写到页面中,稍微有点追求的会将这个弹框封装成一个组件,但是还是会觉得自己的组件封装的不够好,今天我就带大家一起来封装一个相对好用的弹窗表单组件。
<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>来看看效果: