Skip to content
This repository was archived by the owner on Mar 27, 2025. It is now read-only.

Commit 0de3e78

Browse files
authored
add useModal
feat: Add useModal() and useModalController() docs: Add useModal() and useModalController() to modal.md
1 parent 7e049cf commit 0de3e78

File tree

10 files changed

+183
-14
lines changed

10 files changed

+183
-14
lines changed

apps/docs/src/docs/components/modal.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,75 @@ const nestedModal1 = ref(false)
138138
const nestedModal2 = ref(false)
139139
const nestedModal3 = ref(false)
140140
</script>
141+
142+
## Programmatically Control
143+
144+
### `useModal()`
145+
146+
You can use `useModal()` to get the closest modal in child component and hide it.
147+
148+
```vue
149+
<BModal>
150+
<MyComponent>
151+
</BModal>
152+
```
153+
154+
```vue
155+
<template>
156+
<BButton @click="hideSelf">Done</BButton>
157+
</template>
158+
159+
<script setup lang="ts">
160+
import {useModal} from 'bootstrap-vue-next'
161+
162+
const {hide} = useModal()
163+
164+
function hideSelf() {
165+
hide()
166+
}
167+
</script>
168+
```
169+
170+
You can also provide an id to get particular modal and show/hide it. Currently, we don't support using CSS selector to
171+
find modal since the `BModal` in lazy mode may not render at page initial.
172+
173+
```vue
174+
<template>
175+
<BModal v-if="someConditions" id="my-modal"> ...</BModal>
176+
</template>
177+
178+
<script setup lang="ts">
179+
import {useModal} from 'bootstrap-vue-next'
180+
import {ref} from 'vue'
181+
182+
const someConditions = ref(...)
183+
184+
const {show, hide, modal} = useModal('my-modal')
185+
186+
// modal variable is BModal component ref
187+
if (modal.value) {
188+
show()
189+
hide()
190+
191+
modal.value.show()
192+
modal.value.hide()
193+
} else {
194+
// If modal component not exists, you can still call show/hide methods but nothing happened
195+
show()
196+
hide()
197+
}
198+
</script>
199+
```
200+
201+
### `useModalController()`
202+
203+
`modalController` can hide modals everywhere.
204+
205+
```ts
206+
import {useModalController} from 'bootstrap-vue-next'
207+
208+
const modalController = useModalController()
209+
210+
modalController.hide() // Hide last modal
211+
modalController.hideAll() // Hide all modals at once
212+
```

packages/bootstrap-vue-next/src/components/BModal.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@ const sharedSlots: SharedSlotsData = reactive({
496496
defineExpose({
497497
hide,
498498
show: showFn,
499+
id: computedId.value,
499500
})
500501
</script>
501502

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export {useBreadcrumb, useColorMode} from '..'
1+
export {useBreadcrumb, useColorMode, useModal, useModalController} from '..'

packages/bootstrap-vue-next/src/composables/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export {
1414
export {default as useFormInput} from './useFormInput'
1515
export {default as normalizeOptions} from './useFormSelect'
1616
export {default as useId} from './useId'
17+
export {default as useModal} from './useModal'
18+
export {default as useModalController} from './useModalController'
1719
export {default as useModalManager} from './useModalManager'
1820
export {default as useSafeScrollLock} from './useSafeScrollLock'
1921
export {default as useStateClass} from './useStateClass'
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import {type ComponentInternalInstance, computed, getCurrentInstance} from 'vue'
2+
import {useSharedModalStack} from './useModalManager'
3+
4+
export default (id: string | undefined = undefined) => {
5+
const {find} = useSharedModalStack()
6+
const instance = getCurrentInstance()
7+
8+
const modalComponent = computed(() => {
9+
if (id) {
10+
return find(id)
11+
}
12+
13+
if (!instance) {
14+
return null
15+
}
16+
17+
return findBModal(instance)
18+
})
19+
20+
const modal = computed(() => modalComponent.value?.proxy)
21+
22+
return {
23+
show(): void {
24+
modalComponent.value?.exposed?.show()
25+
},
26+
hide(trigger = ''): void {
27+
modalComponent.value?.exposed?.hide(trigger)
28+
},
29+
modal,
30+
}
31+
}
32+
33+
function findBModal(component: ComponentInternalInstance): ComponentInternalInstance | null {
34+
if (!component.parent) {
35+
return null
36+
}
37+
38+
if (component.parent.type.__name === 'BModal') {
39+
return component.parent
40+
}
41+
42+
return findBModal(component.parent)
43+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type {BModalController} from '../types'
2+
import {useSharedModalStack} from './useModalManager'
3+
4+
export default (): BModalController => {
5+
const {last, stack} = useSharedModalStack()
6+
7+
const hide = (trigger = '') => {
8+
if (last.value) {
9+
last.value.exposed?.hide(trigger)
10+
}
11+
}
12+
13+
const hideAll = (trigger = '') => {
14+
const modals = stack.value.reverse()
15+
16+
for (const modal of modals) {
17+
modal.exposed?.hide(trigger)
18+
}
19+
}
20+
21+
return {
22+
hide,
23+
hideAll,
24+
25+
// Todo: Supports listening events globally in the future
26+
}
27+
}

packages/bootstrap-vue-next/src/composables/useModalManager.ts

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
1+
import {createSharedComposable, getSSRHandler, tryOnScopeDispose, unrefElement} from '@vueuse/core'
12
import {
2-
createSharedComposable,
3-
getSSRHandler,
4-
tryOnScopeDispose,
5-
unrefElement,
6-
useCounter,
7-
} from '@vueuse/core'
8-
import {type Ref, watch} from 'vue'
3+
type ComponentInternalInstance,
4+
computed,
5+
getCurrentInstance,
6+
ref,
7+
type Ref,
8+
watch,
9+
} from 'vue'
910

1011
const MODAL_OPEN_CLASS_NAME = 'modal-open'
1112

12-
const useSharedModalCounter = createSharedComposable(() => {
13-
const {count, inc, dec} = useCounter()
13+
export const useSharedModalStack = createSharedComposable(() => {
14+
const registry: Ref<ComponentInternalInstance[]> = ref([])
15+
const stack: Ref<ComponentInternalInstance[]> = ref([])
16+
const count = computed(() => stack.value.length)
17+
const last = computed(() => stack.value[stack.value.length - 1])
18+
const push = (modal: ComponentInternalInstance) => stack.value.push(modal)
19+
const pop = () => stack.value.pop()
20+
const remove = (modal: ComponentInternalInstance): void => {
21+
stack.value = stack.value.filter((item) => item.uid !== modal.uid)
22+
}
23+
const find = (id: string) => registry.value.find((modal) => modal.exposed!.id === id) || null
1424

1525
const updateHTMLAttrs = getSSRHandler('updateHTMLAttrs', (selector, attribute, value) => {
1626
const el =
@@ -34,19 +44,27 @@ const useSharedModalCounter = createSharedComposable(() => {
3444
updateHTMLAttrs('body', 'class', newValue > 0 ? MODAL_OPEN_CLASS_NAME : '')
3545
})
3646

37-
return {inc, dec}
47+
return {registry, stack, last, count, push, pop, remove, find}
3848
})
3949

4050
export default (modalOpen: Ref<boolean>): void => {
41-
const {inc, dec} = useSharedModalCounter()
51+
const {registry, push, remove, stack} = useSharedModalStack()
52+
53+
const currentModal = getCurrentInstance()
54+
55+
if (!currentModal || currentModal.type.__name !== 'BModal') {
56+
throw new Error('useModalManager must only use in BModal component')
57+
}
58+
59+
registry.value.push(currentModal)
4260

4361
watch(
4462
modalOpen,
4563
(newValue, oldValue) => {
4664
if (newValue) {
47-
inc()
65+
push(currentModal)
4866
} else if (oldValue && !newValue) {
49-
dec()
67+
remove(currentModal)
5068
}
5169
},
5270
{immediate: true}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface BModalController {
2+
hide: (trigger?: string) => void
3+
hideAll: (trigger?: string) => void
4+
}

packages/bootstrap-vue-next/src/types/exports/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export type {
55
AlignmentJustifyContent,
66
AlignmentTextHorizontal,
77
AlignmentVertical,
8+
BModalController,
89
BPopoverDelayObject,
910
BreadcrumbItem,
1011
Breakpoint,

packages/bootstrap-vue-next/src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type {AlignmentTextHorizontal} from './AlignmentTextHorizontal'
66
export type {AlignmentVertical} from './AlignmentVertical'
77
export type {Animation} from './Animation'
88
export type {AriaInvalid} from './AriaInvalid'
9+
export type {BModalController} from './BModalController'
910
export type {BPopoverDelayObject} from './BPopoverDelayObject'
1011
export type {BPopoverPlacement} from './BPopoverPlacement'
1112
export type {BTableProvider} from './BTableProvider'

0 commit comments

Comments
 (0)