mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-14 12:14:41 +01:00
feat(Modal): open programmatically (#78)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
18
playground/components/ModalProgrammaticExample.vue
Normal file
18
playground/components/ModalProgrammaticExample.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
const modal = useModal()
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
count: number
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UModal title="This modal was opened programmatically">
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex w-full justify-between">
|
||||||
|
<p>Count: {{ count }}</p>
|
||||||
|
<UButton color="gray" label="Close" @click="modal.close()" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UModal>
|
||||||
|
</template>
|
||||||
@@ -1,5 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { LazyModalProgrammaticExample } from '#components'
|
||||||
|
|
||||||
const open = ref(false)
|
const open = ref(false)
|
||||||
|
|
||||||
|
const modal = useModal()
|
||||||
|
const count = ref(0)
|
||||||
|
const openModal = () => {
|
||||||
|
count.value++
|
||||||
|
modal.open(LazyModalProgrammaticExample, {
|
||||||
|
description: 'And you can even provide a description !',
|
||||||
|
count: count.value
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -49,5 +61,7 @@ const open = ref(false)
|
|||||||
<UModal title="Modal with custom close button" description="The `close` prop inherits from the Button props." :close="{ color: 'primary', variant: 'solid', size: 'xs' }" :ui="{ close: 'top-3.5 rounded-full' }">
|
<UModal title="Modal with custom close button" description="The `close` prop inherits from the Button props." :close="{ color: 'primary', variant: 'solid', size: 'xs' }" :ui="{ close: 'top-3.5 rounded-full' }">
|
||||||
<UButton label="Open with custom close button" color="gray" />
|
<UButton label="Open with custom close button" color="gray" />
|
||||||
</UModal>
|
</UModal>
|
||||||
|
|
||||||
|
<UButton label="Open programmatically" color="white" @click="openModal" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ export default defineNuxtModule<ModuleOptions>({
|
|||||||
addPlugin({
|
addPlugin({
|
||||||
src: resolve('./runtime/plugins/colors')
|
src: resolve('./runtime/plugins/colors')
|
||||||
})
|
})
|
||||||
|
addPlugin({
|
||||||
|
src: resolve('./runtime/plugins/modal')
|
||||||
|
})
|
||||||
|
|
||||||
addComponentsDir({
|
addComponentsDir({
|
||||||
path: resolve('./runtime/components'),
|
path: resolve('./runtime/components'),
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { toRef } from 'vue'
|
|||||||
import { ConfigProvider, TooltipProvider, useForwardProps } from 'radix-vue'
|
import { ConfigProvider, TooltipProvider, useForwardProps } from 'radix-vue'
|
||||||
import { reactivePick } from '@vueuse/core'
|
import { reactivePick } from '@vueuse/core'
|
||||||
import { useId } from '#imports'
|
import { useId } from '#imports'
|
||||||
import { UToaster } from '#components'
|
import { UToaster, UModalProvider } from '#components'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<ProviderProps>(), {
|
const props = withDefaults(defineProps<ProviderProps>(), {
|
||||||
useId: () => useId()
|
useId: () => useId()
|
||||||
@@ -31,5 +31,7 @@ const toasterProps = toRef(() => props.toaster)
|
|||||||
<slot />
|
<slot />
|
||||||
</UToaster>
|
</UToaster>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<UModalProvider />
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
12
src/runtime/components/ModalProvider.vue
Normal file
12
src/runtime/components/ModalProvider.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { inject } from 'vue'
|
||||||
|
import { useModal, modalInjectionKey } from '../composables/useModal'
|
||||||
|
|
||||||
|
const modalState = inject(modalInjectionKey)
|
||||||
|
|
||||||
|
const { isOpen } = useModal()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component :is="modalState.component" v-if="modalState" v-bind="modalState.props" v-model:open="isOpen" />
|
||||||
|
</template>
|
||||||
71
src/runtime/composables/useModal.ts
Normal file
71
src/runtime/composables/useModal.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { ref, inject } from 'vue'
|
||||||
|
import type { ShallowRef, Component, InjectionKey } from 'vue'
|
||||||
|
import { createSharedComposable } from '@vueuse/core'
|
||||||
|
import type { ModalProps } from '#ui/types'
|
||||||
|
import type { ComponentProps } from '#ui/types/component'
|
||||||
|
|
||||||
|
export interface ModalState {
|
||||||
|
component: Component | string
|
||||||
|
props: ModalProps
|
||||||
|
}
|
||||||
|
|
||||||
|
export const modalInjectionKey: InjectionKey<ShallowRef<ModalState>> = Symbol('nuxt-ui.modal')
|
||||||
|
|
||||||
|
function _useModal() {
|
||||||
|
const modalState = inject(modalInjectionKey)
|
||||||
|
|
||||||
|
const isOpen = ref(false)
|
||||||
|
|
||||||
|
function open<T extends Component>(component: T, props?: ModalProps & ComponentProps<T>) {
|
||||||
|
if (!modalState) {
|
||||||
|
throw new Error('useModal() is called without provider')
|
||||||
|
}
|
||||||
|
|
||||||
|
modalState.value = {
|
||||||
|
component,
|
||||||
|
props: props ?? {}
|
||||||
|
}
|
||||||
|
|
||||||
|
isOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function close() {
|
||||||
|
if (!modalState) return
|
||||||
|
|
||||||
|
isOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
if (!modalState) return
|
||||||
|
|
||||||
|
modalState.value = {
|
||||||
|
component: 'div',
|
||||||
|
props: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows updating the modal props
|
||||||
|
*/
|
||||||
|
function patch<T extends Component = Record<string, never>>(props: Partial<ModalProps & ComponentProps<T>>) {
|
||||||
|
if (!modalState) return
|
||||||
|
|
||||||
|
modalState.value = {
|
||||||
|
...modalState.value,
|
||||||
|
props: {
|
||||||
|
...modalState.value.props,
|
||||||
|
...props
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
reset,
|
||||||
|
patch,
|
||||||
|
isOpen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useModal = createSharedComposable(_useModal)
|
||||||
11
src/runtime/plugins/modal.ts
Normal file
11
src/runtime/plugins/modal.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { shallowRef } from 'vue'
|
||||||
|
import { defineNuxtPlugin } from '#imports'
|
||||||
|
import { modalInjectionKey, type ModalState } from '../composables/useModal'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
const modalState = shallowRef<ModalState>({
|
||||||
|
component: 'div',
|
||||||
|
props: {}
|
||||||
|
})
|
||||||
|
nuxtApp.vueApp.provide(modalInjectionKey, modalState)
|
||||||
|
})
|
||||||
14
src/runtime/types/component.d.ts
vendored
Normal file
14
src/runtime/types/component.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export type ComponentProps<T> =
|
||||||
|
T extends new () => { $props: infer P } ? NonNullable<P> :
|
||||||
|
T extends (props: infer P, ...args: any) => any ? P :
|
||||||
|
Record<string, never>
|
||||||
|
|
||||||
|
export type ComponentSlots<T> =
|
||||||
|
T extends new () => { $slots: infer S } ? NonNullable<S> :
|
||||||
|
T extends (props: any, ctx: { slots: infer S, attrs: any, emit: any }, ...args: any) => any ? NonNullable<S> :
|
||||||
|
Record<string, never>
|
||||||
|
|
||||||
|
export type ComponentEmit<T> =
|
||||||
|
T extends new () => { $emit: infer E } ? NonNullable<E> :
|
||||||
|
T extends (props: any, ctx: { slots: any, attrs: any, emit: infer E }, ...args: any) => any ? NonNullable<E> :
|
||||||
|
Record<string, never>
|
||||||
Reference in New Issue
Block a user