Skip to content

Commit c862022

Browse files
authored
Introduced ModalRoot component to prevent rendering modals inside ModalLink (#4)
1 parent a9cbc87 commit c862022

10 files changed

+278
-169
lines changed

demo-app/resources/js/Pages/CreateRole.vue

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup>
22
import { useForm } from '@inertiajs/vue3'
33
import { default as Axios } from 'axios'
4-
import { Modal } from 'inertiaui/modal'
4+
import { Modal, ModalLink } from 'inertiaui/modal'
55
import { ref } from 'vue'
66
77
defineProps(['headerValue'])
@@ -73,7 +73,11 @@ function submit() {
7373
</div>
7474
</div>
7575

76-
<div class="flex justify-end">
76+
<div class="flex items-center justify-end">
77+
<ModalLink max-width="sm" href="#another-local-modal" class="mr-auto text-sm text-pink-500">
78+
What's that?
79+
</ModalLink>
80+
7781
<button
7882
type="button"
7983
class="inline-flex items-center rounded-md border border-transparent bg-gray-600 px-4 py-2 text-sm font-medium text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2"
@@ -89,5 +93,9 @@ function submit() {
8993
</button>
9094
</div>
9195
</form>
96+
97+
<Modal name="another-local-modal">
98+
Hawaiian noises?
99+
</Modal>
92100
</Modal>
93101
</template>

demo-app/resources/js/app.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ import { createApp, h } from 'vue'
55
import { createInertiaApp } from '@inertiajs/vue3'
66
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'
77
import { ZiggyVue } from '../../vendor/tightenco/ziggy'
8-
import { putConfig } from 'inertiaui/modal'
8+
import { putConfig, ModalRoot } from 'inertiaui/modal'
99

1010
const appName = import.meta.env.VITE_APP_NAME || 'Laravel'
1111

1212
createInertiaApp({
1313
title: (title) => `${title} - ${appName}`,
1414
resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
1515
setup({ el, App, props, plugin }) {
16-
return createApp({ render: () => h(App, props) })
16+
return createApp({ render: () => h(ModalRoot, () => h(App, props)) })
1717
.use(plugin)
1818
.use(ZiggyVue)
1919
.mount(el)

docs/installation.md

+39-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,44 @@ npm install @inertiaui/modal-react
1616

1717
There's no backend package required for Inertia Modal, so you don't need to install anything using Composer.
1818

19-
### Tailwind Configuration
19+
## Inertia.js Configuration
20+
21+
Inertia Modal requires a `ModalRoot` component to be mounted in your app. You can do this in the main `app.js` file where you initialize your Inertia app using the `createInertiaApp` function.
22+
23+
You only need to change the render function to include the `ModalRoot` component and pass the `App` component as a child of `ModalRoot`:
24+
25+
```js
26+
import { ModalRoot } from '@inertiaui/modal-vue' // [!code ++]
27+
28+
createInertiaApp({
29+
setup({ el, App, props, plugin }) {
30+
return
31+
createApp({ render: () => h(App, props) }) // [!code --]
32+
createApp({ render: () => h(ModalRoot, () => h(App, props)) }) // [!code ++]
33+
.use(plugin)
34+
.mount(el)
35+
}
36+
})
37+
```
38+
39+
Alternatively, you can include the `ModalRoot` component in the [layout template](https://inertiajs.com/pages#persistent-layouts) of your app:
40+
41+
```vue
42+
<script setup>
43+
import { ModalRoot } from '@inertiaui/modal-vue'
44+
</script>
45+
46+
<template>
47+
<div>
48+
<!-- Your layout here -->
49+
<slot />
50+
</div>
51+
52+
<ModalRoot />
53+
</template>
54+
```
55+
56+
## Tailwind Configuration
2057

2158
Inertia Modal uses Tailwind CSS for styling. You need to include the package path in the *content* array of your `tailwind.config.js` file:
2259

@@ -42,7 +79,7 @@ export default {
4279

4380
:::
4481

45-
### Vite Configuration
82+
## Vite Configuration
4683

4784
There's some additional configuration required to use Inertia Modal. In the `vite.config.js` file, add the following config to the root of the configuration object:
4885

vue/src/Modal.vue

+138-28
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,175 @@
11
<script setup>
2-
import { inject, onBeforeUnmount, ref, provide } from 'vue'
2+
import { inject, onBeforeUnmount, ref, computed, useAttrs, onMounted } from 'vue'
3+
import { TransitionRoot, TransitionChild, Dialog } from '@headlessui/vue'
4+
5+
import { getConfig, getConfigByType } from './config'
6+
import { modalPropNames } from './modalStack'
7+
import { only } from './helpers'
8+
import { useModalStack } from './modalStack'
39
import ModalContent from './ModalContent.vue'
4-
import ModalWrapper from './ModalWrapper.vue'
10+
import ModalRenderer from './ModalRenderer.vue'
511
import SlideoverContent from './SlideoverContent.vue'
6-
import { useModalStack } from './modalStack'
712
813
const props = defineProps({
914
name: {
1015
type: String,
1116
required: false,
1217
},
18+
// The slideover prop in on top because we need to know if it's a slideover
19+
// before we can determine the defaule value of other props
20+
slideover: {
21+
type: Boolean,
22+
default: () => getConfig('type') === 'slideover',
23+
},
24+
closeButton: {
25+
type: Boolean,
26+
default: (props) => getConfigByType(props.slideover, 'closeButton'),
27+
},
28+
closeExplicitly: {
29+
type: Boolean,
30+
default: (props) => getConfigByType(props.slideover, 'closeExplicitly'),
31+
},
32+
maxWidth: {
33+
type: String,
34+
default: (props) => getConfigByType(props.slideover, 'maxWidth'),
35+
},
36+
paddingClasses: {
37+
type: [Boolean, String],
38+
default: (props) => getConfigByType(props.slideover, 'paddingClasses'),
39+
},
40+
panelClasses: {
41+
type: [Boolean, String],
42+
default: (props) => getConfigByType(props.slideover, 'panelClasses'),
43+
},
44+
position: {
45+
type: String,
46+
default: (props) => getConfigByType(props.slideover, 'position'),
47+
},
1348
})
1449
1550
const modalStack = useModalStack()
16-
const injectedModalContext = props.name ? ref({}) : inject('modalContext')
51+
const modalContext = props.name ? ref({}) : inject('modalContext')
52+
const modalProps = computed(() => {
53+
return {
54+
...only(props, modalPropNames),
55+
...modalContext.value.modalProps,
56+
}
57+
})
1758
59+
// Local Modals...
1860
if (props.name) {
1961
modalStack.registerLocalModal(props.name, function (context) {
20-
injectedModalContext.value = context
62+
modalContext.value = context
63+
registerEventListeners()
2164
})
2265
23-
// Now this component is the provider instead of ModalLink
24-
provide('modalContext', injectedModalContext)
25-
2666
onBeforeUnmount(() => {
2767
modalStack.removeLocalModal(props.name)
2868
})
2969
}
3070
31-
const emits = defineEmits(['emit'])
71+
onMounted(() => {
72+
modalStack.verifyRoot()
73+
74+
if (!props.name) {
75+
registerEventListeners()
76+
}
77+
})
78+
79+
function closeDialog() {
80+
if (!modalProps.value.closeExplicitly) {
81+
modalContext.value.close()
82+
}
83+
}
84+
85+
const unsubscribeEventListeners = ref(null)
86+
onBeforeUnmount(() => unsubscribeEventListeners.value?.())
87+
88+
const $attrs = useAttrs()
89+
90+
function registerEventListeners() {
91+
unsubscribeEventListeners.value = modalContext.value.registerEventListenersFromAttrs($attrs)
92+
}
93+
94+
const emits = defineEmits(['modal-event'])
3295
3396
function emit(event, ...args) {
34-
emits('emit', event, ...args)
97+
emits('modal-event', event, ...args)
3598
}
3699
37100
defineExpose({
38-
close: injectedModalContext.value.close,
101+
close: modalContext.value.close,
39102
emit,
40-
getChildModal: injectedModalContext.value.getChildModal,
41-
getParentModal: injectedModalContext.value.getParentModal,
42-
modalContext: injectedModalContext.value,
43-
reload: injectedModalContext.value.reload,
103+
getChildModal: modalContext.value.getChildModal,
104+
getParentModal: modalContext.value.getParentModal,
105+
modalContext: modalContext.value,
106+
reload: modalContext.value.reload,
44107
})
45108
</script>
46109
47110
<template>
48-
<ModalWrapper v-slot="{ modalContext, modalProps }">
49-
<component
50-
:is="modalProps.slideover ? SlideoverContent : ModalContent"
51-
:modal-context="modalContext"
52-
:modal-props="modalProps"
111+
<TransitionRoot
112+
:unmount="false"
113+
:show="modalContext.open ?? false"
114+
enter="transition transform ease-in-out duration-300"
115+
enter-from="opacity-0 scale-95"
116+
enter-to="opacity-100 scale-100"
117+
leave="transition transform ease-in-out duration-300"
118+
leave-from="opacity-100 scale-100"
119+
leave-to="opacity-0 scale-95"
120+
>
121+
<Dialog
122+
:data-inertiaui-modal-id="modalContext.id"
123+
:data-inertiaui-modal-index="modalContext.index"
124+
class="im-dialog relative z-20"
125+
@close="closeDialog"
53126
>
54-
<slot
55-
:close="modalContext.close"
56-
:emit="emit"
57-
:get-child-modal="modalContext.getChildModal"
58-
:get-parent-modal="modalContext.getParentModal"
127+
<!-- Only transition the backdrop for the first modal in the stack -->
128+
<TransitionChild
129+
v-if="modalContext.index === 0"
130+
as="template"
131+
enter="transition transform ease-in-out duration-300"
132+
enter-from="opacity-0"
133+
enter-to="opacity-100"
134+
leave="transition transform ease-in-out duration-300"
135+
leave-from="opacity-100"
136+
leave-to="opacity-0"
137+
>
138+
<div
139+
v-show="modalContext.onTopOfStack"
140+
class="im-backdrop fixed inset-0 z-30 bg-black/75"
141+
aria-hidden="true"
142+
/>
143+
</TransitionChild>
144+
145+
<!-- On multiple modals, only show a backdrop for the modal that is on top of the stack -->
146+
<div
147+
v-if="modalContext.index > 0 && modalContext.onTopOfStack"
148+
class="im-backdrop fixed inset-0 z-30 bg-black/75"
149+
/>
150+
151+
<!-- The modal/slideover content itself -->
152+
<component
153+
:is="modalProps.slideover ? SlideoverContent : ModalContent"
59154
:modal-context="modalContext"
60155
:modal-props="modalProps"
61-
:reload="modalContext.reload"
156+
>
157+
<slot
158+
:close="modalContext.close"
159+
:emit="emit"
160+
:get-child-modal="modalContext.getChildModal"
161+
:get-parent-modal="modalContext.getParentModal"
162+
:modal-context="modalContext"
163+
:modal-props="modalProps"
164+
:reload="modalContext.reload"
165+
/>
166+
</component>
167+
168+
<!-- The next modal in the stack -->
169+
<ModalRenderer
170+
v-if="modalStack.stack.value[modalContext.index + 1]"
171+
:index="modalContext.index + 1"
62172
/>
63-
</component>
64-
</ModalWrapper>
173+
</Dialog>
174+
</TransitionRoot>
65175
</template>

vue/src/ModalLink.vue

+10-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup>
22
import { modalPropNames, useModalStack } from './modalStack'
3-
import { nextTick, ref, provide, watch, onMounted, useAttrs } from 'vue'
3+
import { nextTick, ref, provide, watch, onMounted, useAttrs, onBeforeUnmount } from 'vue'
44
import { only, rejectNullValues } from './helpers'
55
66
const props = defineProps({
@@ -95,20 +95,20 @@ watch(
9595
)
9696
9797
onMounted(() => {
98+
modalStack.verifyRoot()
99+
98100
if (props.fragment && window.location.hash === `#${props.fragment}`) {
99101
handle()
100102
}
101103
})
102104
105+
const unsubscribeEventListeners = ref(null)
106+
onBeforeUnmount(() => unsubscribeEventListeners.value?.())
107+
103108
const $attrs = useAttrs()
104109
105-
function handleEmittedEvent(event, ...args) {
106-
// // e.g. refresh-key -> onRefreshKey
107-
const kebabEvent = event.replace(/-([a-z])/g, (g) => g[1].toUpperCase())
108-
const listener = `on${kebabEvent.charAt(0).toUpperCase()}${kebabEvent.slice(1)}`
109-
if (listener in $attrs) {
110-
$attrs[listener](...args)
111-
}
110+
function registerEventListeners() {
111+
unsubscribeEventListeners.value = modalContext.value.registerEventListenersFromAttrs($attrs)
112112
}
113113
114114
watch(modalContext, (value, oldValue) => {
@@ -117,6 +117,8 @@ watch(modalContext, (value, oldValue) => {
117117
window.location.hash = props.fragment
118118
}
119119
120+
registerEventListeners()
121+
120122
nextTick(() => {
121123
modalContext.value.open = true
122124
emit('success')
@@ -168,11 +170,4 @@ function handle() {
168170
>
169171
<slot :loading="loading" />
170172
</component>
171-
172-
<modalContext.component
173-
v-if="modalContext?.component"
174-
v-show="false"
175-
v-bind="modalContext.componentProps"
176-
@emit="handleEmittedEvent"
177-
/>
178173
</template>

vue/src/ModalRenderer.vue

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<script setup>
2+
import { useModalStack } from './modalStack'
3+
import { computed, provide } from 'vue'
4+
5+
const props = defineProps({
6+
index: {
7+
type: Number,
8+
required: true,
9+
},
10+
})
11+
12+
const modalStack = useModalStack()
13+
14+
const modalContext = computed(() => {
15+
return modalStack.stack.value[props.index]
16+
})
17+
18+
provide('modalContext', modalContext)
19+
</script>
20+
21+
<template>
22+
<modalContext.component
23+
v-if="modalContext?.component"
24+
v-bind="modalContext.componentProps"
25+
@modal-event="(event, ...args) => modalContext.emit(event, ...args)"
26+
/>
27+
</template>

0 commit comments

Comments
 (0)