Skip to content

Commit

Permalink
feat(type): support auto completion for the useModal slots with type …
Browse files Browse the repository at this point in the history
…ComponentSlots
  • Loading branch information
hunterliu1003 committed Dec 15, 2023
1 parent 70f5d79 commit 7db7267
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 43 deletions.
29 changes: 29 additions & 0 deletions packages/vue-final-modal/src/Component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* vue-component-type-helpers
* Copy from https://github.com/vuejs/language-tools/tree/master/packages/component-type-helpers
*/

// export type ComponentType<T> =
// T extends new () => {} ? 1 :
// T extends (...args: any) => any ? 2 :
// 0

export type ComponentProps<T> =
T extends new () => { $props: infer P } ? NonNullable<P> :
T extends (props: infer P, ...args: any) => any ? P :
{}

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> :
{}

// 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> :
// {}

// export type ComponentExposed<T> =
// T extends new () => infer E ? E :
// T extends (props: any, ctx: any, expose: (exposed: infer E) => any, ...args: any) => any ? NonNullable<E> :
// {}
30 changes: 8 additions & 22 deletions packages/vue-final-modal/src/Modal.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,23 @@
import type { App, CSSProperties, ComponentInternalInstance, FunctionalComponent, Raw, Ref } from 'vue'
import type { App, CSSProperties, Component, ComponentInternalInstance, FunctionalComponent, Raw, Ref } from 'vue'
import type { ComponentProps, ComponentSlots } from './Component'

export type ModalId = number | string | symbol
export type StyleValue = string | CSSProperties | (string | CSSProperties)[]

/** A fake Component Constructor that is only used for extracting `$props` as type `P` */
type Constructor<P = any> = {
__isFragment?: never
__isTeleport?: never
__isSuspense?: never
new(...args: any[]): { $props: P }
}

export interface ModalSlotOptions { component: Raw<ComponentType>; attrs?: Record<string, any> }
export type ModalSlot = string | ComponentType | ModalSlotOptions
export interface ModalSlotOptions { component: Raw<Component>; attrs?: Record<string, any> }
export type ModalSlot = string | Component | ModalSlotOptions

type ComponentConstructor = (abstract new (...args: any) => any)
/** Including both generic and non-generic vue components */
export type ComponentType = ComponentConstructor | FunctionalComponent<any, any>

type FunctionalComponentProps<T> = T extends FunctionalComponent<infer P> ? P : Record<any, any>
type NonGenericComponentProps<T> = T extends Constructor<infer P> ? P : Record<any, any>
export type ComponentProps<T extends ComponentType> =
T extends ComponentConstructor
? NonGenericComponentProps<T>
: FunctionalComponentProps<T>

export type UseModalOptions<T extends ComponentType> = {
export type UseModalOptions<T extends Component> = {
defaultModelValue?: boolean
keepAlive?: boolean
component?: T
attrs?: ComponentProps<T>
slots?: {
[key: string]: ModalSlot
[K in keyof ComponentSlots<T>]?: ModalSlot
}
}

Expand All @@ -42,7 +28,7 @@ export type UseModalOptionsPrivate = {
resolveClosed: () => void
}

export interface UseModalReturnType<T extends ComponentType> {
export interface UseModalReturnType<T extends Component> {
options: UseModalOptions<T> & UseModalOptionsPrivate
open: () => Promise<string>
close: () => Promise<string>
Expand All @@ -55,7 +41,7 @@ export type Vfm = {
modals: ComponentInternalInstance[]
openedModals: ComponentInternalInstance[]
openedModalOverlays: ComponentInternalInstance[]
dynamicModals: (UseModalOptions<any> & UseModalOptionsPrivate)[]
dynamicModals: (UseModalOptions<Component> & UseModalOptionsPrivate)[]
modalsContainers: Ref<symbol[]>
get: (modalId: ModalId) => undefined | ComponentInternalInstance
toggle: (modalId: ModalId, show?: boolean) => undefined | Promise<string>
Expand Down
12 changes: 7 additions & 5 deletions packages/vue-final-modal/src/components/ModalsContainer.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<script setup lang="ts">
import type { Component } from 'vue'
import { computed, onBeforeUnmount } from 'vue'
import type { ModalSlotOptions } from '..'
import { isString } from '~/utils'
import { useVfm } from '~/useApi'
import { isModalSlotOptions, useVfm } from '~/useApi'
const { modalsContainers, dynamicModals } = useVfm()
Expand Down Expand Up @@ -41,12 +43,12 @@ function resolvedOpened(index: number) {
<template v-for="(slot, key) in modal.slots" #[key] :key="key">
<div v-if="isString(slot)" v-html="slot" />
<component
:is="slot.component"
v-else-if="'component' in slot"
v-bind="slot.attrs"
:is="(slot as ModalSlotOptions).component"
v-else-if="isModalSlotOptions(slot)"
v-bind="(slot as ModalSlotOptions).attrs"
/>
<component
:is="slot"
:is="slot as Component"
v-else
/>
</template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ defineOptions({ inheritAttrs: false })
const instance = getCurrentInstance()
defineSlots<{
'default'(props: { close: () => boolean }): void
'swipe-banner'(): void
'default'?(props: { close: () => boolean }): void
'swipe-banner'?(): void
}>()
const { modals, openedModals, openedModalOverlays } = useVfm()
Expand Down
33 changes: 19 additions & 14 deletions packages/vue-final-modal/src/useApi.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { Component } from 'vue'
import { computed, markRaw, nextTick, reactive, useAttrs } from 'vue'
import { tryOnUnmounted } from '@vueuse/core'
import VueFinalModal from './components/VueFinalModal/VueFinalModal.vue'
import type { ComponentProps, ComponentType, ModalSlot, ModalSlotOptions, UseModalOptions, UseModalOptionsPrivate, UseModalReturnType, Vfm } from './Modal'
import type { ModalSlotOptions, UseModalOptions, UseModalOptionsPrivate, UseModalReturnType, Vfm } from './Modal'
import { activeVfm, getActiveVfm } from './plugin'
import { isString } from '~/utils'
import type { ComponentProps } from './Component'
import { isString, objectEntries } from '~/utils'

/**
* Returns the vfm instance. Equivalent to using `$vfm` inside
Expand All @@ -23,24 +25,24 @@ export function useVfm(): Vfm {
return vfm!
}

function withMarkRaw<T extends ComponentType>(options: Partial<UseModalOptions<T>>, DefaultComponent: ComponentType = VueFinalModal) {
function withMarkRaw<T extends Component>(options: Partial<UseModalOptions<T>>, DefaultComponent: Component = VueFinalModal) {
const { component, slots: innerSlots, ...rest } = options

const slots = typeof innerSlots === 'undefined'
const slots: UseModalOptions<T>['slots'] = typeof innerSlots === 'undefined'
? {}
: Object.fromEntries<ModalSlot>(Object.entries(innerSlots).map(([name, maybeComponent]) => {
: Object.fromEntries(objectEntries(innerSlots).map(([name, maybeComponent]) => {
if (isString(maybeComponent))
return [name, maybeComponent] as const

if ('component' in maybeComponent) {
if (isModalSlotOptions(maybeComponent)) {
return [name, {
...maybeComponent,
component: markRaw(maybeComponent.component),
}]
}

return [name, markRaw(maybeComponent)]
}))
return [name, markRaw(maybeComponent as Component)]
})) as UseModalOptions<T>['slots']

return {
...rest,
Expand All @@ -52,7 +54,7 @@ function withMarkRaw<T extends ComponentType>(options: Partial<UseModalOptions<T
/**
* Create a dynamic modal.
*/
export function useModal<T extends ComponentType = typeof VueFinalModal>(_options: UseModalOptions<T>): UseModalReturnType<T> {
export function useModal<T extends Component = typeof VueFinalModal>(_options: UseModalOptions<T>): UseModalReturnType<T> {
const options = reactive({
id: Symbol(__DEV__ ? 'useModal' : ''),
modelValue: !!_options?.defaultModelValue,
Expand Down Expand Up @@ -124,7 +126,7 @@ export function useModal<T extends ComponentType = typeof VueFinalModal>(_option

// patch options.slots
if (slots) {
Object.entries(slots).forEach(([name, slot]) => {
objectEntries(slots).forEach(([name, slot]) => {
const originSlot = options.slots![name]
if (isString(originSlot))
options.slots![name] = slot
Expand All @@ -136,7 +138,7 @@ export function useModal<T extends ComponentType = typeof VueFinalModal>(_option
}
}

function patchComponentOptions<T extends ComponentType>(
function patchComponentOptions<T extends Component>(
options: UseModalOptions<T> | ModalSlotOptions,
newOptions: Partial<UseModalOptions<T>> | ModalSlotOptions,
) {
Expand Down Expand Up @@ -171,15 +173,18 @@ export function useModal<T extends ComponentType = typeof VueFinalModal>(_option
}
}

export function useModalSlot<T extends ComponentType>(options: {
export function useModalSlot<T extends Component>(options: {
component: T
attrs?: ComponentProps<T>
}) {
return options
}

function isModalSlotOptions(value: any): value is ModalSlotOptions {
return 'component' in value || 'attrs' in value
export function isModalSlotOptions(value: unknown): value is ModalSlotOptions {
if (typeof value === 'object' && value !== null)
return 'component' in value
else
return false
}

export function pickModalProps(props: any, modalProps: any) {
Expand Down
8 changes: 8 additions & 0 deletions packages/vue-final-modal/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,11 @@ export function arrayRemoveItem<T>(arr: T[], item: T) {
if (index !== -1)
return arr.splice(index, 1)
}

type Entries<T> = { [K in keyof T]: [K, T[K]] }[keyof T][]
/**
* Type safe variant of `Object.entries()`
*/
export function objectEntries<T extends Record<any, any>>(object: T): Entries<T> {
return Object.entries(object) as any
}

0 comments on commit 7db7267

Please sign in to comment.