Pop-up windows are a common requirement in front-end development. The components in the Element UI framework el-dialog
provide basic functions related to pop-up windows. However, in actual development, we will inevitably encounter some customized requirements, such as secondary encapsulation of pop-up windows to uniformly manage styles and behaviors in the project.
This article will share how to use useDialog
Hook encapsulation el-dialog
to achieve a more flexible and easier-to-use pop-up component.
1. Clarification of issues
"Applying a common component to multiple pages" is a very common practical scenario.
For example: Take the purchase of an application as an example. The user may make a purchase on the payment page, or may trigger a purchase request while browsing other pages. In this case, a dialog box needs to pop up to guide the user to complete the purchase behavior.
To achieve this functionality, the following steps have typically been taken in the past:
- Encapsulate the purchase component : First create a general purchase component so that it can be reused on different pages and scenarios.
- Render the purchase component on the payment page : embed the purchase component directly into the payment page.
- Use
el-dialog
the display purchase componentel-dialog
on other pages: control the display of the component on other pages , and usevisible
state variables (usually aref
responsive variable) to dynamically control the pop-up and closing of the dialog box.
Although this method can meet functional requirements, as the component is used by more and more pages and functions, maintenance will become more complex and cumbersome - for each additional page of use, the logic to control display/hide must be written repeatedly code.
So, is there a better way to simplify this process? Is it possible to use a separate function to globally control the opening and closing of the purchase component in some way, thereby reducing code duplication and maintenance costs?
2. About useDialog Hook
In Vue, Hooks allow "hooking" Vue features into functional components or APIs. They are usually used in the Composition API, which is a set of responsive and reusable logic functions provided by Vue.
The Hook mentioned in this article useDialog
is a custom Hook that encapsulates el-dialog
the basic functions of the component. It can also provide additional features to manage and display pop-up windows in the project.
3. Implement useDialog Hook
useDialog
Hook needs to achieve the following goals:
- Meet the basic usage,
el-dialog
the basic attributes passed in and the content displayed by the default slot, exportopenDialog
andcloseDialog
functions; - Supported
el-dialog
event configuration; - Supports default
slot
component attribute configuration; - Support
el-dialog
other slot configurations, such asheader
andfooter
etc.; - Throwing specific events in content components supports closing the dialog;
- Supported display content is
jsx
,普通文本
,Vue Component
; - Supports callback functions that control whether the displayed content can be closed, for example
beforeClose
; - Supports display before hooks, for example
onBeforeOpen
; - Supports modifying configuration properties during definition and pop-up;
- Supports inheriting the prototype of root vue, you can use functions such
vue-i18n
as ;$t
- Support
ts
parameter prompt;
(1) Prepare useDialog.ts
file implementation type definition
import type { Ref } from 'vue'
import { h, render } from 'vue'
import { ElDialog } from 'element-plus'
import type {
ComponentInternalInstance,
} from '@vue/runtime-core'
type Content = Parameters<typeof h>[0] | string | JSX.Element
// 使用 InstanceType 获取 ElDialog 组件实例的类型
type ElDialogInstance = InstanceType<typeof ElDialog>
// 从组件实例中提取 Props 类型
type DialogProps = ElDialogInstance['$props'] & {
}
interface ElDialogSlots {
header?: (...args: any[]) => Content
footer?: (...args: any[]) => Content
}
interface Options<P> {
dialogProps?: DialogProps
dialogSlots?: ElDialogSlots
contentProps?: P
}
(2) Implement ordinary useDialog
functions
The following functions implement basic usage including goals 1, 2, 3, 4, 6 and 11.
Goal 1: Meet basic usage, pass in
el-dialog
basic attributes and default slot display content, exportopenDialog
andcloseDialog
functions;
Goal 2: Supportel-dialog
event configuration;
Goal 3.: Supportslot
attribute configuration of default components;
Goal 4: Supportel-dialog
other slot configurations, such asheader
andfooter
etc.;
Goal 6: Support display content ofjsx
,普通文本
,Vue Component
;
Goal 11: Supportts
parameter prompts;
export function useDialog<P = any>(content: Content, options?: Ref<Options<P>> | Options<P>) {
let dialogInstance: ComponentInternalInstance | null = null
let fragment: Element | null = null
// 关闭并卸载组件
const closeAfter = () => {
if (fragment) {
render(null, fragment as unknown as Element) // 卸载组件
fragment.textContent = '' // 清空文档片段
fragment = null
}
dialogInstance = null
}
function closeDialog() {
if (dialogInstance)
dialogInstance.props.modelValue = false
}
// 创建并挂载组件
function openDialog() {
if (dialogInstance) {
closeDialog()
closeAfter()
}
const { dialogProps, contentProps } = options
fragment = document.createDocumentFragment() as unknown as Element
const vNode = h(ElDialog, {
...dialogProps,
modelValue: true,
onClosed: () => {
dialogProps?.onClosed?.()
closeAfter()
},
}, {
default: () => [typeof content === 'string'
? content
: h(content as any, {
...contentProps,
})],
...options.dialogSlots,
})
render(vNode, fragment)
dialogInstance = vNode.component
document.body.appendChild(fragment)
}
onUnmounted(() => {
closeDialog()
})
return { openDialog, closeDialog }
}
(3) Achieve Goal 5
Goal 5: Throw specific events in content components to support closing the dialog;
- supported in definition
closeEventName
;
interface Options<P> {
// ...
closeEventName?: string // 新增的属性
}
- Modify
useDialog
the function to receivecloseEventName
the event to close the dialog.
export function useDialog<P = any>(content: Content, options?: Ref<Options<P>> | Options<P>) {
// ...
// 创建并挂载组件
function openDialog() {
// ...
fragment = document.createDocumentFragment() as unknown as Element
// 转换closeEventName事件
const closeEventName = `on${upperFirst(_options?.closeEventName || 'closeDialog')}`
const vNode = h(ElDialog, {
// ...
}, {
default: () => [typeof content === 'string'
? content
: h(content as any, {
...contentProps,
[closeEventName]: closeDialog, // 监听自定义关闭事件,并执行关闭
})],
...options.dialogSlots,
})
render(vNode, fragment)
dialogInstance = vNode.component
document.body.appendChild(fragment)
}
onUnmounted(() => {
closeDialog()
})
return { openDialog, closeDialog }
}
(4) Achieve Goals 7 and 8
Goal 7: Support a callback function that controls whether it can be closed in the displayed content, for example
beforeClose
;
Goal 8: Support hooks before displaying, for exampleonBeforeOpen
;
- It is supported in the definition
onBeforeOpen
andbeforeCloseDialog
passed to the content component by default, with component call settings;
type DialogProps = ElDialogInstance['$props'] & {
onBeforeOpen?: () => boolean | void
}
- Modify
useDialog
the function to receiveonBeforeOpen
the event and pass it onbeforeCloseDialog
.
export function useDialog<P = any>(content: Content, options?: Ref<Options<P>> | Options<P>) {
// ...
// 创建并挂载组件
function openDialog() {
// ...
const { dialogProps, contentProps } = options
// 调用before钩子,如果为false则不打开
if (dialogProps?.onBeforeOpen?.() === false) {
return
}
// ...
// 定义当前块关闭前钩子变量
let onBeforeClose: (() => Promise<boolean | void> | boolean | void) | null
const vNode = h(ElDialog, {
// ...
beforeClose: async (done) => {
// 配置`el-dialog`的关闭回调钩子函数
const result = await onBeforeClose?.()
if (result === false) {
return
}
done()
},
onClosed: () => {
dialogProps?.onClosed?.()
closeAfter()
// 关闭后回收当前变量
onBeforeClose = null
},
}, {
default: () => [typeof content === 'string'
? content
: h(content as any, {
// ...
beforeCloseDialog: (fn: (() => boolean | void)) => {
// 把`beforeCloseDialog`传递给`content`,当组件内部使用`props.beforeCloseDialog(fn)`时,会把fn传递给`onBeforeClose`
onBeforeClose = fn
},
})],
...options.dialogSlots,
})
render(vNode, fragment)
dialogInstance = vNode.component
document.body.appendChild(fragment)
}
onUnmounted(() => {
closeDialog()
})
return { openDialog, closeDialog }
}
(5) Achieve goals 9 and 10
Goal 9: Support modifying configuration properties when defining and popping up;
Goal 10: Support inheriting the prototype of root vue, you can use functions suchvue-i18n
as ;$t
// 定义工具函数,获取计算属性的option
function getOptions<P>(options?: Ref<Options<P>> | Options<P>) {
if (!options)
return {}
return isRef(options) ? options.value : options
}
export function useDialog<P = any>(content: Content, options?: Ref<Options<P>> | Options<P>) {
// ...
// 获取当前组件实例,用于设置当前dialog的上下文,继承prototype
const instance = getCurrentInstance()
// 创建并挂载组件,新增`modifyOptions`参数
function openDialog(modifyOptions?: Partial<Options<P>>) {
// ...
const _options = getOptions(options)
// 如果有修改,则合并options。替换之前的options变量为 _options
if (modifyOptions)
merge(_options, modifyOptions)
// ...
const vNode = h(ElDialog, {
// ...
}, {
// ...
})
// 设置当前的上下文为使用者的上下文
vNode.appContext = instance?.appContext || null
render(vNode, fragment)
dialogInstance = vNode.component
document.body.appendChild(fragment)
}
onUnmounted(() => {
closeDialog()
})
return { openDialog, closeDialog }
}
After using Hook through the above package useDialog
, when you need to pop up a window, you only need to introduce the Hook and call openDialog
the method, which is very convenient and concise. In addition, such encapsulation will also make it more convenient to modify the pop-up window logic later. You only need to useDialog
modify it in Hook, without having to edit it one by one.
4. UseDialog Hook case practice
Next, we use useDialog
Hook to solve the application purchase problem mentioned at the beginning.
(1) Create components/buy.vue
purchase component
<script lang="ts" setup>
const props = defineProps({
from: {
type: String,
default: '',
},
})
</script>
<template>
我是购买组件
</template>
(2) pages/subscription.vue
Use buy.vue
the purchase component on the page
<script lang="ts" setup>
import Buy from '@/components/buy.vue'
</script>
<template>
<Buy from="subscription" />
</template>
buy.vue
(3) Pop-up purchase components on other function pages
<script lang="ts" setup>
import { useDialog } from '@/hooks/useDialog'
const Buy = defineAsyncComponent(() => import('@/components/buy.vue'))
const { openDialog } = useDialog(Buy, {
dialogProps: {
// ...
title: '购买'
},
contentProps: {
from: 'function',
},
})
const onSomeClick = () => {
openDialog()
}
</script>
Extension: other applications of useDialog Hook
beforeClose
& closeEventName
Example: buy.vue
Purchase components
<script lang="ts" setup>
const props = defineProps({
from: {
type: String,
default: '',
},
beforeCloseDialog: {
type: Function,
default: () => true,
},
})
const emit = defineEmits(['closeDialog'])
props.beforeCloseDialog(() => {
// 假如from 为 空字符串不能关闭
if (!props.from) {
return false
}
return true
})
// 关闭dialog
const onBuySuccess = () => emit('closeDialog')
</script>
<script lang="ts" setup>
import { useDialog } from '@/hooks/useDialog'
const Buy = defineAsyncComponent(() => import('@/components/buy.vue'))
const { openDialog } = useDialog(Buy, {
dialogProps: {
// ...
title: '购买'
},
contentProps: {
from: '',
},
})
const onSomeClick = () => {
openDialog()
}
</script>
Summarize
Using useDialog
Hook encapsulation el-dialog
can make front-end technology more interesting and concise. The author also hopes that everyone can try this kind of encapsulation method to make the front-end code more elegant and easy to maintain.
Excellent engineers are like excellent chefs. They master exquisite cooking and seasoning skills to make every dish delicious!
LigaAI attaches great importance to the maintenance and construction of developer culture and will continue to share more technology sharing and interesting technology practices.
To help developers set sail, LigaAI looks forward to traveling with you all the way!
A programmer born in the 1990s developed a video porting software and made over 7 million in less than a year. The ending was very punishing! High school students create their own open source programming language as a coming-of-age ceremony - sharp comments from netizens: Relying on RustDesk due to rampant fraud, domestic service Taobao (taobao.com) suspended domestic services and restarted web version optimization work Java 17 is the most commonly used Java LTS version Windows 10 market share Reaching 70%, Windows 11 continues to decline Open Source Daily | Google supports Hongmeng to take over; open source Rabbit R1; Android phones supported by Docker; Microsoft's anxiety and ambition; Haier Electric shuts down the open platform Apple releases M4 chip Google deletes Android universal kernel (ACK ) Support for RISC-V architecture Yunfeng resigned from Alibaba and plans to produce independent games on the Windows platform in the future