├── .github └── FUNDING.yml ├── .vscode └── extensions.json ├── .prettierrc ├── src ├── index.ts ├── modal.ts ├── resolver.ts ├── plugin.ts ├── inertia.d.ts └── use-modal.ts ├── tsconfig.node.json ├── examples ├── Layout.vue ├── app-mix.js ├── app-vite.ts ├── Edit.vue └── Modal.vue ├── .gitignore ├── vite.config.ts ├── tsconfig.json ├── LICENSE.md ├── package.json └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [lepikhinb] 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": false, 4 | "bracketSameLine": true, 5 | "printWidth": 200, 6 | "singleAttributePerLine": false 7 | } 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { plugin as modal } from "./plugin" 2 | export { useModal } from "./use-modal" 3 | export { Modal } from "./modal" 4 | export type { ModalPluginOptions } from "./plugin" 5 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /examples/Layout.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /src/modal.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent } from "vue" 2 | import { useModal } from "./use-modal" 3 | 4 | export const Modal = defineComponent({ 5 | setup() { 6 | const { vnode } = useModal() 7 | 8 | return () => vnode.value 9 | }, 10 | }) 11 | -------------------------------------------------------------------------------- /src/resolver.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue" 2 | 3 | const resolveCallback = ref() 4 | 5 | export default { 6 | setResolveCallback: (callback: CallableFunction) => { 7 | resolveCallback.value = callback 8 | }, 9 | resolve: (name: string) => resolveCallback.value!(name), 10 | } 11 | -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { App } from "vue" 2 | import resolver from "./resolver" 3 | 4 | export type ModalPluginOptions = { 5 | resolve: (name: string) => any 6 | } 7 | 8 | export const plugin = { 9 | install(app: App, options: ModalPluginOptions) { 10 | resolver.setResolveCallback(options.resolve) 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | *.tgz -------------------------------------------------------------------------------- /src/inertia.d.ts: -------------------------------------------------------------------------------- 1 | import "@inertiajs/vue3" 2 | import { Page } from "@inertiajs/core" 3 | 4 | interface Modal { 5 | component: string 6 | baseURL: string 7 | redirectURL: string | null 8 | props: Record 9 | key: string 10 | nonce: string 11 | } 12 | 13 | declare module "@inertiajs/vue3" { 14 | export declare function usePage(): Page<{ modal: Modal }> 15 | } 16 | 17 | export {} 18 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "path" 2 | import { defineConfig } from "vite" 3 | 4 | export default defineConfig({ 5 | build: { 6 | lib: { 7 | entry: resolve(__dirname, "src/index.ts"), 8 | name: "Momentum Modal", 9 | fileName: `momentum-modal`, 10 | }, 11 | rollupOptions: { 12 | external: ["vue", "@inertiajs/vue3", "axios"], 13 | }, 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /examples/app-mix.js: -------------------------------------------------------------------------------- 1 | import { createApp, h } from "vue" 2 | import { createInertiaApp } from "@inertiajs/inertia-vue3" 3 | import { modal } from "momentum-modal" 4 | 5 | createInertiaApp({ 6 | resolve: (name) => require(`./Pages/${name}`), 7 | title: (title) => (title ? `${title} - Ping CRM` : "Ping CRM"), 8 | setup({ el, App, props, plugin }) { 9 | createApp({ render: () => h(App, props) }) 10 | .use(modal, { 11 | resolve: (name) => import(`./Pages/${name}`), 12 | }) 13 | .use(plugin) 14 | .mount(el) 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "lib": ["esnext", "dom"], 14 | "skipLibCheck": true, 15 | "outDir": "dist", 16 | "declaration": true, 17 | "declarationDir": "dist/types" 18 | }, 19 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /examples/app-vite.ts: -------------------------------------------------------------------------------- 1 | import { createApp, h } from "vue" 2 | import { createInertiaApp } from "@inertiajs/vue3" 3 | import { modal, ModalPluginOptions } from "momentum-modal" 4 | 5 | function resolvePageComponent(name: string, pages: Record) { 6 | for (const path in pages) { 7 | if (path.endsWith(`${name.replace(".", "/")}.vue`)) { 8 | return typeof pages[path] === "function" ? pages[path]() : pages[path] 9 | } 10 | } 11 | 12 | throw new Error(`Page not found: ${name}`) 13 | } 14 | 15 | createInertiaApp({ 16 | progress: false, 17 | resolve: (name) => resolvePageComponent(name, import.meta.glob("./Pages/**/*.vue")), 18 | setup({ el, App, props, plugin }) { 19 | createApp({ render: () => h(App, props) }) 20 | .use(modal, { 21 | resolve: (name: string) => resolvePageComponent(name, import.meta.glob("./Pages/**/*.vue")), 22 | } as ModalPluginOptions) 23 | .use(plugin) 24 | .mount(el) 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /examples/Edit.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Boris Lepikhin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "momentum-modal", 3 | "version": "0.2.3", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "npm run prettier && vite build && vue-tsc --declaration --emitDeclarationOnly", 7 | "prettier": "prettier src/ --write", 8 | "patch": "npm version patch --no-git-tag-version", 9 | "minor": "npm version minor --no-git-tag-version" 10 | }, 11 | "description": "A Vue 3 plugin that lets you implement backend-driven modal dialogs for Inertia apps.", 12 | "keywords": [ 13 | "laravel", 14 | "inertia", 15 | "vue", 16 | "modal", 17 | "dialog" 18 | ], 19 | "author": "Boris Lepikhin", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/lepikhinb/momentum-modal-plugin.git" 23 | }, 24 | "homepage": "https://github.com/lepikhinb/momentum-modal#readme", 25 | "files": [ 26 | "dist" 27 | ], 28 | "type": "module", 29 | "main": "./dist/momentum-modal.umd.cjs", 30 | "module": "./dist/momentum-modal.js", 31 | "types": "./dist/types/index.d.ts", 32 | "exports": { 33 | ".": { 34 | "import": "./dist/momentum-modal.js", 35 | "require": "./dist/momentum-modal.umd.cjs" 36 | } 37 | }, 38 | "peerDependencies": { 39 | "@inertiajs/vue3": "^1.0.0 || ^2.0.0", 40 | "axios": "^1.2.0", 41 | "vue": "^3.x" 42 | }, 43 | "devDependencies": { 44 | "@types/node": "^18.0.0", 45 | "axios": "^1.2.0", 46 | "typescript": "^4.5.4", 47 | "vite": "^4.0.4", 48 | "vue-tsc": "^1.0.24" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/Modal.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Momentum Modal 2 | 3 | Momentum Modal is a Vue 3 plugin that lets you implement backend-driven modal dialogs for Inertia apps. 4 | 5 | You can find the full documentation [here](https://github.com/lepikhinb/momentum-modal). 6 | 7 | Check out the [demo app](https://modal.advanced-inertia.com) demonstrating the Modal package in action. 8 | 9 | ## Advanced Inertia 10 | 11 | [](https://advanced-inertia.com) 12 | 13 | Take your Inertia.js skills to the next level with my book [Advanced Inertia](https://advanced-inertia.com/). 14 | Learn advanced concepts and make apps with Laravel and Inertia.js a breeze to build and maintain. 15 | 16 | ## Momentum 17 | 18 | Momentum is a set of packages designed to improve your experience building Inertia-powered apps. 19 | 20 | - [Modal](https://github.com/lepikhinb/momentum-modal) — Build dynamic modal dialogs for Inertia apps 21 | - [Preflight](https://github.com/lepikhinb/momentum-preflight) — Realtime backend-driven validation for Inertia apps 22 | - [Paginator](https://github.com/lepikhinb/momentum-paginator) — Headless wrapper around Laravel Pagination 23 | - [Trail](https://github.com/lepikhinb/momentum-trail) — Frontend package to use Laravel routes with Inertia 24 | - [Lock](https://github.com/lepikhinb/momentum-lock) — Frontend package to use Laravel permissions with Inertia 25 | - [Layout](https://github.com/lepikhinb/momentum-layout) — Persistent layouts for Vue 3 apps 26 | - [Vite Plugin Watch](https://github.com/lepikhinb/vite-plugin-watch) — Vite plugin to run shell commands on file changes 27 | 28 | ## Credits 29 | 30 | ## Credits 31 | 32 | - [Boris Lepikhin](https://twitter.com/lepikhinb) 33 | - [All Contributors](../../contributors) 34 | 35 | ## License 36 | 37 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 38 | -------------------------------------------------------------------------------- /src/use-modal.ts: -------------------------------------------------------------------------------- 1 | import { usePage } from "@inertiajs/vue3" 2 | import { router } from "@inertiajs/vue3" 3 | import { defineAsyncComponent, h, nextTick, watch, computed, ref, shallowRef } from "vue" 4 | import axios from "axios" 5 | import resolver from "./resolver" 6 | 7 | const page = usePage() 8 | const modal = computed(() => page?.props?.modal) 9 | const props = computed(() => modal.value?.props) 10 | const key = computed(() => modal.value?.key) 11 | 12 | const componentName = ref() 13 | const component = shallowRef() 14 | const show = ref(false) 15 | const vnode = ref() 16 | const nonce = ref() 17 | 18 | const setHeaders = (values: Record) => { 19 | Object.entries(values).forEach(([key, value]) => 20 | ["post", "put", "patch", "delete"].forEach((method) => { 21 | /** @ts-ignore */ 22 | axios.defaults.headers[method][key] = value 23 | }) 24 | ) 25 | } 26 | 27 | const resetHeaders = () => { 28 | const headers = ["X-Inertia-Modal-Key", "X-Inertia-Modal-Redirect"] 29 | 30 | headers.forEach(([key, value]) => 31 | ["get", "post", "put", "patch", "delete"].forEach((method) => { 32 | /** @ts-ignore */ 33 | delete axios.defaults.headers[method][key] 34 | }) 35 | ) 36 | } 37 | 38 | const updateHeaders = () => { 39 | setHeaders({ 40 | "X-Inertia-Modal-Key": key.value, 41 | "X-Inertia-Modal-Redirect": modal.value?.redirectURL, 42 | }) 43 | 44 | axios.defaults.headers.get["X-Inertia-Modal-Redirect"] = modal.value?.redirectURL ?? "" 45 | } 46 | 47 | const close = () => { 48 | show.value = false 49 | 50 | resetHeaders() 51 | } 52 | 53 | const resolveComponent = () => { 54 | if (nonce.value == modal.value?.nonce || !modal.value?.component) { 55 | return close() 56 | } 57 | 58 | if (componentName.value != modal.value?.component) { 59 | componentName.value = modal.value.component 60 | 61 | if (componentName.value) { 62 | component.value = defineAsyncComponent(() => resolver.resolve(componentName.value)) 63 | } else { 64 | component.value = false 65 | } 66 | } 67 | 68 | nonce.value = modal.value?.nonce 69 | vnode.value = component.value 70 | ? h(component.value, { 71 | key: key.value, 72 | ...props.value, 73 | }) 74 | : "" 75 | 76 | nextTick(() => (show.value = true)) 77 | } 78 | 79 | resolveComponent() 80 | 81 | if (typeof window !== "undefined") { 82 | window.addEventListener("popstate", (event: PopStateEvent) => { 83 | nonce.value = null 84 | }) 85 | } 86 | 87 | watch( 88 | modal, 89 | () => { 90 | if (modal.value?.nonce !== nonce.value) { 91 | resolveComponent() 92 | } 93 | }, 94 | { deep: true } 95 | ) 96 | 97 | watch(key, updateHeaders) 98 | 99 | const redirect = () => { 100 | var redirectURL = modal.value?.redirectURL ?? modal.value?.baseURL 101 | 102 | vnode.value = false 103 | 104 | if (!redirectURL) { 105 | return 106 | } 107 | 108 | return router.visit(redirectURL, { 109 | preserveScroll: true, 110 | preserveState: true, 111 | }) 112 | } 113 | 114 | export const useModal = () => { 115 | return { 116 | show, 117 | vnode, 118 | close, 119 | redirect, 120 | props, 121 | } 122 | } 123 | --------------------------------------------------------------------------------