Skip to content
31 changes: 17 additions & 14 deletions packages/bootstrap-vue-next/src/composables/useModal/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
type Component,
type ComponentInternalInstance,
computed,
getCurrentInstance,
Expand Down Expand Up @@ -38,27 +39,29 @@ export const useModal = () => {
/**
* @returns {PromiseWithComponent} Returns a promise object with methods to control the modal (show, hide, toggle, get, set, destroy)
*/
const create = (
obj: ModalOrchestratorCreateParam = {},
const create = <ComponentProps = Record<string, unknown>>(
obj: ModalOrchestratorCreateParam<ComponentProps> = {} as ModalOrchestratorCreateParam<ComponentProps>,
options: OrchestratorCreateOptions = {}
): PromiseWithComponent<typeof BModal, ModalOrchestratorParam> => {
): PromiseWithComponent<typeof BModal, ModalOrchestratorParam<ComponentProps>> => {
if (!_isOrchestratorInstalled.value) {
throw new Error('BApp or BOrchestrator component must be mounted to use the modal controller')
}

const resolvedProps = toRef(obj)
const resolvedProps = toRef(obj as unknown as ModalOrchestratorParam<ComponentProps>) as Ref<
ModalOrchestratorParam<ComponentProps>
>
const _self = resolvedProps.value?.id || Symbol('Modals controller')

const promise = buildPromise<
typeof BModal,
ModalOrchestratorParam,
ModalOrchestratorParam<ComponentProps>,
ModalOrchestratorArrayValue
>(_self, store as Ref<ModalOrchestratorArrayValue[]>)

promise.stop = watch(
resolvedProps,
(_newValue) => {
const newValue = {...toValue(_newValue)}
const newValue = {...toValue(_newValue)} as Record<string, unknown>
const previousIndex = store.value.findIndex((el) => el._self === _self)
const previous =
previousIndex === -1 ? {_component: markRaw(BModal)} : store.value[previousIndex]
Expand All @@ -73,21 +76,21 @@ export const useModal = () => {

for (const key in newValue) {
if (key.startsWith('on')) {
v[key as keyof ModalOrchestratorCreateParam] =
newValue[key as keyof ModalOrchestratorCreateParam]
v[key as keyof ModalOrchestratorArrayValue] = newValue[key] as never
} else if (key === 'component' && newValue.component) {
v._component = markRaw(newValue.component)
v._component = markRaw(newValue.component as Component)
} else if (key === 'slots' && newValue.slots) {
v.slots = markRaw(newValue.slots)
v.slots = markRaw(newValue.slots) as never
} else {
v[key as keyof ModalOrchestratorCreateParam] = toValue(
newValue[key as keyof ModalOrchestratorCreateParam]
)
v[key as keyof ModalOrchestratorArrayValue] = toValue(newValue[key]) as never
}
}
v.modelValue = v.modelValue ?? false
v['onUpdate:modelValue'] = (val: boolean) => {
newValue['onUpdate:modelValue']?.(val)
const onUpdateModelValue = newValue['onUpdate:modelValue'] as
| ((val: boolean) => void)
| undefined
onUpdateModelValue?.(val)
const {modelValue} = toValue(obj)
if (isRef(obj) && !isRef(modelValue)) obj.value.modelValue = val
if (isRef(modelValue) && !isReadonly(modelValue)) {
Expand Down
145 changes: 145 additions & 0 deletions packages/bootstrap-vue-next/src/composables/useModal/useModal.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import {describe, expect, it} from 'vitest'
import {defineComponent, h} from 'vue'
import {mount} from '@vue/test-utils'
import BApp from '../../components/BApp/BApp.vue'
import {useModal} from './index'

describe('useModal', () => {
it('create method accepts custom component props without TypeScript errors', async () => {
// Define a custom modal component with custom props
const CustomModal = defineComponent({

Check warning on line 10 in packages/bootstrap-vue-next/src/composables/useModal/useModal.spec.ts

View workflow job for this annotation

GitHub Actions / test-lint

There is more than one component in this file

Check warning on line 10 in packages/bootstrap-vue-next/src/composables/useModal/useModal.spec.ts

View workflow job for this annotation

GitHub Actions / test-lint

There is more than one component in this file
name: 'CustomModal',
props: {
customProp: {
type: String,
required: true,
},
anotherProp: {
type: Number,
default: 42,
},
},
setup(props) {
return () =>
h('div', {class: 'custom-modal'}, [
h('p', `Custom Prop: ${props.customProp}`),
h('p', `Another Prop: ${props.anotherProp}`),
])
},
})

const TestComponent = defineComponent({

Check warning on line 31 in packages/bootstrap-vue-next/src/composables/useModal/useModal.spec.ts

View workflow job for this annotation

GitHub Actions / test-lint

There is more than one component in this file

Check warning on line 31 in packages/bootstrap-vue-next/src/composables/useModal/useModal.spec.ts

View workflow job for this annotation

GitHub Actions / test-lint

There is more than one component in this file
setup() {
const {create} = useModal()

// This should not cause TypeScript errors
const modal = create({
component: CustomModal,
customProp: 'test value',
anotherProp: 100,
})

return () =>
h('div', [
h(
'button',
{
onClick: () => modal.show(),
},
'Show Modal'
),
])
},
})

const wrapper = mount(BApp, {
slots: {
default: () => h(TestComponent),
},
})

expect(wrapper.exists()).toBe(true)
})

it('create method accepts BModal props', async () => {
const TestComponent = defineComponent({

Check warning on line 65 in packages/bootstrap-vue-next/src/composables/useModal/useModal.spec.ts

View workflow job for this annotation

GitHub Actions / test-lint

There is more than one component in this file

Check warning on line 65 in packages/bootstrap-vue-next/src/composables/useModal/useModal.spec.ts

View workflow job for this annotation

GitHub Actions / test-lint

There is more than one component in this file
setup() {
const {create} = useModal()

// Standard BModal props should still work
const modal = create({
title: 'Test Modal',
body: 'Test body content',
size: 'lg',
})

return () =>
h('div', [
h(
'button',
{
onClick: () => modal.show(),
},
'Show Modal'
),
])
},
})

const wrapper = mount(BApp, {
slots: {
default: () => h(TestComponent),
},
})

expect(wrapper.exists()).toBe(true)
})

it('create method accepts both BModal props and custom props together', async () => {
const CustomModal = defineComponent({

Check warning on line 99 in packages/bootstrap-vue-next/src/composables/useModal/useModal.spec.ts

View workflow job for this annotation

GitHub Actions / test-lint

There is more than one component in this file

Check warning on line 99 in packages/bootstrap-vue-next/src/composables/useModal/useModal.spec.ts

View workflow job for this annotation

GitHub Actions / test-lint

There is more than one component in this file
name: 'CustomModalWithDefaults',
props: {
myCustomField: {
type: String,
default: 'default',
},
},
setup(props) {
return () => h('div', {class: 'custom'}, props.myCustomField)
},
})

const TestComponent = defineComponent({

Check warning on line 112 in packages/bootstrap-vue-next/src/composables/useModal/useModal.spec.ts

View workflow job for this annotation

GitHub Actions / test-lint

There is more than one component in this file

Check warning on line 112 in packages/bootstrap-vue-next/src/composables/useModal/useModal.spec.ts

View workflow job for this annotation

GitHub Actions / test-lint

There is more than one component in this file
setup() {
const {create} = useModal()

// Mix of standard and custom props
const modal = create({
component: CustomModal,
title: 'Standard Title',
myCustomField: 'custom value',
size: 'md',
})

return () =>
h('div', [
h(
'button',
{
onClick: () => modal.show(),
},
'Show Modal'
),
])
},
})

const wrapper = mount(BApp, {
slots: {
default: () => h(TestComponent),
},
})

expect(wrapper.exists()).toBe(true)
})
})
52 changes: 36 additions & 16 deletions packages/bootstrap-vue-next/src/types/ComponentOrchestratorTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,11 @@ export type ToastOrchestratorArrayValue = Omit<BToastProps, 'modelValue'> & {
slots?: {
[K in keyof BToastSlots]?: BToastSlots[K] | Readonly<Component>
}
}
export type ToastOrchestratorParam = Omit<BToastProps, 'modelValue'> & {
} & Record<string, unknown>
export type ToastOrchestratorParam<ComponentProps = Record<string, unknown>> = Omit<
BToastProps,
'modelValue'
> & {
'title'?: MaybeRefOrGetter<BToastProps['title']>
'body'?: MaybeRefOrGetter<BToastProps['body']>
/**
Expand Down Expand Up @@ -106,9 +109,11 @@ export type ToastOrchestratorParam = Omit<BToastProps, 'modelValue'> & {
slots?: {
[K in keyof BToastSlots]?: MaybeRefOrGetter<BToastSlots[K] | Readonly<Component>>
}
}
} & ComponentProps

export type ToastOrchestratorCreateParam = MaybeRef<ToastOrchestratorParam>
export type ToastOrchestratorCreateParam<ComponentProps = Record<string, unknown>> = MaybeRef<
ToastOrchestratorParam<ComponentProps>
>

export type TooltipOrchestratorArrayValue = BTooltipProps & {
'type': 'tooltip'
Expand All @@ -127,9 +132,12 @@ export type TooltipOrchestratorArrayValue = BTooltipProps & {
}
} & {
[K in keyof BPopoverEmits as CamelCase<Prefix<'on-', K>>]?: (e: BPopoverEmits[K][0]) => void
}
} & Record<string, unknown>

export type TooltipOrchestratorParam = Omit<BTooltipProps, 'body' | 'title' | 'modelValue'> & {
export type TooltipOrchestratorParam<ComponentProps = Record<string, unknown>> = Omit<
BTooltipProps,
'body' | 'title' | 'modelValue'
> & {
'onUpdate:modelValue'?: (val: boolean) => void
'title'?: MaybeRefOrGetter<BTooltipProps['title']>
'body'?: MaybeRefOrGetter<BTooltipProps['body']>
Expand All @@ -142,9 +150,11 @@ export type TooltipOrchestratorParam = Omit<BTooltipProps, 'body' | 'title' | 'm
}
} & {
[K in keyof BPopoverEmits as CamelCase<Prefix<'on-', K>>]?: (e: BPopoverEmits[K][0]) => void
}
} & ComponentProps

export type TooltipOrchestratorCreateParam = MaybeRef<TooltipOrchestratorParam>
export type TooltipOrchestratorCreateParam<ComponentProps = Record<string, unknown>> = MaybeRef<
TooltipOrchestratorParam<ComponentProps>
>

export type PopoverOrchestratorArrayValue = BPopoverProps &
BTooltipProps & {
Expand All @@ -164,9 +174,12 @@ export type PopoverOrchestratorArrayValue = BPopoverProps &
}
} & {
[K in keyof BPopoverEmits as CamelCase<Prefix<'on-', K>>]?: (e: BPopoverEmits[K][0]) => void
}
} & Record<string, unknown>

export type PopoverOrchestratorParam = Omit<BPopoverProps, 'body' | 'title' | 'modelValue'> & {
export type PopoverOrchestratorParam<ComponentProps = Record<string, unknown>> = Omit<
BPopoverProps,
'body' | 'title' | 'modelValue'
> & {
'onUpdate:modelValue'?: (val: boolean) => void
'title'?: MaybeRefOrGetter<BPopoverProps['title']>
'body'?: MaybeRefOrGetter<BPopoverProps['body']>
Expand All @@ -179,9 +192,11 @@ export type PopoverOrchestratorParam = Omit<BPopoverProps, 'body' | 'title' | 'm
}
} & {
[K in keyof BPopoverEmits as CamelCase<Prefix<'on-', K>>]?: (e: BPopoverEmits[K][0]) => void
}
} & ComponentProps

export type PopoverOrchestratorCreateParam = MaybeRef<PopoverOrchestratorParam>
export type PopoverOrchestratorCreateParam<ComponentProps = Record<string, unknown>> = MaybeRef<
PopoverOrchestratorParam<ComponentProps>
>

export type ModalOrchestratorArrayValue = BModalProps & {
'type': 'modal'
Expand All @@ -200,9 +215,12 @@ export type ModalOrchestratorArrayValue = BModalProps & {
}
} & {
[K in keyof BModalEmits as CamelCase<Prefix<'on-', K>>]?: (e: BModalEmits[K][0]) => void
}
} & Record<string, unknown>

export type ModalOrchestratorParam = Omit<BModalProps, 'body' | 'title' | 'modelValue'> & {
export type ModalOrchestratorParam<ComponentProps = Record<string, unknown>> = Omit<
BModalProps,
'body' | 'title' | 'modelValue'
> & {
'onUpdate:modelValue'?: (val: boolean) => void
'title'?: MaybeRefOrGetter<BModalProps['title']>
'body'?: MaybeRefOrGetter<BModalProps['body']>
Expand All @@ -218,9 +236,11 @@ export type ModalOrchestratorParam = Omit<BModalProps, 'body' | 'title' | 'model
}
} & {
[K in keyof BModalEmits as CamelCase<Prefix<'on-', K>>]?: (e: BModalEmits[K][0]) => void
}
} & ComponentProps

export type ModalOrchestratorCreateParam = MaybeRef<ModalOrchestratorParam>
export type ModalOrchestratorCreateParam<ComponentProps = Record<string, unknown>> = MaybeRef<
ModalOrchestratorParam<ComponentProps>
>

export type OrchestratorCreateOptions = {
keep?: boolean
Expand Down
Loading