-
Notifications
You must be signed in to change notification settings - Fork 315
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature]: Calling dialog using vue hooks #748
Comments
Hello There! Implementation StepsInstall npm install vue-component-type-helpers dialog.ts /* eslint-disable ts/no-unsafe-function-type */
import type { App, Component, ComponentInternalInstance } from "vue"
import { createApp, h } from "vue"
import type { ComponentProps } from "vue-component-type-helpers"
export function useDialog<T extends Component>(component: T, parentApp: ComponentInternalInstance | null = getCurrentInstance()) {
const isOpen = ref(false)
const currentProps = shallowRef<Record<string, any>>({})
const okCallbacks: Function[] = []
const cancelCallbacks: Function[] = []
let instance: App<Element> | null = null
let htmlNode: HTMLDivElement | null = null
const api = {
isOpen: readonly(isOpen),
onOk,
onCancel,
update,
open,
close,
}
onUnmounted(() => close(), parentApp)
watch(isOpen, () => {
update({ open: isOpen.value })
if (!isOpen.value)
close()
})
function open(props?: ComponentProps<T>) {
isOpen.value = true
currentProps.value = { ...props, open: isOpen.value }
mount(component)
return api
}
function close() {
isOpen.value = false
const node = document.body.querySelector<HTMLDivElement>(`div[role="dialog"][data-state="open"]`)
if (node) {
node.addEventListener("animationend", destroy, { once: true })
}
else {
// Forcefully destory everything
destroy()
}
return api
}
function update(props: Record<string, any>) {
currentProps.value = { ...currentProps.value, ...props }
return api
}
function onOk(fn: (...args: any) => any) {
okCallbacks.push(fn)
return api
}
function onCancel(fn: (...args: any) => any) {
cancelCallbacks.push(fn)
return api
}
function destroy() {
instance?.unmount()
if (htmlNode)
window.document.body.removeChild(htmlNode)
instance = null
htmlNode = null
}
function mount(component: Component) {
htmlNode = window.document.createElement("div")
window.document.body.appendChild(htmlNode)
instance = createComponent(component)
instance.config.globalProperties = parentApp!.appContext.config.globalProperties
instance._context.directives = parentApp!.appContext.directives
instance._context.provides = parentApp!.appContext.provides
instance.mount(htmlNode)
return instance
}
function createComponent(component: Component) {
return createApp({
render() {
return h(component, {
"onUpdate:open": (open: boolean) => {
isOpen.value = open
},
"onOk": async (values: any) => {
for (const cb of okCallbacks)
await cb(values)
},
"onCancel": async (values: any) => {
for (const cb of cancelCallbacks)
await cb(values)
},
...currentProps.value,
})
},
})
}
return api
} MyDialog.vue <script lang="ts" setup>
const emit = defineEmits<{
ok: [number]
cancel: []
}>()
const open = defineModel<boolean>("open")
</script>
<template>
<UDialog v-model:open="open">
<UDialogContent>
<UDialogTitle>
My Dialog
</UDialogTitle>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Ipsum, doloribus nisi! Totam fugit, quidem perspiciatis harum quo labore doloremque iste quisquam magnam veritatis suscipit inventore facere voluptatibus, pariatur modi maiores!
<UButton @click="emit('ok', 123)">
Ok
</UButton>
<UButton
variant="destructive"
@click="emit('cancel')">
Cancel
</UButton>
</UDialogContent>
</UDialog>
</template> Usage, in app.vue <script setup lang="ts">
// Import `dialog.ts`
const dialog = useDialog()
dialog.open()
.onOk((payload) => {
console.log("OK with payload: ", payload)
dialog.close()
})
.onCancel(() => {
console.log("Cancel")
dialog.close()
})
</script>
<template>
<Button @click="dialog.open()">
Open Dialog
</Button>
</template> Explanation
There are some improvements that could be made, but i need this to be reviewed first. It requires a bit refinement, but we could add it as a reusable composable. |
This seems like a lot of unnecessary code. If you want to control opening and closing of dialogs manually, you can use this (it's what I use). It works with dialogs, sheets, popups, and any other component with open state. composable/useDialog.ts
Then in your Vue components:
Or with named props when you have multiple dialogs
|
Oh I understand, |
Hey guys, thanks for the input These sources might be helpful |
Thank you for your help. |
<script setup lang="ts">
import {Toaster} from '@/components/ui/sonner'
import {createTemplatePromise} from "@vueuse/core";
import {provide} from "vue";
type DialogResult = 'ok' | 'cancel'
const TemplatePromise = createTemplatePromise<DialogResult, [string]>({
transition: {
name: 'fade',
appear: true,
},
})
provide('templatePromise', TemplatePromise);
function asyncFn() {
return new Promise<DialogResult>((resolve) => {
setTimeout(() => {
resolve('ok')
}, 1000)
})
}
</script>
<template>
<RouterView></RouterView>
<Toaster class="pointer-events-auto"/>
<TemplatePromise v-slot="{ resolve, args, isResolving }">
<div class="fixed inset-0 bg-black/10 flex items-center z-30">
<dialog open class="border-gray/10 shadow rounded ma">
<div>Dialog {{ args[0] }}</div>
<p>Open console to see logs</p>
<div class="flex gap-2 justify-end">
<button class="w-35" @click="resolve('cancel')">
Cancel
</button>
<button class="w-35" :disabled="isResolving" @click="resolve(asyncFn())">
{{ isResolving ? 'Confirming...' : 'OK' }}
</button>
</div>
</dialog>
</div>
</TemplatePromise>
</template>
const templatePromise = inject('templatePromise'); |
This is also possible import {h, defineComponent, createApp} from 'vue';
import {createTemplatePromise} from "@vueuse/core";
type DialogResult = 'ok' | 'cancel';
export function useTemplatePromise() {
const TemplatePromise = createTemplatePromise<DialogResult, [string]>({
transition: {
name: 'fade',
appear: true,
},
});
type TemplatePromiseSlots = InstanceType<typeof TemplatePromise>['$slots'];
const DialogComponent = defineComponent({
setup() {
return () => h(TemplatePromise, {}, {
default: ({
resolve,
args
}: Parameters<TemplatePromiseSlots['default']>[0]) => h('div', {class: 'fixed inset-0 bg-black/10 flex items-center z-30'}, [
h('dialog', {open: true, class: 'border-gray/10 shadow rounded ma'}, [
h('div', `Dialog ${args ? args[0] : ''}`),
h('p', 'Open console to see logs'),
h('div', {class: 'flex gap-2 justify-end'}, [
h('button', {class: 'w-35', onClick: () => resolve('cancel')}, 'Cancel'),
h('button', {
class: 'w-35',
onClick: () => resolve(asyncFn())
}, 'OK')
])
])
])
});
}
});
function asyncFn() {
return new Promise<DialogResult>((resolve) => {
setTimeout(() => {
resolve('ok');
}, 1000);
});
}
const app = createApp(DialogComponent);
const containerId = 'dialog-container';
let container = document.getElementById(containerId);
if (!container) {
container = document.createElement('div');
container.id = containerId;
document.body.appendChild(container);
} else {
const existingApp = (container as any).__vue_app__;
if (existingApp) {
existingApp.unmount();
}
}
app.mount(container);
return {
start: TemplatePromise.start,
};
} |
for those who are looking the cleaner way, i've created myself a reuseable function
import type { VNode } from "vue";
export type DialogProps = {
title: string;
content: string | VNode;
okText?: string;
cancelText?: string;
};
const DialogTemplate = createTemplatePromise<boolean, [DialogProps]>();
export const useDialog = () => DialogTemplate;
<script setup lang="ts">
const DialogTemplate = useDialog();
const { t } = useI18n();
</script>
<template>
<DialogTemplate v-slot="{ resolve, args: [props] }">
<AlertDialog :open="true">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{{ props.title }}
</AlertDialogTitle>
<AlertDialogDescription>
<p v-if="typeof props.content === 'string'">
{{ props.content }}
</p>
<component v-else :is="props.content" />
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel @click="resolve(false)">
{{ props.cancelText ?? t("button.cancel") }}
</AlertDialogCancel>
<AlertDialogAction @click="resolve(true)">
{{ props.okText ?? t("button.ok") }}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</DialogTemplate>
</template>
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<AppDialogProvider />
</template>
<style lang="scss">
.page-enter-active,
.page-leave-active {
@apply transition-all duration-300 ease-in-out;
}
.page-enter-from {
@apply -translate-x-4 opacity-0;
}
.page-leave-to {
@apply translate-x-10 opacity-0;
}
.fade-enter-active,
.fade-leave-active {
@apply transition-all duration-300 ease-in-out;
}
.fade-enter-from,
.fade-leave-to {
@apply opacity-0;
}
</style> example of usage const dialog = useDialog();
const handleConfirmation = async () => {
const res = await dialog.start({
title: "Confirmation",
content: "Do you really want to delete the user?",
});
if (res) {
//do something
};
}; |
thanks guys you are so helpful |
Describe the feature
Calling dialog using vue hooks
Additional information
The text was updated successfully, but these errors were encountered: