🤔 优化使用体验 Vue Hook 改造 Dialog 对话框组件实现 API 调用

前言

在上一篇文章中,我们使用 Vue+Zag+PandaCSS 实现一个超丝滑的对话框组件,这篇文章基于对话框组件进行补充,新增一种通过 Vue Hook 去控制对话框的使用方式:

传统组件用法:

tsx 复制代码
<script setup lang="ts">
import { ref } from "vue";

import Button from "./components/button/index.tsx";
import Dialog from "./components/dialog/index.tsx";

const isOpen = ref(false);
</script>

<template>
  <div>
    <Button @click="isOpen = !isOpen">show dialog</Button>
    <Dialog
      v-model="isOpen"
      title="Edit Profile"
      content="Make changes to your profile here. Click save when you are done."
    >
    </Dialog>
  </div>
</template>

hook 用法:

tsx 复制代码
<template>
  <div>
    <ZButton @click="open">show dialog</ZButton>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

import { ZButton } from "./components/button/index.ts";
import { useDialog } from "./components/dialog/index.ts";

const { open, close } = useDialog({
  title: "Edit Profile",
  content: "Make changes to your profile here. Click save when you are done.",
});
</script>

相比于直接在页面中引入再手动写在模板中,使用 Hook 调用更加简单,而且大部分的对话框场景并不复杂,不需要去进行大量的自定义元素样式,因此 Hook 往往是更加高频的使用方式。

如果你想学习有关组件实现 Vue TSX 的前置知识,也可以参考以往的文章,前文回顾:

🚀一篇文章学会如何使用 JSX/TSX 开发 Vue 组件

🖱️使用 Vue+pandaCSS+tsx 实现一个超精致的按钮组件

😎使用 Vue+Zag+PandaCSS 实现一个超丝滑的对话框组件

改造

在改造前,我们先确认一下这个 hook 需要实现的功能:

  1. 调用 hook 的时候 自动将 Dialog 组件挂载 在页面中,不需要在模板中手动写入
  2. 不需要手动管理 Dialog 打开或关闭的状态,只需要调用打开或关闭的方法即可使用

自动挂载组件

首先实现调用 hook 时自动挂载组件

tsx 复制代码
import { createApp, onMounted, onUnmounted } from "vue";

import { ZDialog, ZDialogProps } from ".";

const useDialog = (props: ZDialogProps) => {
  const app = createApp(ZDialog, { ...props, modelValue: true });

  let DialogDom: HTMLDivElement;

  onMounted(() => {
    DialogDom = document.createElement("div");
    app.mount(DialogDom);
    document.body.appendChild(DialogDom);
  });

  onUnmounted(() => {
    app.unmount();
    document.body.removeChild(DialogDom);
  });
};

export default useDialog;
  1. createApp 函数用于创建一个Vue实例,该实例的根组件为 ZDialog 组件,并将 props 对象与modelValue 属性合并作为 rootProps 传入, modelValue 暂时默认为 true。
  2. 定义了一个名为 DialogDom 的变量,类型为 HTMLDivElement,用于存储即将创建的对话框组件的 DOM 节点。
  3. onMounted 钩子函数在组件挂载前调用创建了一个新的 div 元素作为组件的挂载点,并使用 app.mount 方法将新的 Vue 实例挂载到该元素上。
  4. 接着,DialogDom 被添加到 document.body 中,使对话框组件显示在页面上。
  5. onUnmounted钩子函数在组件卸载前调用 app.unmount() 方法将 Vue 实例卸载,然后使用document.body.removeChild(DialogDom) 移除对话框组件的 DOM 节点。

这样就实现了调用 hook 时自动挂载元素,由于 modelValue 默认为 true 所以已进入页面就会弹出对话框:

在 hook 内维护状态

接下来在 hook 中补充打开和关闭的方法,并且维护一个弹窗是否打开的状态:

diff 复制代码
import { createApp, onMounted, onUnmounted, ref } from "vue";

import { ZDialog, ZDialogProps } from ".";

const useDialog = (props: ZDialogProps) => {
+  const isOpen = ref(false);
+  const app = createApp(ZDialog, { ...props, modelValue: isOpen.value });

  let DialogDom: HTMLDivElement;

  onMounted(() => {
    DialogDom = document.createElement("div");
    app.mount(DialogDom);
    document.body.appendChild(DialogDom);
  });

  onUnmounted(() => {
    app.unmount();
    document.body.removeChild(DialogDom);
  });

+  const open = () => {
+    isOpen.value = true;
+  };

+  const close = () => {
+    isOpen.value = false;
+  };

+  return { open, close };
};

export default useDialog;

然后修改页面调用 hook 的方法,在点击按钮时执行 open 函数:

tsx 复制代码
<template>
  <div>
    <ZButton @click="open">show dialog</ZButton>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

import { ZButton } from "./components/button/index.ts";
import { useDialog } from "./components/dialog/index.ts";

const { open, close } = useDialog({
  title: "Edit Profile",
  content: "Make changes to your profile here. Click save when you are done.",
});
</script>

在这样修改后,你会发现点击按钮并不能弹出弹窗:

这是为什么呢,明明我已经将 isOpen 传入组件中了,为什么将 isOpen 修改为 true 却没有弹出对话框呢?

这是因为 createApp 函数的第二个参数 rootProps 并不是响应式的,并不会随着你的值改变了而重新渲染,具体的上下文也可以参考这个 issue github.com/vuejs/core/...

因此我们要调整一种写法:

ts 复制代码
// old
const app = createApp(ZDialog, { ...props, modelValue: isOpen.value });

// new
const app = createApp(() => <ZDialog {...props} v-model={isOpen.value} />);

直接将 props 传入 Dialog 组件,而不经过 rootProps, 通过函数式组件直接返回一个 ZDialog 组件的实例,这么写还有一个好处,就是可以直接使用 v-model 双向绑定,不需要传递值的同时再传递一个修改值的方法。修改后再看看效果:

可以看到我们已经顺利的实现了对话框的打开和关闭功能。

响应式参数

想要实现对话框内的元素随着 props 的变化进行响应式更改,只需要将传入的 props 使用 reactive 函数包装一下:

tsx 复制代码
<template>
  <div>
    <ZButton @click="open">show dialog</ZButton>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from "vue";

import { ZButton } from "./components/button/index.ts";
import { useDialog } from "./components/dialog/index.ts";

const dialogProps = reactive({
  title: "Edit Profile",
  content: "Make changes to your profile here. Click save when you are done.",
  size: "sm",
});

setTimeout(() => {
  dialogProps.size = "xl";
}, 5000);

const { open, close } = useDialog(dialogProps);
</script>

这里使用 reactive 函数包装了 props 再传入 hook,设置了一个定时器在 5 秒后修改弹窗的大小,效果如下图:

可以看到对话框随着 props 的改变顺利的重新渲染改变了尺寸。

最后修改一下 hook 的参数,将 modelValue 从参数中忽略掉,避免使用 hook 传值时类型提示错误,重复传入了 modelValue:

ts 复制代码
// old
const useDialog = (props:ZDialogProps) => {

// new
const useDialog = (props: Omit<ZDialogProps, "modelValue">) => {

传入自定义插槽

在 vue 中,由于组件得通过插槽的形式传入自定义元素,而不是像 react 那样 props 一把梭,因此我们除了 props 之外,我们还得单独处理 slots 属性:

diff 复制代码
import { createApp, onMounted, onUnmounted, ref } from "vue";

import { ZDialog, ZDialogProps } from ".";

const useDialog = (
  props: Omit<ZDialogProps, "modelValue">,
+  slots: InstanceType<typeof ZDialog>["$slots"],
) => {
  const isOpen = ref(false);
  const app = createApp(() => (
+    <ZDialog {...props} v-model={isOpen.value} v-slots={slots} />
  ));

  let DialogDom: HTMLDivElement;

  onMounted(() => {
    DialogDom = document.createElement("div");
    app.mount(DialogDom);
    document.body.appendChild(DialogDom);
  });

  onUnmounted(() => {
    app.unmount();
    document.body.removeChild(DialogDom);
  });

  const open = () => {
    isOpen.value = true;
  };

  const close = () => {
    isOpen.value = false;
  };

  return { open, close };
};

export default useDialog;

这里为 hook 增加第二个参数 slots,类型通过 InstanceType<typeof ZDialog>["$slots"] 的方式获取,然后在组件中通过 v-slots 的方式传入,这里的类型需要你在组件中提前定义好,在我的 Dialog 组件中我是这么定义的:

tsx 复制代码
const ZDialog = defineComponent({
  slots: Object as SlotsType<{
    default?: any;
    title?: any;
    footer?: any;
  }>,
  // ...
});

export default ZDialog;

测试一下效果,在调用 hook 时第二个参数传入 title 插槽一个自定义的文本内容,h() 函数用于在 vue 文件中创建 vnodes,如果你使用的是 JSX 则直接传入一个返回 Element 的函数即可:

ts 复制代码
// vue
const { open, close } = useDialog(dialogProps, {
  title: h("h1", { style: { color: "red" }, innerHTML: "Hello" }),
});

// jsx
const { open, close } = useDialog(dialogProps, {
  title: <h1 style={{color: "red"}}>"Hello</h1>,
});

我们看下效果,标题确实变为自定义传入的红色 Hello 了:

总结

将对话框组件改造为 hook 的调用方式,其中有一些小的细节还是很容易踩坑的。例如 rootProps 没有响应式,如果传递 slots 等等。本文对应的源码可以在这里看到 github.com/oil-oil/zax..., 后续的更新也都在这个仓库中, 快去点个 star 🌟

如果你想学习如何在 react 中实现 hook 调用对话框组件,或者如何实现异步的对话框确认操作,也可以看看这一篇文章: 🤔 使用 React Hook 实现 Modal 组件 API 调用两种方式

如果文章对你有帮助在收藏的同时也可以为我点个赞👍,respect!

相关推荐
橙光成长2 小时前
PDF离线工具箱深度解析:4MB纯前端单文件,40+功能全本地运行,文件绝不上传
前端·pdf·1024程序员节
小poop9 小时前
CSS 基础与实战:从入门到精通网页美化
前端
Csvn10 小时前
运行时性能
前端
Never_every9911 小时前
位图转矢量工具哪个好?实测抠抠图AI位图转矢量工具
大数据·前端
paopaokaka_luck11 小时前
宠物门店经营管理系统(销售与库存联动、进货自动记账、 退换与退款关联、员工个人业绩、ECharts 经营分析、滞销识别与客户分层、宠物档案年龄计算)
前端·javascript·spring boot·mysql·echarts·mybatis·1024程序员节
空中湖12 小时前
用 React 写数据大屏:为什么我不建议用图表库
前端·react.js·前端框架
欣欣之王来了13 小时前
原始HTML插值:v-html指令的使用与安全风险
前端·学习·架构·项目·vue3教程
IT_陈寒13 小时前
Redis的LRU淘汰策略坑了我一整天
前端·人工智能·后端
计算机魔术师14 小时前
砍半价格、参数再涨,国产旗舰追到 Opus 鼻尖了
前端
paopaokaka_luck14 小时前
心理咨询管理系统(WebSocket实时沟通、基于用户的协同过滤、 ECharts多维分析、 预约业务流程、预约校验与定时处理)
java·vue.js·spring boot·mysql·spring·echarts·1024程序员节