Skip to content

Instantly share code, notes, and snippets.

@tjk
Last active April 25, 2023 16:28
Show Gist options
  • Save tjk/3564f1dd66a3c296f6090c5c971d9d7d to your computer and use it in GitHub Desktop.
Save tjk/3564f1dd66a3c296f6090c5c971d9d7d to your computer and use it in GitHub Desktop.
vue composable modal pattern (problems)
<template lang="pug">
div
//- <----- rest of app goes here (RouterView / main shell / w.e)
//- don't even need to use teleport with this pattern...
Modal(v-for="({ body, bodyProps, modalProps }, idx) in modalStack" v-bind="modalProps" @close="modalClose(idx)")
//- XXX support { title, h2Md }, etc. besides body+bodyProps --
Suspense(v-if="!idx && body")
Component(:is="body" v-bind="bodyProps")
</template>
<script setup lang="ts">
import { provideModal } from "./modal"
import Modal from "./modal.vue"
const { modalStack, modalClose } = provideModal() // stack is readonly -- this is the ProvideModalContext
</script>
<template lang="pug">
div {{ modalOneProp }}
</template>
<script setup lang="ts">
defineProps<{
modalOneProp: string
}>()
</script>
<template lang="pug">
div
button(@click="openModal") Open modal
</template>
<script setup lang="ts">
import { useModal } from "./modal"
import ModalOne from "./modal-one.vue"
// this composable maintains a stack of modals to show
const { modalPush } = useModal()
function openModal() {
// this composable api so that we can open modals from other composables
modalPush({
// i basically have this working except for 2 really bad issues:
// 1) cannot use `inject()` inside @components/modal/one.vue that is "in scope" of stuff provided 'above' this file
// 2) hmr is kinda busted
body: ModalOne,
bodyProps: {
modalOneProp: "hello world",
},
})
}
</script>
import {
type AllowedComponentProps,
type Component,
type Ref,
type VNodeProps,
} from "vue"
// https://stackoverflow.com/a/73784241/387413
type ComponentProps<C extends Component> = C extends new (...args: any) => any
? Omit<
InstanceType<C>["$props"],
keyof VNodeProps | keyof AllowedComponentProps
>
: never
type ModalPushOpts<K extends Component> = {
id: string
modalProps?: ComponentProps<typeof Modal>
body?: K
bodyProps?: ComponentProps<K>
}
export type ModalStack = ModalPushOpts<any>[]
const MODAL = Symbol("MODAL")
export function provideModal(): ModalContext {
const ctx = {
stack: reactive<ModalStack>([]),
}
provide(MODAL, ctx)
return {
modalStack: readonly(ctx.stack),
modalClose(idx: number) {
ctx.stack.splice(idx, 1)
},
}
}
export function useModal() {
const ctx = inject<ModalContext>(MODAL)
if (!ctx) {
throw new Error("must provideModal()")
}
return {
modalPush<K extends Component>(opts: ModalPushOpts<K>) {
if (ctx.stack[0]?.id !== opts.id) {
ctx.stack.unshift(markRaw(opts))
}
},
}
}
<template lang="pug">
.fixed.inset-0.flex.flex-col.items-center(style="background:rgba(0,0,0,0.4)" @click="$emit('close')")
.bg-white(@click.stop)
slot
</template>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment