vue使用h函数封装dialog组件(以命令的形式使用dialog组件)
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">场景</a></li><li><a href="#_label1">命令式弹窗</a></li><li><a href="#_label2">为啥弹窗中的表单不能够正常展示呢?</a></li><li><a href="#_label3">给新创建的app应用注册childTest组件使用到的东西</a></li><li><a href="#_label4">关于使用createApp创建新的应用实例</a></li><li><a href="#_label5">弹窗底部新增取消和确认按钮</a></li><li><a href="#_label6">点击关闭弹窗时,需要移除之前创建的div</a></li><li><a href="#_label7">关闭弹窗正确销毁相关组件</a></li><li><a href="#_label8">点击确认按钮时验证规则</a></li><li><a href="#_label9">如何把表单中的数据暴露出去</a></li><li><a href="#_label10">点击确定时,业务完成后关闭弹窗</a></li><li><a href="#_label11">优化业务组件</a></li><li><a href="#_label12">最终的代码</a></li></ul></div><p class="maodian"><a name="_label0"></a></p><h2>场景</h2><p>有些时候我们的页面是有很多的弹窗<br />如果我们把这些弹窗都写html中会有一大坨<br />因此:我们需要把弹窗封装成命令式的形式</p>
<p class="maodian"><a name="_label1"></a></p><h2>命令式弹窗</h2>
<div class="jb51code"><pre class="brush:js;">// 使用弹窗的组件
<template>
<div>
<el-button @click="openMask">点击弹窗</el-button>
</div>
</template>
<script setup lang="ts">
import childTest from '@/components/childTest.vue'
import { renderDialog } from '@/hooks/dialog'
function openMask(){
// 第1个参数:表示的是组件,你写弹窗中的组件
// 第2个参数:表示的组件属性,比如:确认按钮的名称等
// 第3个参数:表示的模态框的属性。比如:模态宽的宽度,标题名称,是否可移动
renderDialog(childTest,{},{title:'测试弹窗'})
}
</script></pre></div>
<div class="jb51code"><pre class="brush:js;">// 封装的弹窗
import { createApp, h } from "vue";
import { ElDialog } from "element-plus";
export function renderDialog(component:any,props:any, modalProps:any){
const dialog= h(
ElDialog, // 模态框组件
{
...modalProps, // 模态框属性
modelValue:true, // 模态框是否显示
}, // 因为是模态框组件,肯定是模态框的属性
{
default:()=>h(component, props ) // 插槽,el-dialog下的内容
}
)
console.log(dialog)
// 创建一个新的 Vue 应用实例。这个应用实例是独立的,与主应用分离。
const app = createApp(dialog)
const div = document.createElement('div')
document.body.appendChild(div)
app.mount(div)
}</pre></div>
<div class="jb51code"><pre class="brush:js;">//childTest.vue 组件
<template>
<div>
<span>It's a modal Dialog</span>
<el-form :model="form" label-width="auto" style="max-width: 600px">
<el-form-item label="Activity name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="Activity zone">
<el-select v-model="form.region" placeholder="please select your zone">
<el-option label="Zone one" value="shanghai" />
<el-option label="Zone two" value="beijing" />
</el-select>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref,reactive } from 'vue'
const dialogVisible = ref(true)
const form = reactive({
name: '',
region: '',
})
const onSubmit = () => {
console.log('submit!')
}
</script>
</pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011309184761.png" /></p>
<p class="maodian"><a name="_label2"></a></p><h2>为啥弹窗中的表单不能够正常展示呢?</h2>
<p>在控制台会有下面的提示信息:<br />Failed to resolve component:<br />el-form If this is a native custom element,<br />make sure to exclude it from component resolution via compilerOptions.isCustomElement<br />翻译过来就是<br />无法解析组件:el-form如果这是一个原生自定义元素,<br />请确保通过 compilerOptions.isCustomElement 将其从组件解析中排除</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011309184720.jpg" /></p>
<p>其实就是说:我重新创建了一个新的app,这个app中没有注册组件。<br />因此会警告,页面渲染不出来。</p>
<div class="jb51code"><pre class="brush:js;">// 我重新创建了一个app,这个app中没有注册 element-plus 组件。
const app = createApp(dialog)
</pre></div>
<p>现在我们重新注册element-plus组件。<br />准确的说:我们要注册 childTest.vue 组件使用到的东西</p>
<p class="maodian"><a name="_label3"></a></p><h2>给新创建的app应用注册childTest组件使用到的东西</h2>
<p>我们将会在这个命令式弹窗中重新注册需要使用到的组件</p>
<div class="jb51code"><pre class="brush:js;">// 封装的弹窗
import { createApp, h } from "vue";
import { ElDialog } from "element-plus";
// 引入组件和样式
import ElementPlus from "element-plus";
// import "element-plus/dist/index.css";
export function renderDialog(component:any,props:any, modalProps:any){
const dialog= h(
ElDialog, // 模态框组件
{
...modalProps, // 模态框属性
modelValue:true, // 模态框显示
}, // 因为是模态框组件,肯定是模态框的属性
{
default:()=>h(component, props ) // 插槽,el-dialog下的内容
}
)
console.log(dialog)
// 创建一个新的 Vue 应用实例。这个应用实例是独立的,与主应用分离。
const app = createApp(dialog)
// 在新实例中注册 Element Plus, 这弹窗中的组件就可以正常显示了
app.use(ElementPlus);
const div = document.createElement('div')
document.body.appendChild(div)
app.mount(div)
}</pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011309184731.png" /></p>
<p>现在我们发现可以正常展示弹窗中的表单了。因为我们注册了element-plus组件。<br />但是我们发现又发现了另外一个问题。<br />弹窗底部没有取消和确认按钮。<br />需要我们再次通过h函数来创建</p>
<p class="maodian"><a name="_label4"></a></p><h2>关于使用createApp创建新的应用实例</h2>
<p>在Vue 3中,我们可以使用 createApp 来创建新的应用实例<br />但是这样会创建一个完全独立的应用<br />它不会共享主应用的组件、插件等。<br />因此我们需要重新注册</p>
<p class="maodian"><a name="_label5"></a></p><h2>弹窗底部新增取消和确认按钮</h2>
<p>我们将会使用h函数中的插槽来创建底部的取消按钮</p>
<div class="jb51code"><pre class="brush:js;">// 封装的弹窗
import { createApp, h } from "vue";
import { ElDialog, ElButton, ElForm, ElFormItem, ElInput, ElSelect, ElOption } from "element-plus";
import ElementPlus from "element-plus";
export function renderDialog(component: any, props: any, modalProps: any) {
// 创建弹窗实例
const dialog = h(
ElDialog,
{
...modalProps,
modelValue: true,
},
{
// 主要内容插槽
default: () => h(component, props),
// 底部插槽
footer:() =>h(
'div',
{ class: 'dialog-footer' },
[
h(
ElButton,
{
onClick: () => {
console.log('取消')
}
},
() => '取消'
),
h(
ElButton,
{
type: 'primary',
onClick: () => {
console.log('确定')
}
},
() => '确定'
)
]
)
}
);
// 创建一个新的 Vue 应用实例。这个应用实例是独立的,与主应用分离。
const app = createApp(dialog)
// 在新实例中注册 Element Plus, 这弹窗中的组件就可以正常显示了
app.use(ElementPlus);
const div = document.createElement('div')
document.body.appendChild(div)
app.mount(div)
}</pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011309184852.png" /></p>
<p class="maodian"><a name="_label6"></a></p><h2>点击关闭弹窗时,需要移除之前创建的div</h2>
<p>卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div。<br />2个地方需要移除:1,点击确认按钮。 2,点击其他地方的关闭</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011309184868.png" /></p>
<p class="maodian"><a name="_label7"></a></p><h2>关闭弹窗正确销毁相关组件</h2>
<div class="jb51code"><pre class="brush:js;">// 封装的弹窗
import { createApp, h } from "vue";
import { ElDialog, ElButton, ElForm, ElFormItem, ElInput, ElSelect, ElOption } from "element-plus";
import ElementPlus from "element-plus";
export function renderDialog(component: any, props: any, modalProps: any) {
console.log('111')
// 创建弹窗实例
const dialog = h(
ElDialog,
{
...modalProps,
modelValue: true,
onClose: ()=> {
console.log('关闭的回调')
app.unmount() // 这样卸载会让动画消失
// 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
document.body.removeChild(div)
}
},
{
// 主要内容插槽
default: () => h(component, props),
// 底部插槽
footer:() =>h(
'div',
{
class: 'dialog-footer',
},
[
h(
ElButton,
{
onClick: () => {
console.log('点击取消按钮')
// 卸载一个已挂载的应用实例。卸载一个应用会触发该应用组件树内所有组件的卸载生命周期钩子。
app.unmount() // 这样卸载会让动画消失
// 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
document.body.removeChild(div)
}
},
() => '取消'
),
h(
ElButton,
{
type: 'primary',
onClick: () => {
console.log('确定')
}
},
() => '确定'
)
]
)
}
);
// 创建一个新的 Vue 应用实例。这个应用实例是独立的,与主应用分离。
const app = createApp(dialog)
// 在新实例中注册 Element Plus, 这弹窗中的组件就可以正常显示了
app.use(ElementPlus);
// 这个div元素在在销毁应用时需要被移除哈
const div = document.createElement('div')
document.body.appendChild(div)
app.mount(div)
}</pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011309184846.png" /></p>
<p class="maodian"><a name="_label8"></a></p><h2>点击确认按钮时验证规则</h2>
<p>有些时候,我们弹窗中的表单是需要进行规则校验的。<br />我们下面来实现这个功能点<br />传递的组件</p>
<div class="jb51code"><pre class="brush:js;"><template>
<el-form
ref="ruleFormRef"
style="max-width: 600px"
:model="ruleForm"
:rules="rules"
label-width="auto"
>
<el-form-item label="Activity name" prop="name">
<el-input v-model="ruleForm.name" />
</el-form-item>
<el-form-item label="Activity zone" prop="region">
<el-select v-model="ruleForm.region" placeholder="Activity zone">
<el-option label="Zone one" value="shanghai" />
<el-option label="Zone two" value="beijing" />
</el-select>
</el-form-item>
<el-form-item label="Activity time" required>
<el-col :span="11">
<el-form-item prop="date1">
<el-date-picker
v-model="ruleForm.date1"
type="date"
aria-label="Pick a date"
placeholder="Pick a date"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col class="text-center" :span="2">
<span class="text-gray-500">-</span>
</el-col>
<el-col :span="11">
<el-form-item prop="date2">
<el-time-picker
v-model="ruleForm.date2"
aria-label="Pick a time"
placeholder="Pick a time"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-form-item>
<el-form-item label="Resources" prop="resource">
<el-radio-group v-model="ruleForm.resource">
<el-radio value="Sponsorship">Sponsorship</el-radio>
<el-radio value="Venue">Venue</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="Activity form" prop="desc">
<el-input v-model="ruleForm.desc" type="textarea" />
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
interface RuleForm {
name: string
region: string
date1: string
date2: string
resource: string
desc: string
}
const ruleFormRef = ref<FormInstance>()
const ruleForm = reactive<RuleForm>({
name: 'Hello',
region: '',
date1: '',
date2: '',
resource: '',
desc: '',
})
const rules = reactive<FormRules<RuleForm>>({
name: [
{ required: true, message: 'Please input Activity name', trigger: 'blur' },
{ min: 3, max: 5, message: 'Length should be 3 to 5', trigger: 'blur' },
],
region: [
{
required: true,
message: 'Please select Activity zone',
trigger: 'change',
},
],
date1: [
{
type: 'date',
required: true,
message: 'Please pick a date',
trigger: 'change',
},
],
date2: [
{
type: 'date',
required: true,
message: 'Please pick a time',
trigger: 'change',
},
],
resource: [
{
required: true,
message: 'Please select activity resource',
trigger: 'change',
},
],
desc: [
{ required: true, message: 'Please input activity form', trigger: 'blur' },
],
})
const submitForm = async () => {
if (!ruleFormRef.value) {
console.error('ruleFormRef is not initialized')
return false
}
try {
const valid = await ruleFormRef.value.validate()
if (valid) {
console.log('表单校验通过', ruleForm)
return Promise.resolve(ruleForm)
}
} catch (error) {
// 为啥submitForm中,valid的值是false会执行catch ?
// el-form 组件的 validate 方法的工作机制导致的。 validate 方法在表单验证失败时会抛出异常
console.error('err', error)
return false
/**
* 下面这样写为啥界面会报错呢?
* return Promise.reject(error)
* 当表单验证失败时,ruleFormRef.value.validate() 会抛出一个异常。
* 虽然你用了 try...catch 捕获这个异常,并且在 catch 块中通过 return Promise.reject(error) 返回了一个被拒绝的 Promise
* 但如果调用 submitForm 的地方没有正确地处理这个被拒绝的 Promise(即没有使用 .catch() 或者 await 来接收错误),
* 那么浏览器控制台就会显示一个 "Uncaught (in promise)" 错误。
* 在 catch 中再次 return Promise.reject(error) 是多余的, 直接return false
* */
/**
* 如果你这样写
* throw error 直接抛出错误即可
* 那么就需要再调用submitForm的地方捕获异常
* */
}
}
defineExpose({
submitForm:submitForm
})
</script></pre></div>
<div class="jb51code"><pre class="brush:js;">// 封装的弹窗
import { createApp, h, ref } from "vue";
import { ElDialog, ElButton, ElForm, ElFormItem, ElInput, ElSelect, ElOption } from "element-plus";
import ElementPlus from "element-plus";
export function renderDialog(component: any, props: any, modalProps: any) {
const instanceElement = ref()
console.log('111', instanceElement)
// 创建弹窗实例
const dialog = h(
ElDialog,
{
...modalProps,
modelValue: true,
onClose: ()=> {
console.log('关闭的回调')
app.unmount() // 这样卸载会让动画消失
// 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
document.body.removeChild(div)
}
},
{
// 主要内容插槽,这里的ref必须接收一个ref
default: () => h(component, {...props, ref: instanceElement}),
// 底部插槽
footer:() =>h(
'div',
{
class: 'dialog-footer',
},
[
h(
ElButton,
{
onClick: () => {
console.log('点击取消按钮')
// 卸载一个已挂载的应用实例。卸载一个应用会触发该应用组件树内所有组件的卸载生命周期钩子。
app.unmount() // 这样卸载会让动画消失
// 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
document.body.removeChild(div)
}
},
() => '取消'
),
h(
ElButton,
{
type: 'primary',
onClick: () => {
instanceElement?.value?.submitForm().then((res:any) =>{
console.log('得到的值',res)
})
console.log('确定')
}
},
() => '确定'
)
]
)
}
);
// 创建一个新的 Vue 应用实例。这个应用实例是独立的,与主应用分离。
const app = createApp(dialog)
// 在新实例中注册 Element Plus, 这弹窗中的组件就可以正常显示了
app.use(ElementPlus);
// 这个div元素在在销毁应用时需要被移除哈
const div = document.createElement('div')
document.body.appendChild(div)
app.mount(div)
}</pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011309184828.png" /></p>
<p>关键的点:通过ref拿到childTest组件中的方法,childTest要暴露需要的方法</p>
<p class="maodian"><a name="_label9"></a></p><h2>如何把表单中的数据暴露出去</h2>
<p>可以通过回调函数的方式把数据暴露出去哈。</p>
<div class="jb51code"><pre class="brush:js;">// 封装的弹窗
import { createApp, h, ref } from "vue";
import { ElDialog, ElButton, ElForm, ElFormItem, ElInput, ElSelect, ElOption } from "element-plus";
import ElementPlus from "element-plus";
export function renderDialog(component: any, props: any, modalProps: any, onConfirm: (data: any) => any ) {
// 第4个参数是回调函数
const instanceElement = ref()
console.log('111', instanceElement)
// 创建弹窗实例
const dialog = h(
ElDialog,
{
...modalProps,
modelValue: true,
onClose: ()=> {
console.log('关闭的回调')
app.unmount() // 这样卸载会让动画消失
// 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
document.body.removeChild(div)
}
},
{
// 主要内容插槽,这里的ref必须接收一个ref
default: () => h(component, {...props, ref: instanceElement}),
// 底部插槽
footer:() =>h(
'div',
{
class: 'dialog-footer',
},
[
h(
ElButton,
{
onClick: () => {
console.log('点击取消按钮')
// 卸载一个已挂载的应用实例。卸载一个应用会触发该应用组件树内所有组件的卸载生命周期钩子。
app.unmount() // 这样卸载会让动画消失
// 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
document.body.removeChild(div)
}
},
() => '取消'
),
h(
ElButton,
{
type: 'primary',
onClick: () => {
// submitForm 调用表单组件中需要验证或者暴露出去的数据
instanceElement?.value?.submitForm().then((res:any) =>{
console.log('得到的值',res)
// 验证通过后调用回调函数传递数据, 如验证失败,res 的值有可能是一个false。
onConfirm(res)
// 怎么把这个事件传递出去,让使用的时候知道点击了确认并且知道验证通过了
}).catch((error: any) => {
// 验证失败时也可以传递错误信息
console.log('验证失败', error)
})
console.log('确定')
}
},
() => '确定'
)
]
)
}
);
// 创建一个新的 Vue 应用实例。这个应用实例是独立的,与主应用分离。
const app = createApp(dialog)
// 在新实例中注册 Element Plus, 这弹窗中的组件就可以正常显示了
app.use(ElementPlus);
// 这个div元素在在销毁应用时需要被移除哈
const div = document.createElement('div')
document.body.appendChild(div)
app.mount(div)
}</pre></div>
<div class="jb51code"><pre class="brush:js;"><template>
<div>
<el-button @click="openMask">点击弹窗</el-button>
</div>
</template>
<script setup lang="ts">
import childTest from '@/components/childTest.vue'
import { renderDialog } from '@/hooks/dialog'
import { getCurrentInstance } from 'vue';
const currentInstance = getCurrentInstance();
function openMask(){
console.log('currentInstance',currentInstance)
renderDialog(childTest,{},{title:'测试弹窗', width: '700'}, (res)=>{
console.log('通过回调函数返回值', res)
})
}
</script></pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011309184855.png" /></p>
<p class="maodian"><a name="_label10"></a></p><h2>点击确定时,业务完成后关闭弹窗</h2>
<p>现在想要点击确定,等业务处理完成之后,才关闭弹窗。<br />需要在使用完成业务的时候返回一个promise,让封装的弹窗调用这个promise<br />这样就可以知道什么时候关闭弹窗了</p>
<div class="jb51code"><pre class="brush:js;">// 封装的弹窗
import { createApp, h, ref } from "vue";
import { ElDialog, ElButton, ElForm, ElFormItem, ElInput, ElSelect, ElOption } from "element-plus";
import ElementPlus from "element-plus";
export function renderDialog(component: any, props: any, modalProps: any, onConfirm: (data: any) => any ) {
// 第4个参数是回调函数
const instanceElement = ref()
console.log('111', instanceElement)
// 创建弹窗实例
const dialog = h(
ElDialog,
{
...modalProps,
modelValue: true,
onClose: ()=> {
console.log('关闭的回调')
app.unmount() // 这样卸载会让动画消失
// 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
document.body.removeChild(div)
}
},
{
// 主要内容插槽,这里的ref必须接收一个ref
default: () => h(component, {...props, ref: instanceElement}),
// 底部插槽
footer:() =>h(
'div',
{
class: 'dialog-footer',
},
[
h(
ElButton,
{
onClick: () => {
console.log('点击取消按钮')
// 卸载一个已挂载的应用实例。卸载一个应用会触发该应用组件树内所有组件的卸载生命周期钩子。
app.unmount() // 这样卸载会让动画消失
// 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
document.body.removeChild(div)
}
},
() => '取消'
),
h(
ElButton,
{
type: 'primary',
onClick: () => {
// submitForm 调用表单组件中需要验证或者暴露出去的数据
instanceElement?.value?.submitForm().then((res:any) =>{
console.log('得到的值',res)
// 验证通过后调用回调函数传递数据,如验证失败,res 的值有可能是一个false。
const callbackResult = onConfirm(res);
// 如果回调函数返回的是 Promise,则等待业务完成后再关闭弹窗
if (callbackResult instanceof Promise) {
// 注意这里的finally,这样写在服务出现异常的时候会有问题,这里是有问题的,需要优化
// 注意这里的finally,这样写在服务出现异常的时候会有问题,这里是有问题的,需要优化
callbackResult.finally(() => {
// 弹窗关闭逻辑
app.unmount()
document.body.removeChild(div)
});
} else {
// 如果不是 Promise,立即关闭弹窗
app.unmount()
document.body.removeChild(div)
}
}).catch((error: any) => {
// 验证失败时也可以传递错误信息
console.log('验证失败', error)
})
}
},
() => '确定'
)
]
)
}
);
// 创建一个新的 Vue 应用实例。这个应用实例是独立的,与主应用分离。
const app = createApp(dialog)
// 在新实例中注册 Element Plus, 这弹窗中的组件就可以正常显示了
app.use(ElementPlus);
// 这个div元素在在销毁应用时需要被移除哈
const div = document.createElement('div')
document.body.appendChild(div)
app.mount(div)
}</pre></div>
<div class="jb51code"><pre class="brush:js;"><template>
<div>
<el-button @click="openMask">点击弹窗</el-button>
</div>
</template>
<script setup lang="ts">
import childTest from '@/components/childTest.vue'
import { renderDialog } from '@/hooks/dialog'
import { getCurrentInstance } from 'vue';
const currentInstance = getCurrentInstance();
function openMask(){
console.log('currentInstance',currentInstance)
renderDialog(childTest,{},{title:'测试弹窗', width: '700'}, (res)=>{
console.log('通过回调函数返回值', res)
// 这里返回一个promise对象,这样就可以让业务完成后才关闭弹窗
return fetch("https://dog.ceo/api/breed/pembroke/images/random")
.then((res) => {
return res.json();
})
.then((res) => {
console.log('获取的图片地址为:', res.message);
});
})
}
</script></pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011309184870.png" /></p>
<p class="maodian"><a name="_label11"></a></p><h2>优化业务组件</h2>
<div class="jb51code"><pre class="brush:js;">// 封装的弹窗
import { createApp, h, ref } from "vue";
import { ElDialog, ElButton, ElForm, ElFormItem, ElInput, ElSelect, ElOption } from "element-plus";
import ElementPlus from "element-plus";
export function renderDialog(component: any, props: any, modalProps: any, onConfirm: (data: any) => any ) {
// 关闭弹窗,避免重复代码
const closeDialog = () => {
// 成功时关闭弹窗
app.unmount();
// 检查div是否仍然存在且为body的子元素,否者可能出现异常
if (div && div.parentNode) {
document.body.removeChild(div)
}
}
// 第4个参数是回调函数
const instanceElement = ref()
console.log('111', instanceElement)
// 创建弹窗实例
const dialog = h(
ElDialog,
{
...modalProps,
modelValue: true,
onClose: ()=> {
console.log('关闭的回调')
app.unmount() // 这样卸载会让动画消失
// 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
document.body.removeChild(div)
}
},
{
// 主要内容插槽,这里的ref必须接收一个ref
default: () => h(component, {...props, ref: instanceElement}),
// 底部插槽
footer:() =>h(
'div',
{
class: 'dialog-footer',
},
[
h(
ElButton,
{
onClick: () => {
console.log('点击取消按钮')
// 卸载一个已挂载的应用实例。卸载一个应用会触发该应用组件树内所有组件的卸载生命周期钩子。
app.unmount() // 这样卸载会让动画消失
// 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
document.body.removeChild(div)
}
},
() => '取消'
),
h(
ElButton,
{
type: 'primary',
onClick: () => {
// submitForm 调用表单组件中需要验证或者暴露出去的数据
instanceElement?.value?.submitForm().then((res:any) =>{
console.log('得到的值',res)
// 验证通过后调用回调函数传递数据,如验证失败,res 的值有可能是一个false。
const callbackResult = onConfirm(res);
// 如果回调函数返回的是 Promise,则等待业务完成后再关闭弹窗
if (callbackResult instanceof Promise) {
callbackResult.then(() => {
if(res){
console.log('111')
closeDialog()
}
}).catch(error=>{
console.log('222')
console.error('回调函数执行出错,如:网络错误', error);
// 错误情况下也关闭弹窗
closeDialog()
});
} else {
// 如果不是 Promise,并且验证时通过了的。立即关闭弹窗
console.log('333', res)
if(res){
closeDialog()
}
}
}).catch((error: any) => {
console.log('44444')
// 验证失败时也可以传递错误信息
console.log('验证失败', error)
})
}
},
() => '确定'
)
]
)
}
);
// 创建一个新的 Vue 应用实例。这个应用实例是独立的,与主应用分离。
const app = createApp(dialog)
// 在新实例中注册 Element Plus, 这弹窗中的组件就可以正常显示了
app.use(ElementPlus);
// 这个div元素在在销毁应用时需要被移除哈
const div = document.createElement('div')
document.body.appendChild(div)
app.mount(div)
}</pre></div>
<div class="jb51code"><pre class="brush:js;"><template>
<div>
<el-button @click="openMask">点击弹窗</el-button>
</div>
</template>
<script setup lang="ts">
import childTest from '@/components/childTest.vue'
import { renderDialog } from '@/hooks/dialog'
import { getCurrentInstance } from 'vue';
const currentInstance = getCurrentInstance();
function openMask(){
console.log('currentInstance',currentInstance)
renderDialog(childTest,{},{title:'测试弹窗', width: '700'}, (res)=>{
console.log('通过回调函数返回值', res)
// 这里返回一个promise对象,这样就可以让业务完成后才关闭弹窗
return fetch("https://dog.ceo/api/breed/pembroke/images/random")
.then((res) => {
return res.json();
})
.then((res) => {
console.log('获取的图片地址为:', res.message);
});
})
}
</script></pre></div>
<p>眼尖的小伙伴可能已经发现了这一段代码。<br />1,验证不通过会也会触发卸载弹窗<br />2,callbackResult.finally是不合适的<br />3.</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011309184889.png" /></p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202601/2026011309184850.png" /></p>
<p class="maodian"><a name="_label12"></a></p><h2>最终的代码</h2>
<div class="jb51code"><pre class="brush:js;">// 封装的弹窗
import { createApp, h, ref } from "vue";
import { ElDialog, ElButton, ElForm, ElFormItem, ElInput, ElSelect, ElOption } from "element-plus";
import ElementPlus from "element-plus";
export function renderDialog(component: any, props: any, modalProps: any, onConfirm: (data: any) => any ) {
// 关闭弹窗,避免重复代码
const closeDialog = () => {
// 成功时关闭弹窗
app.unmount();
// 检查div是否仍然存在且为body的子元素,否者可能出现异常
if (div && div.parentNode) {
document.body.removeChild(div)
}
}
// 第4个参数是回调函数
const instanceElement = ref()
console.log('111', instanceElement)
const isLoading = ref(false)
// 创建弹窗实例
const dialog = h(
ElDialog,
{
...modalProps,
modelValue: true,
onClose: ()=> {
isLoading.value = false
console.log('关闭的回调')
app.unmount() // 这样卸载会让动画消失
// 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
document.body.removeChild(div)
}
},
{
// 主要内容插槽,这里的ref必须接收一个ref
default: () => h(component, {...props, ref: instanceElement}),
// 底部插槽,noShowFooterBool是true,不显示; false的显示底部
footer: props.noShowFooterBool ? null : () =>h(
'div',
{
class: 'dialog-footer',
},
[
h(
ElButton,
{
onClick: () => {
console.log('点击取消按钮')
// 卸载一个已挂载的应用实例。卸载一个应用会触发该应用组件树内所有组件的卸载生命周期钩子。
app.unmount() // 这样卸载会让动画消失
// 卸载的同时需要把我们创建的div元素移除,否则页面上会出现很多div
document.body.removeChild(div)
}
},
() => props.cancelText || '取消'
),
h(
ElButton,
{
type: 'primary',
loading: isLoading.value,
onClick: () => {
isLoading.value = true
// submitForm 调用表单组件中需要验证或者暴露出去的数据
instanceElement?.value?.submitForm().then((res:any) =>{
if(!res){
isLoading.value = false
}
console.log('得到的值',res)
// 验证通过后调用回调函数传递数据,如验证失败,res 的值有可能是一个false。
const callbackResult = onConfirm(res);
// 如果回调函数返回的是 Promise,则等待业务完成后再关闭弹窗
if (callbackResult instanceof Promise) {
callbackResult.then(() => {
if(res){
console.log('111')
closeDialog()
}else{
isLoading.value = false
}
}).catch(error=>{
console.log('222')
console.error('回调函数执行出错,如:网络错误', error);
// 错误情况下也关闭弹窗
closeDialog()
});
} else {
// 如果不是 Promise,并且验证时通过了的。立即关闭弹窗
console.log('333', res)
if(res){
closeDialog()
}else{
isLoading.value = false
}
}
}).catch((error: any) => {
console.log('44444')
isLoading.value = false
// 验证失败时也可以传递错误信息
console.log('验证失败', error)
})
}
},
() => props.confirmText ||'确定'
)
]
)
}
);
// 创建一个新的 Vue 应用实例。这个应用实例是独立的,与主应用分离。
const app = createApp(dialog)
// 在新实例中注册 Element Plus, 这弹窗中的组件就可以正常显示了
app.use(ElementPlus);
// 这个div元素在在销毁应用时需要被移除哈
const div = document.createElement('div')
document.body.appendChild(div)
app.mount(div)
}</pre></div>
<div class="jb51code"><pre class="brush:js;"><template>
<div>
<el-button @click="openMask">点击弹窗</el-button>
</div>
</template>
<script setup lang="ts">
import childTest from '@/components/childTest.vue'
import { renderDialog } from '@/hooks/dialog'
import { getCurrentInstance } from 'vue';
const currentInstance = getCurrentInstance();
function openMask(){
console.log('currentInstance',currentInstance)
const otherProps ={cancelText:'取消哈', confirmText: '确认哈',showFooterBool:true }
const dialogSetObject = {title:'测试弹窗哈', width: '700', draggable: true}
renderDialog(childTest,otherProps,dialogSetObject, (res)=>{
console.log('通过回调函数返回值', res)
// 这里返回一个promise对象,这样就可以让业务完成后才关闭弹窗
return fetch("https://dog.ceo/api/breed/pembroke/images/random")
.then((res) => {
return res.json();
})
.then((res) => {
console.log('获取的图片地址为:', res.message);
});
})
}
</script>
<style lang="scss" scoped>
</style></pre></div>
<div class="jb51code"><pre class="brush:js;"><template>
<el-form
ref="ruleFormRef"
style="max-width: 600px"
:model="ruleForm"
:rules="rules"
label-width="auto"
>
<el-form-item label="Activity name" prop="name">
<el-input v-model="ruleForm.name" />
</el-form-item>
<el-form-item label="Activity zone" prop="region">
<el-select v-model="ruleForm.region" placeholder="Activity zone">
<el-option label="Zone one" value="shanghai" />
<el-option label="Zone two" value="beijing" />
</el-select>
</el-form-item>
<el-form-item label="Activity time" required>
<el-col :span="11">
<el-form-item prop="date1">
<el-date-picker
v-model="ruleForm.date1"
type="date"
aria-label="Pick a date"
placeholder="Pick a date"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col class="text-center" :span="2">
<span class="text-gray-500">-</span>
</el-col>
<el-col :span="11">
<el-form-item prop="date2">
<el-time-picker
v-model="ruleForm.date2"
aria-label="Pick a time"
placeholder="Pick a time"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-form-item>
<el-form-item label="Resources" prop="resource">
<el-radio-group v-model="ruleForm.resource">
<el-radio value="Sponsorship">Sponsorship</el-radio>
<el-radio value="Venue">Venue</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="Activity form" prop="desc">
<el-input v-model="ruleForm.desc" type="textarea" />
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
interface RuleForm {
name: string
region: string
date1: string
date2: string
resource: string
desc: string
}
const ruleFormRef = ref<FormInstance>()
const ruleForm = reactive<RuleForm>({
name: 'Hello',
region: '',
date1: '',
date2: '',
resource: '',
desc: '',
})
const rules = reactive<FormRules<RuleForm>>({
name: [
{ required: true, message: 'Please input Activity name', trigger: 'blur' },
{ min: 3, max: 5, message: 'Length should be 3 to 5', trigger: 'blur' },
],
region: [
{
required: true,
message: 'Please select Activity zone',
trigger: 'change',
},
],
date1: [
{
type: 'date',
required: true,
message: 'Please pick a date',
trigger: 'change',
},
],
date2: [
{
type: 'date',
required: true,
message: 'Please pick a time',
trigger: 'change',
},
],
resource: [
{
required: true,
message: 'Please select activity resource',
trigger: 'change',
},
],
desc: [
{ required: true, message: 'Please input activity form', trigger: 'blur' },
],
})
const submitForm = async () => {
if (!ruleFormRef.value) {
console.error('ruleFormRef is not initialized')
return false
}
try {
const valid = await ruleFormRef.value.validate()
if (valid) {
// 验证通过后,就会可以把你需要的数据暴露出去
return Promise.resolve(ruleForm)
}
} catch (error) {
// 为啥submitForm中,valid的值是false会执行catch ?
// el-form 组件的 validate 方法的工作机制导致的。 validate 方法在表单验证失败时会抛出异常
console.error('err', error)
return false
/**
* 下面这样写为啥界面会报错呢?
* return Promise.reject(error)
* 当表单验证失败时,ruleFormRef.value.validate() 会抛出一个异常。
* 虽然你用了 try...catch 捕获这个异常,并且在 catch 块中通过 return Promise.reject(error) 返回了一个被拒绝的 Promise
* 但如果调用 submitForm 的地方没有正确地处理这个被拒绝的 Promise(即没有使用 .catch() 或者 await 来接收错误),
* 那么浏览器控制台就会显示一个 "Uncaught (in promise)" 错误。
* 在 catch 中再次 return Promise.reject(error) 是多余的, 直接return false
* */
/**
* 如果你这样写
* throw error 直接抛出错误即可
* 那么就需要再调用submitForm的地方捕获异常
* */
}
}
defineExpose({
submitForm:submitForm
})
</script></pre></div>
頁:
[1]