Skip to content

Commit

Permalink
Introduced ModalRoot component to prevent rendering modals inside `…
Browse files Browse the repository at this point in the history
…ModalLink` (#4)
  • Loading branch information
pascalbaljet authored Oct 2, 2024
1 parent a9cbc87 commit c862022
Show file tree
Hide file tree
Showing 10 changed files with 278 additions and 169 deletions.
12 changes: 10 additions & 2 deletions demo-app/resources/js/Pages/CreateRole.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup>
import { useForm } from '@inertiajs/vue3'
import { default as Axios } from 'axios'
import { Modal } from 'inertiaui/modal'
import { Modal, ModalLink } from 'inertiaui/modal'
import { ref } from 'vue'
defineProps(['headerValue'])
Expand Down Expand Up @@ -73,7 +73,11 @@ function submit() {
</div>
</div>

<div class="flex justify-end">
<div class="flex items-center justify-end">
<ModalLink max-width="sm" href="#another-local-modal" class="mr-auto text-sm text-pink-500">
What's that?
</ModalLink>

<button
type="button"
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"
Expand All @@ -89,5 +93,9 @@ function submit() {
</button>
</div>
</form>

<Modal name="another-local-modal">
Hawaiian noises?
</Modal>
</Modal>
</template>
4 changes: 2 additions & 2 deletions demo-app/resources/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'
import { ZiggyVue } from '../../vendor/tightenco/ziggy'
import { putConfig } from 'inertiaui/modal'
import { putConfig, ModalRoot } from 'inertiaui/modal'

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

createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
setup({ el, App, props, plugin }) {
return createApp({ render: () => h(App, props) })
return createApp({ render: () => h(ModalRoot, () => h(App, props)) })
.use(plugin)
.use(ZiggyVue)
.mount(el)
Expand Down
41 changes: 39 additions & 2 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,44 @@ npm install @inertiaui/modal-react

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

### Tailwind Configuration
## Inertia.js Configuration

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.

You only need to change the render function to include the `ModalRoot` component and pass the `App` component as a child of `ModalRoot`:

```js
import { ModalRoot } from '@inertiaui/modal-vue' // [!code ++]

createInertiaApp({
setup({ el, App, props, plugin }) {
return
createApp({ render: () => h(App, props) }) // [!code --]
createApp({ render: () => h(ModalRoot, () => h(App, props)) }) // [!code ++]
.use(plugin)
.mount(el)
}
})
```

Alternatively, you can include the `ModalRoot` component in the [layout template](https://inertiajs.com/pages#persistent-layouts) of your app:

```vue
<script setup>
import { ModalRoot } from '@inertiaui/modal-vue'
</script>
<template>
<div>
<!-- Your layout here -->
<slot />
</div>
<ModalRoot />
</template>
```

## Tailwind Configuration

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

Expand All @@ -42,7 +79,7 @@ export default {

:::

### Vite Configuration
## Vite Configuration

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:

Expand Down
166 changes: 138 additions & 28 deletions vue/src/Modal.vue
Original file line number Diff line number Diff line change
@@ -1,65 +1,175 @@
<script setup>
import { inject, onBeforeUnmount, ref, provide } from 'vue'
import { inject, onBeforeUnmount, ref, computed, useAttrs, onMounted } from 'vue'
import { TransitionRoot, TransitionChild, Dialog } from '@headlessui/vue'
import { getConfig, getConfigByType } from './config'
import { modalPropNames } from './modalStack'
import { only } from './helpers'
import { useModalStack } from './modalStack'
import ModalContent from './ModalContent.vue'
import ModalWrapper from './ModalWrapper.vue'
import ModalRenderer from './ModalRenderer.vue'
import SlideoverContent from './SlideoverContent.vue'
import { useModalStack } from './modalStack'
const props = defineProps({
name: {
type: String,
required: false,
},
// The slideover prop in on top because we need to know if it's a slideover
// before we can determine the defaule value of other props
slideover: {
type: Boolean,
default: () => getConfig('type') === 'slideover',
},
closeButton: {
type: Boolean,
default: (props) => getConfigByType(props.slideover, 'closeButton'),
},
closeExplicitly: {
type: Boolean,
default: (props) => getConfigByType(props.slideover, 'closeExplicitly'),
},
maxWidth: {
type: String,
default: (props) => getConfigByType(props.slideover, 'maxWidth'),
},
paddingClasses: {
type: [Boolean, String],
default: (props) => getConfigByType(props.slideover, 'paddingClasses'),
},
panelClasses: {
type: [Boolean, String],
default: (props) => getConfigByType(props.slideover, 'panelClasses'),
},
position: {
type: String,
default: (props) => getConfigByType(props.slideover, 'position'),
},
})
const modalStack = useModalStack()
const injectedModalContext = props.name ? ref({}) : inject('modalContext')
const modalContext = props.name ? ref({}) : inject('modalContext')
const modalProps = computed(() => {
return {
...only(props, modalPropNames),
...modalContext.value.modalProps,
}
})
// Local Modals...
if (props.name) {
modalStack.registerLocalModal(props.name, function (context) {
injectedModalContext.value = context
modalContext.value = context
registerEventListeners()
})
// Now this component is the provider instead of ModalLink
provide('modalContext', injectedModalContext)
onBeforeUnmount(() => {
modalStack.removeLocalModal(props.name)
})
}
const emits = defineEmits(['emit'])
onMounted(() => {
modalStack.verifyRoot()
if (!props.name) {
registerEventListeners()
}
})
function closeDialog() {
if (!modalProps.value.closeExplicitly) {
modalContext.value.close()
}
}
const unsubscribeEventListeners = ref(null)
onBeforeUnmount(() => unsubscribeEventListeners.value?.())
const $attrs = useAttrs()
function registerEventListeners() {
unsubscribeEventListeners.value = modalContext.value.registerEventListenersFromAttrs($attrs)
}
const emits = defineEmits(['modal-event'])
function emit(event, ...args) {
emits('emit', event, ...args)
emits('modal-event', event, ...args)
}
defineExpose({
close: injectedModalContext.value.close,
close: modalContext.value.close,
emit,
getChildModal: injectedModalContext.value.getChildModal,
getParentModal: injectedModalContext.value.getParentModal,
modalContext: injectedModalContext.value,
reload: injectedModalContext.value.reload,
getChildModal: modalContext.value.getChildModal,
getParentModal: modalContext.value.getParentModal,
modalContext: modalContext.value,
reload: modalContext.value.reload,
})
</script>
<template>
<ModalWrapper v-slot="{ modalContext, modalProps }">
<component
:is="modalProps.slideover ? SlideoverContent : ModalContent"
:modal-context="modalContext"
:modal-props="modalProps"
<TransitionRoot
:unmount="false"
:show="modalContext.open ?? false"
enter="transition transform ease-in-out duration-300"
enter-from="opacity-0 scale-95"
enter-to="opacity-100 scale-100"
leave="transition transform ease-in-out duration-300"
leave-from="opacity-100 scale-100"
leave-to="opacity-0 scale-95"
>
<Dialog
:data-inertiaui-modal-id="modalContext.id"
:data-inertiaui-modal-index="modalContext.index"
class="im-dialog relative z-20"
@close="closeDialog"
>
<slot
:close="modalContext.close"
:emit="emit"
:get-child-modal="modalContext.getChildModal"
:get-parent-modal="modalContext.getParentModal"
<!-- Only transition the backdrop for the first modal in the stack -->
<TransitionChild
v-if="modalContext.index === 0"
as="template"
enter="transition transform ease-in-out duration-300"
enter-from="opacity-0"
enter-to="opacity-100"
leave="transition transform ease-in-out duration-300"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div
v-show="modalContext.onTopOfStack"
class="im-backdrop fixed inset-0 z-30 bg-black/75"
aria-hidden="true"
/>
</TransitionChild>
<!-- On multiple modals, only show a backdrop for the modal that is on top of the stack -->
<div
v-if="modalContext.index > 0 && modalContext.onTopOfStack"
class="im-backdrop fixed inset-0 z-30 bg-black/75"
/>
<!-- The modal/slideover content itself -->
<component
:is="modalProps.slideover ? SlideoverContent : ModalContent"
:modal-context="modalContext"
:modal-props="modalProps"
:reload="modalContext.reload"
>
<slot
:close="modalContext.close"
:emit="emit"
:get-child-modal="modalContext.getChildModal"
:get-parent-modal="modalContext.getParentModal"
:modal-context="modalContext"
:modal-props="modalProps"
:reload="modalContext.reload"
/>
</component>
<!-- The next modal in the stack -->
<ModalRenderer
v-if="modalStack.stack.value[modalContext.index + 1]"
:index="modalContext.index + 1"
/>
</component>
</ModalWrapper>
</Dialog>
</TransitionRoot>
</template>
25 changes: 10 additions & 15 deletions vue/src/ModalLink.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup>
import { modalPropNames, useModalStack } from './modalStack'
import { nextTick, ref, provide, watch, onMounted, useAttrs } from 'vue'
import { nextTick, ref, provide, watch, onMounted, useAttrs, onBeforeUnmount } from 'vue'
import { only, rejectNullValues } from './helpers'
const props = defineProps({
Expand Down Expand Up @@ -95,20 +95,20 @@ watch(
)
onMounted(() => {
modalStack.verifyRoot()
if (props.fragment && window.location.hash === `#${props.fragment}`) {
handle()
}
})
const unsubscribeEventListeners = ref(null)
onBeforeUnmount(() => unsubscribeEventListeners.value?.())
const $attrs = useAttrs()
function handleEmittedEvent(event, ...args) {
// // e.g. refresh-key -> onRefreshKey
const kebabEvent = event.replace(/-([a-z])/g, (g) => g[1].toUpperCase())
const listener = `on${kebabEvent.charAt(0).toUpperCase()}${kebabEvent.slice(1)}`
if (listener in $attrs) {
$attrs[listener](...args)
}
function registerEventListeners() {
unsubscribeEventListeners.value = modalContext.value.registerEventListenersFromAttrs($attrs)
}
watch(modalContext, (value, oldValue) => {
Expand All @@ -117,6 +117,8 @@ watch(modalContext, (value, oldValue) => {
window.location.hash = props.fragment
}
registerEventListeners()
nextTick(() => {
modalContext.value.open = true
emit('success')
Expand Down Expand Up @@ -168,11 +170,4 @@ function handle() {
>
<slot :loading="loading" />
</component>
<modalContext.component
v-if="modalContext?.component"
v-show="false"
v-bind="modalContext.componentProps"
@emit="handleEmittedEvent"
/>
</template>
27 changes: 27 additions & 0 deletions vue/src/ModalRenderer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script setup>
import { useModalStack } from './modalStack'
import { computed, provide } from 'vue'
const props = defineProps({
index: {
type: Number,
required: true,
},
})
const modalStack = useModalStack()
const modalContext = computed(() => {
return modalStack.stack.value[props.index]
})
provide('modalContext', modalContext)
</script>

<template>
<modalContext.component
v-if="modalContext?.component"
v-bind="modalContext.componentProps"
@modal-event="(event, ...args) => modalContext.emit(event, ...args)"
/>
</template>
Loading

0 comments on commit c862022

Please sign in to comment.