Technology Sharing | How to use Hook to encapsulate el-dialog in pop-up window development?

Pop-up windows are a common requirement in front-end development. The components in the Element UI framework el-dialogprovide 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 useDialogHook encapsulation el-dialogto 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:

  1. Encapsulate the purchase component : First create a general purchase component so that it can be reused on different pages and scenarios.
  2. Render the purchase component on the payment page : embed the purchase component directly into the payment page.
  3. Use el-dialogthe display purchase componentel-dialog on other pages: control the display of the component on other pages , and use visiblestate variables (usually a refresponsive 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 useDialogis a custom Hook that encapsulates el-dialogthe 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

useDialogHook needs to achieve the following goals:

  1. Meet the basic usage, el-dialogthe basic attributes passed in and the content displayed by the default slot, export openDialogand closeDialogfunctions;
  2. Supported el-dialogevent configuration;
  3. Supports default slotcomponent attribute configuration;
  4. Support el-dialogother slot configurations, such as headerand footeretc.;
  5. Throwing specific events in content components supports closing the dialog;
  6. Supported display content is jsx, 普通文本, Vue Component;
  7. Supports callback functions that control whether the displayed content can be closed, for example beforeClose;
  8. Supports display before hooks, for example onBeforeOpen;
  9. Supports modifying configuration properties during definition and pop-up;
  10. Supports inheriting the prototype of root vue, you can use functions such vue-i18nas ;$t
  11. Support tsparameter prompt;

(1) Prepare useDialog.tsfile 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 useDialogfunctions

The following functions implement basic usage including goals 1, 2, 3, 4, 6 and 11.

Goal 1: Meet basic usage, pass in el-dialogbasic attributes and default slot display content, export openDialogand closeDialogfunctions;
Goal 2: Support el-dialogevent configuration;
Goal 3.: Support slotattribute configuration of default components;
Goal 4: Support el-dialogother slot configurations, such as headerand footeretc.;
Goal 6: Support display content of jsx, 普通文本, Vue Component;
Goal 11: Support tsparameter 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;

  1. supported in definition closeEventName;
interface Options<P> {
  // ...
  closeEventName?: string // 新增的属性
}
  1. Modify useDialogthe function to receive closeEventNamethe 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 example onBeforeOpen;

  1. It is supported in the definition onBeforeOpenand beforeCloseDialogpassed to the content component by default, with component call settings;
type DialogProps = ElDialogInstance['$props'] & {
  onBeforeOpen?: () => boolean | void
}
  1. Modify useDialogthe function to receive onBeforeOpenthe event and pass it on beforeCloseDialog.
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 such vue-i18nas ;$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 openDialogthe 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 useDialogmodify it in Hook, without having to edit it one by one.

4. UseDialog Hook case practice

Next, we use useDialogHook to solve the application purchase problem mentioned at the beginning.

(1) Create components/buy.vuepurchase component

<script lang="ts" setup>
  const props = defineProps({
    from: {
      type: String,
      default: '',
    },
  })
</script>
<template>
  我是购买组件
</template>

(2) pages/subscription.vueUse buy.vuethe 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& closeEventNameExample: buy.vuePurchase 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 useDialogHook encapsulation el-dialogcan 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.

Welcome to follow the LigaAI account, and we look forward to you clicking on the new generation intelligent R&D collaboration platform to have more exchanges with us.

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
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/5057806/blog/11091317