🤔 优化使用体验 Vue Hook 改造 Dialog 对话框组件实现 API 调用
前言
在上一篇文章中,我们使用 Vue+Zag+PandaCSS 实现一个超丝滑的对话框组件,这篇文章基于对话框组件进行补充,新增一种通过 Vue Hook 去控制对话框的使用方式:
传统组件用法:
<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 用法:
<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 的前置知识,也可以参考以往的文章,前文回顾:
改造
在改造前,我们先确认一下这个 hook 需要实现的功能:
- 调用 hook 的时候 自动将 Dialog 组件挂载 在页面中,不需要在模板中手动写入
- 不需要手动管理 Dialog 打开或关闭的状态,只需要调用打开或关闭的方法即可使用
自动挂载组件
首先实现调用 hook 时自动挂载组件
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;
createApp
函数用于创建一个Vue实例,该实例的根组件为ZDialog
组件,并将props
对象与modelValue
属性合并作为rootProps
传入,modelValue
暂时默认为true
。- 定义了一个名为
DialogDom
的变量,类型为HTMLDivElement
,用于存储即将创建的对话框组件的 DOM 节点。 onMounted
钩子函数在组件挂载前调用创建了一个新的div
元素作为组件的挂载点,并使用app.mount
方法将新的 Vue 实例挂载到该元素上。- 接着,
DialogDom
被添加到document.body
中,使对话框组件显示在页面上。 onUnmounted
钩子函数在组件卸载前调用app.unmount()
方法将 Vue 实例卸载,然后使用document.body.removeChild(DialogDom)
移除对话框组件的 DOM 节点。
这样就实现了调用 hook 时自动挂载元素,由于 modelValue
默认为 true
所以已进入页面就会弹出对话框:
在 hook 内维护状态
接下来在 hook 中补充打开和关闭的方法,并且维护一个弹窗是否打开的状态:
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
函数:
<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/…
因此我们要调整一种写法:
// 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
函数包装一下:
<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
:
// old
const useDialog = (props:ZDialogProps) => {
// new
const useDialog = (props: Omit<ZDialogProps, "modelValue">) => {
传入自定义插槽
在 vue 中,由于组件得通过插槽的形式传入自定义元素,而不是像 react 那样 props
一把梭,因此我们除了 props
之外,我们还得单独处理 slots
属性:
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
组件中我是这么定义的:
const ZDialog = defineComponent({
slots: Object as SlotsType<{
default?: any;
title?: any;
footer?: any;
}>,
// ...
});
export default ZDialog;
测试一下效果,在调用 hook 时第二个参数传入 title
插槽一个自定义的文本内容,h()
函数用于在 vue 文件中创建 vnodes
,如果你使用的是 JSX 则直接传入一个返回 Element 的函数即可:
// 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 🌟
如果文章对你有帮助在收藏的同时也可以为我点个赞👍,respect!
转载自:https://juejin.cn/post/7287907234716254249