Skip to content

Commit 7db7267

Browse files
committed
feat(type): support auto completion for the useModal slots with type ComponentSlots
1 parent 70f5d79 commit 7db7267

File tree

6 files changed

+73
-43
lines changed

6 files changed

+73
-43
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* vue-component-type-helpers
3+
* Copy from https://github.com/vuejs/language-tools/tree/master/packages/component-type-helpers
4+
*/
5+
6+
// export type ComponentType<T> =
7+
// T extends new () => {} ? 1 :
8+
// T extends (...args: any) => any ? 2 :
9+
// 0
10+
11+
export type ComponentProps<T> =
12+
T extends new () => { $props: infer P } ? NonNullable<P> :
13+
T extends (props: infer P, ...args: any) => any ? P :
14+
{}
15+
16+
export type ComponentSlots<T> =
17+
T extends new () => { $slots: infer S } ? NonNullable<S> :
18+
T extends (props: any, ctx: { slots: infer S; attrs: any; emit: any }, ...args: any) => any ? NonNullable<S> :
19+
{}
20+
21+
// export type ComponentEmit<T> =
22+
// T extends new () => { $emit: infer E } ? NonNullable<E> :
23+
// T extends (props: any, ctx: { slots: any; attrs: any; emit: infer E }, ...args: any) => any ? NonNullable<E> :
24+
// {}
25+
26+
// export type ComponentExposed<T> =
27+
// T extends new () => infer E ? E :
28+
// T extends (props: any, ctx: any, expose: (exposed: infer E) => any, ...args: any) => any ? NonNullable<E> :
29+
// {}

packages/vue-final-modal/src/Modal.ts

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,23 @@
1-
import type { App, CSSProperties, ComponentInternalInstance, FunctionalComponent, Raw, Ref } from 'vue'
1+
import type { App, CSSProperties, Component, ComponentInternalInstance, FunctionalComponent, Raw, Ref } from 'vue'
2+
import type { ComponentProps, ComponentSlots } from './Component'
23

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

6-
/** A fake Component Constructor that is only used for extracting `$props` as type `P` */
7-
type Constructor<P = any> = {
8-
__isFragment?: never
9-
__isTeleport?: never
10-
__isSuspense?: never
11-
new(...args: any[]): { $props: P }
12-
}
13-
14-
export interface ModalSlotOptions { component: Raw<ComponentType>; attrs?: Record<string, any> }
15-
export type ModalSlot = string | ComponentType | ModalSlotOptions
7+
export interface ModalSlotOptions { component: Raw<Component>; attrs?: Record<string, any> }
8+
export type ModalSlot = string | Component | ModalSlotOptions
169

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

21-
type FunctionalComponentProps<T> = T extends FunctionalComponent<infer P> ? P : Record<any, any>
22-
type NonGenericComponentProps<T> = T extends Constructor<infer P> ? P : Record<any, any>
23-
export type ComponentProps<T extends ComponentType> =
24-
T extends ComponentConstructor
25-
? NonGenericComponentProps<T>
26-
: FunctionalComponentProps<T>
27-
28-
export type UseModalOptions<T extends ComponentType> = {
14+
export type UseModalOptions<T extends Component> = {
2915
defaultModelValue?: boolean
3016
keepAlive?: boolean
3117
component?: T
3218
attrs?: ComponentProps<T>
3319
slots?: {
34-
[key: string]: ModalSlot
20+
[K in keyof ComponentSlots<T>]?: ModalSlot
3521
}
3622
}
3723

@@ -42,7 +28,7 @@ export type UseModalOptionsPrivate = {
4228
resolveClosed: () => void
4329
}
4430

45-
export interface UseModalReturnType<T extends ComponentType> {
31+
export interface UseModalReturnType<T extends Component> {
4632
options: UseModalOptions<T> & UseModalOptionsPrivate
4733
open: () => Promise<string>
4834
close: () => Promise<string>
@@ -55,7 +41,7 @@ export type Vfm = {
5541
modals: ComponentInternalInstance[]
5642
openedModals: ComponentInternalInstance[]
5743
openedModalOverlays: ComponentInternalInstance[]
58-
dynamicModals: (UseModalOptions<any> & UseModalOptionsPrivate)[]
44+
dynamicModals: (UseModalOptions<Component> & UseModalOptionsPrivate)[]
5945
modalsContainers: Ref<symbol[]>
6046
get: (modalId: ModalId) => undefined | ComponentInternalInstance
6147
toggle: (modalId: ModalId, show?: boolean) => undefined | Promise<string>

packages/vue-final-modal/src/components/ModalsContainer.vue

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
<script setup lang="ts">
2+
import type { Component } from 'vue'
23
import { computed, onBeforeUnmount } from 'vue'
4+
import type { ModalSlotOptions } from '..'
35
import { isString } from '~/utils'
4-
import { useVfm } from '~/useApi'
6+
import { isModalSlotOptions, useVfm } from '~/useApi'
57
68
const { modalsContainers, dynamicModals } = useVfm()
79
@@ -41,12 +43,12 @@ function resolvedOpened(index: number) {
4143
<template v-for="(slot, key) in modal.slots" #[key] :key="key">
4244
<div v-if="isString(slot)" v-html="slot" />
4345
<component
44-
:is="slot.component"
45-
v-else-if="'component' in slot"
46-
v-bind="slot.attrs"
46+
:is="(slot as ModalSlotOptions).component"
47+
v-else-if="isModalSlotOptions(slot)"
48+
v-bind="(slot as ModalSlotOptions).attrs"
4749
/>
4850
<component
49-
:is="slot"
51+
:is="slot as Component"
5052
v-else
5153
/>
5254
</template>

packages/vue-final-modal/src/components/VueFinalModal/VueFinalModal.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ defineOptions({ inheritAttrs: false })
3535
const instance = getCurrentInstance()
3636
3737
defineSlots<{
38-
'default'(props: { close: () => boolean }): void
39-
'swipe-banner'(): void
38+
'default'?(props: { close: () => boolean }): void
39+
'swipe-banner'?(): void
4040
}>()
4141
4242
const { modals, openedModals, openedModalOverlays } = useVfm()

packages/vue-final-modal/src/useApi.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import type { Component } from 'vue'
12
import { computed, markRaw, nextTick, reactive, useAttrs } from 'vue'
23
import { tryOnUnmounted } from '@vueuse/core'
34
import VueFinalModal from './components/VueFinalModal/VueFinalModal.vue'
4-
import type { ComponentProps, ComponentType, ModalSlot, ModalSlotOptions, UseModalOptions, UseModalOptionsPrivate, UseModalReturnType, Vfm } from './Modal'
5+
import type { ModalSlotOptions, UseModalOptions, UseModalOptionsPrivate, UseModalReturnType, Vfm } from './Modal'
56
import { activeVfm, getActiveVfm } from './plugin'
6-
import { isString } from '~/utils'
7+
import type { ComponentProps } from './Component'
8+
import { isString, objectEntries } from '~/utils'
79

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

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

29-
const slots = typeof innerSlots === 'undefined'
31+
const slots: UseModalOptions<T>['slots'] = typeof innerSlots === 'undefined'
3032
? {}
31-
: Object.fromEntries<ModalSlot>(Object.entries(innerSlots).map(([name, maybeComponent]) => {
33+
: Object.fromEntries(objectEntries(innerSlots).map(([name, maybeComponent]) => {
3234
if (isString(maybeComponent))
3335
return [name, maybeComponent] as const
3436

35-
if ('component' in maybeComponent) {
37+
if (isModalSlotOptions(maybeComponent)) {
3638
return [name, {
3739
...maybeComponent,
3840
component: markRaw(maybeComponent.component),
3941
}]
4042
}
4143

42-
return [name, markRaw(maybeComponent)]
43-
}))
44+
return [name, markRaw(maybeComponent as Component)]
45+
})) as UseModalOptions<T>['slots']
4446

4547
return {
4648
...rest,
@@ -52,7 +54,7 @@ function withMarkRaw<T extends ComponentType>(options: Partial<UseModalOptions<T
5254
/**
5355
* Create a dynamic modal.
5456
*/
55-
export function useModal<T extends ComponentType = typeof VueFinalModal>(_options: UseModalOptions<T>): UseModalReturnType<T> {
57+
export function useModal<T extends Component = typeof VueFinalModal>(_options: UseModalOptions<T>): UseModalReturnType<T> {
5658
const options = reactive({
5759
id: Symbol(__DEV__ ? 'useModal' : ''),
5860
modelValue: !!_options?.defaultModelValue,
@@ -124,7 +126,7 @@ export function useModal<T extends ComponentType = typeof VueFinalModal>(_option
124126

125127
// patch options.slots
126128
if (slots) {
127-
Object.entries(slots).forEach(([name, slot]) => {
129+
objectEntries(slots).forEach(([name, slot]) => {
128130
const originSlot = options.slots![name]
129131
if (isString(originSlot))
130132
options.slots![name] = slot
@@ -136,7 +138,7 @@ export function useModal<T extends ComponentType = typeof VueFinalModal>(_option
136138
}
137139
}
138140

139-
function patchComponentOptions<T extends ComponentType>(
141+
function patchComponentOptions<T extends Component>(
140142
options: UseModalOptions<T> | ModalSlotOptions,
141143
newOptions: Partial<UseModalOptions<T>> | ModalSlotOptions,
142144
) {
@@ -171,15 +173,18 @@ export function useModal<T extends ComponentType = typeof VueFinalModal>(_option
171173
}
172174
}
173175

174-
export function useModalSlot<T extends ComponentType>(options: {
176+
export function useModalSlot<T extends Component>(options: {
175177
component: T
176178
attrs?: ComponentProps<T>
177179
}) {
178180
return options
179181
}
180182

181-
function isModalSlotOptions(value: any): value is ModalSlotOptions {
182-
return 'component' in value || 'attrs' in value
183+
export function isModalSlotOptions(value: unknown): value is ModalSlotOptions {
184+
if (typeof value === 'object' && value !== null)
185+
return 'component' in value
186+
else
187+
return false
183188
}
184189

185190
export function pickModalProps(props: any, modalProps: any) {

packages/vue-final-modal/src/utils.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,11 @@ export function arrayRemoveItem<T>(arr: T[], item: T) {
3131
if (index !== -1)
3232
return arr.splice(index, 1)
3333
}
34+
35+
type Entries<T> = { [K in keyof T]: [K, T[K]] }[keyof T][]
36+
/**
37+
* Type safe variant of `Object.entries()`
38+
*/
39+
export function objectEntries<T extends Record<any, any>>(object: T): Entries<T> {
40+
return Object.entries(object) as any
41+
}

0 commit comments

Comments
 (0)