Skip to content

Commit 7cecf80

Browse files
committed
refactor: extraxt DynamiModal component from ModalsContainer
1 parent 9af645e commit 7cecf80

File tree

8 files changed

+135
-118
lines changed

8 files changed

+135
-118
lines changed

docs/content/2.get-started/1.guide/4.types.md

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -63,26 +63,15 @@ export interface UseModalReturnType<T extends ComponentType> {
6363
```ts
6464
export type Vfm = {
6565
install(app: App): void
66-
modals: ComputedRef<Modal>[]
67-
openedModals: ComputedRef<Modal>[]
68-
openedModalOverlays: ComputedRef<Modal>[]
69-
dynamicModals: (UseModalOptions<any> & UseModalOptionsPrivate)[]
66+
modals: ComponentInternalInstance[]
67+
openedModals: ComponentInternalInstance[]
68+
openedModalOverlays: ComponentInternalInstance[]
69+
dynamicModals: VNode[]
7070
modalsContainers: Ref<symbol[]>
71-
get: (modalId: ModalId) => undefined | ComputedRef<Modal>
71+
get: (modalId: ModalId) => undefined | ComponentInternalInstance
7272
toggle: (modalId: ModalId, show?: boolean) => undefined | Promise<string>
7373
open: (modalId: ModalId) => undefined | Promise<string>
7474
close: (modalId: ModalId) => undefined | Promise<string>
75-
closeAll: () => Promise<[PromiseSettledResult<Promise<string>[]>]>
76-
}
77-
```
78-
79-
## Modal
80-
81-
```ts
82-
export type Modal = {
83-
modalId?: ModalId
84-
hideOverlay: Ref<boolean | undefined> | undefined
85-
overlayVisible: Ref<boolean>
86-
toggle: (show?: boolean) => Promise<string>
75+
closeAll: () => Promise<PromiseSettledResult<string>[]>
8776
}
8877
```

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

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

44
export type ModalId = number | string | symbol
@@ -41,7 +41,7 @@ export type Vfm = {
4141
modals: ComponentInternalInstance[]
4242
openedModals: ComponentInternalInstance[]
4343
openedModalOverlays: ComponentInternalInstance[]
44-
dynamicModals: (UseModalOptions<Component> & UseModalOptionsPrivate)[]
44+
dynamicModals: VNode[]
4545
modalsContainers: Ref<symbol[]>
4646
get: (modalId: ModalId) => undefined | ComponentInternalInstance
4747
toggle: (modalId: ModalId, show?: boolean) => undefined | Promise<string>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { Component, PropType } from 'vue'
2+
import { defineComponent, h } from 'vue'
3+
import type { ModalSlotOptions, UseModalOptions, UseModalOptionsPrivate } from '..'
4+
import { destroyVNode, isModalSlotOptions, useVfm } from '~/useApi'
5+
import { isString, objectEntries } from '~/utils'
6+
7+
export const DynamicModal = defineComponent({
8+
name: 'DynamicModal',
9+
props: {
10+
modal: {
11+
type: Object as PropType<UseModalOptions<Component> & UseModalOptionsPrivate>,
12+
required: true,
13+
},
14+
},
15+
setup(props) {
16+
const { dynamicModals } = useVfm()
17+
function renderDynamicModal(modal: (UseModalOptions<Component> & UseModalOptionsPrivate)) {
18+
if (!modal.component)
19+
return null
20+
const slots = objectEntries(modal.slots || {}).reduce((acc, cur) => {
21+
const slotName = cur[0] as string
22+
const slot = cur[1] as string | Component | ModalSlotOptions
23+
if (isString(slot))
24+
acc[slotName] = () => h('div', { innerHTML: slot })
25+
else if (isModalSlotOptions(slot))
26+
acc[slotName] = () => h(slot.component, slot.attrs)
27+
else
28+
acc[slotName] = () => h(slot)
29+
return acc
30+
}, {} as any)
31+
32+
return h(modal.component, {
33+
'modelValue': modal.modelValue,
34+
'displayDirective': modal?.keepAlive ? 'show' : undefined,
35+
...(typeof modal.attrs === 'object' ? modal.attrs : {}),
36+
'onUpdate:modelValue': (value: boolean) => {
37+
modal.modelValue = value
38+
const onUpdateModelValue = modal.attrs?.['onUpdate:modelValue']
39+
if (onUpdateModelValue)
40+
onUpdateModelValue(value)
41+
},
42+
'on_closed': () => {
43+
modal?.resolveClosed?.()
44+
if (!modal.keepAlive) {
45+
const vNode = dynamicModals.find(component => component.key === modal.id)
46+
if (!vNode)
47+
return
48+
destroyVNode(vNode)
49+
}
50+
},
51+
'on_opened': () => modal?.resolveOpened?.(),
52+
}, slots)
53+
}
54+
55+
return () => renderDynamicModal(props.modal)
56+
},
57+
})
Lines changed: 4 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import type { Component } from 'vue'
2-
import { computed, defineComponent, h, onBeforeUnmount } from 'vue'
3-
import type { ModalSlotOptions } from '..'
4-
import { isModalSlotOptions, useVfm } from '~/useApi'
5-
import { isString, objectEntries } from '~/utils'
1+
import { computed, defineComponent, onBeforeUnmount } from 'vue'
2+
import { useVfm } from '~/useApi'
63

74
export const ModalsContainer = defineComponent({
5+
name: 'ModalsContainer',
86
setup() {
97
const { modalsContainers, dynamicModals } = useVfm()
108

@@ -16,44 +14,11 @@ export const ModalsContainer = defineComponent({
1614
modalsContainers.value = modalsContainers.value.filter(i => i !== uid)
1715
})
1816

19-
function resolvedClosed(index: number) {
20-
dynamicModals[index]?.resolveClosed?.()
21-
if (!dynamicModals[index]?.keepAlive)
22-
dynamicModals.splice(index, 1)
23-
}
24-
25-
function resolvedOpened(index: number) {
26-
dynamicModals[index]?.resolveOpened?.()
27-
}
28-
2917
return () => {
3018
if (!shouldMount.value)
3119
return null
32-
return dynamicModals.map((modal, index) => {
33-
if (!modal.component)
34-
return null
35-
const slots = objectEntries(modal.slots || {}).reduce((acc, cur) => {
36-
const slotName = cur[0] as string
37-
const slot = cur[1] as string | Component | ModalSlotOptions
38-
if (isString(slot))
39-
acc[slotName] = () => h('div', { innerHTML: slot })
40-
else if (isModalSlotOptions(slot))
41-
acc[slotName] = () => h(slot.component, slot.attrs)
42-
else
43-
acc[slotName] = () => h(slot)
44-
return acc
45-
}, {} as any)
4620

47-
return h(modal.component, {
48-
'key': modal.id,
49-
'modelValue': modal.modelValue,
50-
'onUpdate:modelValue': (value: boolean) => modal.modelValue = value,
51-
'displayDirective': modal?.keepAlive ? 'show' : undefined,
52-
...(typeof modal.attrs === 'object' ? modal.attrs : {}),
53-
'on_closed': () => resolvedClosed(index),
54-
'on_opened': () => resolvedOpened(index),
55-
}, slots)
56-
})
21+
return dynamicModals
5722
}
5823
},
5924
})

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,6 @@ export default {
208208
:style="{ zIndex }"
209209
role="dialog"
210210
aria-modal="true"
211-
data-scroll-lock-scrollable
212211
@keydown.esc="() => onEsc()"
213212
@mouseup.self="() => onMouseupRoot()"
214213
@mousedown.self="e => onMousedown(e)"

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export function useModelValue(
2424
return _modelValueLocal.value
2525
},
2626
set value(val: boolean) {
27+
if (_modelValueLocal.value === val)
28+
return
2729
setModelValueLocal(val)
2830
},
2931
} as Ref<boolean>

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type { App, ComponentInternalInstance, ComputedRef } from 'vue'
1+
import type { App, ComponentInternalInstance, ComputedRef, VNode } from 'vue'
22
import { getCurrentInstance, inject, markRaw, ref, shallowReactive } from 'vue'
33
import { vfmSymbol } from './injectionSymbols'
4-
import type { ModalExposed, ModalId, UseModalOptions, UseModalOptionsPrivate, Vfm } from './Modal'
4+
import type { ModalExposed, ModalId, Vfm } from './Modal'
55
import { noop } from './utils'
66

77
// eslint-disable-next-line import/no-mutable-exports
@@ -31,7 +31,7 @@ export function createVfm() {
3131
const modals: ComponentInternalInstance[] = shallowReactive([])
3232
const openedModals: ComponentInternalInstance[] = shallowReactive([])
3333
const openedModalOverlays: ComponentInternalInstance[] = shallowReactive([])
34-
const dynamicModals: (UseModalOptions<any> & UseModalOptionsPrivate)[] = shallowReactive([])
34+
const dynamicModals: VNode[] = shallowReactive([])
3535
const modalsContainers = ref<symbol[]>([])
3636

3737
const vfm: Vfm = markRaw({

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

Lines changed: 61 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import type { Component } from 'vue'
2-
import { computed, markRaw, nextTick, reactive, useAttrs } from 'vue'
1+
import type { Component, VNode } from 'vue'
2+
import { computed, h, markRaw, nextTick, reactive, useAttrs } from 'vue'
33
import { tryOnUnmounted } from '@vueuse/core'
44
import VueFinalModal from './components/VueFinalModal/VueFinalModal.vue'
55
import type { ModalSlotOptions, UseModalOptions, UseModalOptionsPrivate, UseModalReturnType, Vfm } from './Modal'
66
import { activeVfm, getActiveVfm } from './plugin'
77
import type { ComponentEmit, ComponentProps } from './Component'
8+
import { DynamicModal } from './components/DynamicModal'
89
import { isString, objectEntries } from '~/utils'
910

1011
/**
@@ -55,48 +56,35 @@ function withMarkRaw<T extends Component>(options: Partial<UseModalOptions<T>>,
5556
* Create a dynamic modal.
5657
*/
5758
export function useModal<T extends Component = typeof VueFinalModal>(_options: UseModalOptions<T>): UseModalReturnType<T> {
59+
const id = Symbol(__DEV__ ? 'useModal' : '')
60+
5861
const options = reactive({
59-
id: Symbol(__DEV__ ? 'useModal' : ''),
62+
id,
6063
modelValue: !!_options?.defaultModelValue,
6164
resolveOpened: () => { },
6265
resolveClosed: () => { },
6366
attrs: {},
6467
...withMarkRaw<T>(_options),
6568
}) as UseModalOptions<T> & UseModalOptionsPrivate
69+
70+
const vNode = h(DynamicModal, { modal: options, key: id })
71+
6672
tryOnUnmounted(() => {
67-
if (!options?.keepAlive)
68-
destroy()
73+
if (options?.keepAlive)
74+
return
75+
destroyVNode(vNode)
6976
})
7077

71-
if (options.modelValue === true) {
72-
// nextTick will break the SSR, so use `activeVfm` first and then `useVfm()`
73-
if (activeVfm) {
74-
activeVfm?.dynamicModals.push(options)
75-
}
76-
else {
77-
nextTick(() => {
78-
const vfm = useVfm()
79-
vfm?.dynamicModals.push(options)
80-
})
81-
}
82-
}
78+
if (options.modelValue === true)
79+
pushVNode(vNode)
8380

84-
async function open(): Promise<string> {
85-
// nextTick will break the SSR, so use `activeVfm` first and then `useVfm()`
86-
let vfm: Vfm
87-
if (activeVfm) {
88-
vfm = activeVfm
89-
}
90-
else {
91-
await nextTick()
92-
vfm = useVfm()
93-
}
81+
function open(): Promise<string> {
9482
if (options.modelValue)
9583
return Promise.resolve('[Vue Final Modal] modal is already opened.')
9684

97-
destroy()
85+
destroyVNode(vNode)
9886
options.modelValue = true
99-
vfm.dynamicModals.push(options)
87+
pushVNode(vNode)
10088

10189
return new Promise((resolve) => {
10290
options.resolveOpened = () => resolve('opened')
@@ -138,41 +126,58 @@ export function useModal<T extends Component = typeof VueFinalModal>(_options: U
138126
}
139127
}
140128

141-
function patchComponentOptions<T extends Component>(
142-
options: UseModalOptions<T> | ModalSlotOptions,
143-
newOptions: Partial<UseModalOptions<T>> | ModalSlotOptions,
144-
) {
145-
if (newOptions.component)
146-
options.component = newOptions.component
147-
148-
if (newOptions.attrs)
149-
patchAttrs(options.attrs!, newOptions.attrs)
150-
}
151-
152-
function patchAttrs<T extends Record<string, any>>(attrs: T, newAttrs: Partial<T>): T {
153-
Object.entries(newAttrs).forEach(([key, value]) => {
154-
attrs[key as keyof T] = value as any
155-
})
156-
157-
return attrs
158-
}
159-
160-
function destroy(): void {
161-
const vfm = useVfm()
162-
const index = vfm.dynamicModals.indexOf(options)
163-
if (index !== -1)
164-
vfm.dynamicModals.splice(index, 1)
165-
}
166-
167129
return {
168130
options,
169131
open,
170132
close,
171133
patchOptions,
172-
destroy,
134+
destroy: () => destroyVNode(vNode),
173135
}
174136
}
175137

138+
function patchComponentOptions<T extends Component>(
139+
options: UseModalOptions<T> | ModalSlotOptions,
140+
newOptions: Partial<UseModalOptions<T>> | ModalSlotOptions,
141+
) {
142+
if (newOptions.component)
143+
options.component = newOptions.component
144+
145+
if (newOptions.attrs)
146+
patchAttrs(options.attrs!, newOptions.attrs)
147+
}
148+
149+
function patchAttrs<T extends Record<string, any>>(attrs: T, newAttrs: Partial<T>): T {
150+
Object.entries(newAttrs).forEach(([key, value]) => {
151+
attrs[key as keyof T] = value as any
152+
})
153+
154+
return attrs
155+
}
156+
157+
async function pushVNode(vNode: VNode) {
158+
const vfm = await useSsrVfm()
159+
vfm?.dynamicModals.push(vNode)
160+
}
161+
162+
/** nextTick will break the SSR, so use `activeVfm` first and then `useVfm()` */
163+
async function useSsrVfm(): Promise<Vfm> {
164+
if (activeVfm) {
165+
return activeVfm
166+
}
167+
else {
168+
await nextTick()
169+
return useVfm()
170+
}
171+
}
172+
173+
export function destroyVNode(vNode: VNode): void {
174+
const vfm = useVfm()
175+
176+
const index = vfm?.dynamicModals.indexOf(vNode)
177+
if (index !== undefined && index !== -1)
178+
vfm?.dynamicModals.splice(index, 1)
179+
}
180+
176181
export function useModalSlot<T extends Component>(options: {
177182
component: T
178183
attrs?: ComponentProps<T>

0 commit comments

Comments
 (0)