nuxt-crud-modals 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/README.md +122 -0
- package/dist/module.d.mts +17 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +41 -0
- package/dist/runtime/app/components/FormModal.d.vue.ts +38 -0
- package/dist/runtime/app/components/FormModal.vue +80 -0
- package/dist/runtime/app/components/FormModal.vue.d.ts +38 -0
- package/dist/runtime/app/components/LoadingOverlay.d.vue.ts +3 -0
- package/dist/runtime/app/components/LoadingOverlay.vue +51 -0
- package/dist/runtime/app/components/LoadingOverlay.vue.d.ts +3 -0
- package/dist/runtime/app/composables/useLoadingOverlay.d.ts +24 -0
- package/dist/runtime/app/composables/useLoadingOverlay.js +36 -0
- package/dist/runtime/app/utils/modalFactory.d.ts +26 -0
- package/dist/runtime/app/utils/modalFactory.js +90 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/types.d.mts +3 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2026 Tobias Scheibling
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
[![MIT License][license-src]][license-href]
|
|
2
|
+
[![NuxtModules][modules-src]][modules-href]
|
|
3
|
+
|
|
4
|
+
# Nuxt CRUD Modals
|
|
5
|
+
|
|
6
|
+
Nuxt CRUD Modals is a lightweight module designed to streamline the creation, viewing and editing of (database) records through modal interfaces in Nuxt applications. It provides a simple and consistent way to handle CRUD-related UI patterns without repetitive boilerplate.
|
|
7
|
+
|
|
8
|
+
Built on top of Nuxt UI’s modal system, it leverages `UModal` and `useOverlay` under the hood to deliver a flexible and extensible modal experience. The module abstracts common interaction patterns, allowing developers to quickly scaffold modals for different record types while maintaining full control over behavior and presentation.
|
|
9
|
+
|
|
10
|
+
Whether you're building admin panels, dashboards or data-driven applications, Nuxt CRUD Modals module helps you reduce complexity and focus on your business logic by handling the modal lifecycle, state management and integration patterns for you.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- Open, view or edit modals based on record ID
|
|
15
|
+
- Loading overlay for long running data fetching operations
|
|
16
|
+
- `<UFormModal>` component for creating and editing data records
|
|
17
|
+
- Coming soon: delete modals
|
|
18
|
+
|
|
19
|
+
## Peer Dependencies
|
|
20
|
+
|
|
21
|
+
Requires Nuxt UI and Zod 4
|
|
22
|
+
|
|
23
|
+
## Quick Setup
|
|
24
|
+
|
|
25
|
+
Install the module to your Nuxt application with one command:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx nuxt module add nuxt-crud-modals
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
That's it! You can now use CRUD Modals in your Nuxt app
|
|
32
|
+
|
|
33
|
+
## Options
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
export default defineNuxtConfig({
|
|
37
|
+
modules: ['@nuxt/ui', 'nuxt-crud-modals'],
|
|
38
|
+
|
|
39
|
+
//...
|
|
40
|
+
|
|
41
|
+
modals: {
|
|
42
|
+
loadingDelay: 700, // hides the loading overlay for 700 ms
|
|
43
|
+
prefix: 'U', // prefix for the components, e.g. UFormModal
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
First, define a modal for your specific record type. You configure which components should be used for creating, editing, and viewing records, and optionally provide a data-fetching function:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import JobForm from '~/components/JobForm.vue'
|
|
54
|
+
import JobView from '~/components/Job.vue'
|
|
55
|
+
|
|
56
|
+
export const useJobModal = defineCrudModals({
|
|
57
|
+
components: {
|
|
58
|
+
create: JobForm,
|
|
59
|
+
edit: JobForm,
|
|
60
|
+
view: JobView
|
|
61
|
+
},
|
|
62
|
+
fetchData: async (id) => {
|
|
63
|
+
// your data fetching logic
|
|
64
|
+
return {
|
|
65
|
+
id,
|
|
66
|
+
title: 'Sample job',
|
|
67
|
+
status: 'In progress',
|
|
68
|
+
comment: 'Additional briefing needed'
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Open modal to edit a record
|
|
75
|
+
To open the modal in edit mode, call `openToEdit` with the record ID:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const jobModal = useJobModal()
|
|
79
|
+
jobModal.openToEdit(123)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Open modal with initial state to create a new record
|
|
83
|
+
To create a new record, use `openToCreate`. You can optionally pass an initial state:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const jobModal = useJobModal()
|
|
87
|
+
jobModal.openToCreate({ status: 'New' })
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This will open the modal providing the initial state in the modal to prefill the form.
|
|
91
|
+
|
|
92
|
+
## Local development
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Install dependencies
|
|
96
|
+
npm install
|
|
97
|
+
|
|
98
|
+
# Generate type stubs
|
|
99
|
+
npm run dev:prepare
|
|
100
|
+
|
|
101
|
+
# Develop with the playground
|
|
102
|
+
npm run dev
|
|
103
|
+
|
|
104
|
+
# Build the playground
|
|
105
|
+
npm run dev:build
|
|
106
|
+
|
|
107
|
+
# Run ESLint
|
|
108
|
+
npm run lint
|
|
109
|
+
|
|
110
|
+
# Run Vitest
|
|
111
|
+
npm run test
|
|
112
|
+
npm run test:watch
|
|
113
|
+
|
|
114
|
+
# Release new version
|
|
115
|
+
npm run release
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
[license-src]: https://img.shields.io/github/license/tosling/nuxt-crud-modals
|
|
119
|
+
[license-href]: ./LICENSE
|
|
120
|
+
|
|
121
|
+
[modules-src]: https://img.shields.io/badge/Nuxt%20Module-gray?logo=nuxt
|
|
122
|
+
[modules-href]: https://nuxt.com/modules
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
|
|
3
|
+
declare module '@nuxt/schema' {
|
|
4
|
+
interface PublicRuntimeConfig {
|
|
5
|
+
modals: {
|
|
6
|
+
loadingDelay: number;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
interface ModuleOptions {
|
|
11
|
+
loadingDelay: number;
|
|
12
|
+
prefix: string;
|
|
13
|
+
}
|
|
14
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
15
|
+
|
|
16
|
+
export { _default as default };
|
|
17
|
+
export type { ModuleOptions };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { defineNuxtModule, createResolver, addComponentsDir, addImports } from '@nuxt/kit';
|
|
2
|
+
import { defu } from 'defu';
|
|
3
|
+
|
|
4
|
+
const module$1 = defineNuxtModule({
|
|
5
|
+
meta: {
|
|
6
|
+
name: "nuxt-crud-modals",
|
|
7
|
+
configKey: "modals",
|
|
8
|
+
compatibility: {
|
|
9
|
+
nuxt: ">=4.0.0"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
// default configuration options of the module
|
|
13
|
+
defaults: {
|
|
14
|
+
prefix: "U",
|
|
15
|
+
loadingDelay: 500
|
|
16
|
+
// ms
|
|
17
|
+
},
|
|
18
|
+
setup(options, nuxt) {
|
|
19
|
+
const { resolve } = createResolver(import.meta.url);
|
|
20
|
+
nuxt.options.runtimeConfig.public.modals = defu(
|
|
21
|
+
nuxt.options.runtimeConfig.public.modals,
|
|
22
|
+
{
|
|
23
|
+
loadingDelay: options.loadingDelay
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
addComponentsDir({
|
|
27
|
+
path: resolve("runtime/app/components"),
|
|
28
|
+
prefix: options.prefix
|
|
29
|
+
});
|
|
30
|
+
addImports({
|
|
31
|
+
name: "useLoadingOverlay",
|
|
32
|
+
from: resolve("runtime/app/composables/useLoadingOverlay")
|
|
33
|
+
});
|
|
34
|
+
addImports({
|
|
35
|
+
name: "defineCrudModals",
|
|
36
|
+
from: resolve("runtime/app/utils/modalFactory")
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export { module$1 as default };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ZodType } from 'zod';
|
|
2
|
+
import type { FormSubmitEvent } from '@nuxt/ui';
|
|
3
|
+
import type { CrudModalResult } from '../utils/modalFactory.js';
|
|
4
|
+
declare const __VLS_export: <T extends Record<string, any>>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
|
|
5
|
+
props: import("vue").PublicProps & __VLS_PrettifyLocal<({
|
|
6
|
+
schema: ZodType<T>;
|
|
7
|
+
initialState: T;
|
|
8
|
+
onSubmit: (event: FormSubmitEvent<T>) => void | Promise<void>;
|
|
9
|
+
} & {
|
|
10
|
+
open?: boolean;
|
|
11
|
+
}) & {
|
|
12
|
+
onClose?: ((value: CrudModalResult) => any) | undefined;
|
|
13
|
+
"onUpdate:open"?: ((value: boolean | undefined) => any) | undefined;
|
|
14
|
+
}> & (typeof globalThis extends {
|
|
15
|
+
__VLS_PROPS_FALLBACK: infer P;
|
|
16
|
+
} ? P : {});
|
|
17
|
+
expose: (exposed: import("vue").ShallowUnwrapRef<{
|
|
18
|
+
setError: (message: string) => void;
|
|
19
|
+
}>) => void;
|
|
20
|
+
attrs: any;
|
|
21
|
+
slots: {
|
|
22
|
+
fields?: (props: {
|
|
23
|
+
state: import("@vue/reactivity").DistributeRef<import("vue").Reactive<T>>;
|
|
24
|
+
}) => any;
|
|
25
|
+
} & {
|
|
26
|
+
buttons?: (props: {}) => any;
|
|
27
|
+
};
|
|
28
|
+
emit: ((evt: "close", value: CrudModalResult) => void) & ((event: "update:open", value: boolean | undefined) => void);
|
|
29
|
+
}>) => import("vue").VNode & {
|
|
30
|
+
__ctx?: Awaited<typeof __VLS_setup>;
|
|
31
|
+
};
|
|
32
|
+
declare const _default: typeof __VLS_export;
|
|
33
|
+
export default _default;
|
|
34
|
+
type __VLS_PrettifyLocal<T> = (T extends any ? {
|
|
35
|
+
[K in keyof T]: T[K];
|
|
36
|
+
} : {
|
|
37
|
+
[K in keyof T as K]: T[K];
|
|
38
|
+
}) & {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<UModal v-model:open="open">
|
|
3
|
+
<template #body>
|
|
4
|
+
<UForm
|
|
5
|
+
ref="form"
|
|
6
|
+
class="space-y-4"
|
|
7
|
+
:state="state"
|
|
8
|
+
:schema="schema"
|
|
9
|
+
@submit="submit"
|
|
10
|
+
>
|
|
11
|
+
<slot
|
|
12
|
+
name="fields"
|
|
13
|
+
:state="state"
|
|
14
|
+
/>
|
|
15
|
+
|
|
16
|
+
<UAlert
|
|
17
|
+
v-if="error"
|
|
18
|
+
:description="error"
|
|
19
|
+
variant="subtle"
|
|
20
|
+
color="error"
|
|
21
|
+
icon="i-lucide-triangle-alert"
|
|
22
|
+
/>
|
|
23
|
+
|
|
24
|
+
<div class="flex flex-row-reverse gap-2">
|
|
25
|
+
<slot name="buttons">
|
|
26
|
+
<UButton
|
|
27
|
+
type="submit"
|
|
28
|
+
loading-auto
|
|
29
|
+
>
|
|
30
|
+
Submit
|
|
31
|
+
</UButton>
|
|
32
|
+
</slot>
|
|
33
|
+
</div>
|
|
34
|
+
</UForm>
|
|
35
|
+
</template>
|
|
36
|
+
</UModal>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup>
|
|
40
|
+
import { reactive, ref, useTemplateRef, watch } from "vue";
|
|
41
|
+
const props = defineProps({
|
|
42
|
+
schema: { type: Object, required: true },
|
|
43
|
+
initialState: { type: null, required: true },
|
|
44
|
+
onSubmit: { type: Function, required: true }
|
|
45
|
+
});
|
|
46
|
+
const emit = defineEmits(["close"]);
|
|
47
|
+
const error = ref();
|
|
48
|
+
const open = defineModel("open", { type: Boolean });
|
|
49
|
+
const form = useTemplateRef("form");
|
|
50
|
+
const state = reactive({ ...props.initialState });
|
|
51
|
+
function setError(message) {
|
|
52
|
+
error.value = message;
|
|
53
|
+
}
|
|
54
|
+
defineExpose({ setError });
|
|
55
|
+
async function submit(event) {
|
|
56
|
+
try {
|
|
57
|
+
await props.onSubmit(event);
|
|
58
|
+
emit("close", { success: true });
|
|
59
|
+
open.value = false;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
error.value = "Unknown error";
|
|
62
|
+
if (err instanceof Error)
|
|
63
|
+
error.value = err.message;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
watch(open, (newVal) => {
|
|
67
|
+
if (newVal) {
|
|
68
|
+
Object.assign(state, props.initialState);
|
|
69
|
+
error.value = void 0;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
watch(
|
|
73
|
+
() => form.value?.dirty,
|
|
74
|
+
(newVal) => {
|
|
75
|
+
if (newVal) {
|
|
76
|
+
error.value = void 0;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
</script>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ZodType } from 'zod';
|
|
2
|
+
import type { FormSubmitEvent } from '@nuxt/ui';
|
|
3
|
+
import type { CrudModalResult } from '../utils/modalFactory.js';
|
|
4
|
+
declare const __VLS_export: <T extends Record<string, any>>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
|
|
5
|
+
props: import("vue").PublicProps & __VLS_PrettifyLocal<({
|
|
6
|
+
schema: ZodType<T>;
|
|
7
|
+
initialState: T;
|
|
8
|
+
onSubmit: (event: FormSubmitEvent<T>) => void | Promise<void>;
|
|
9
|
+
} & {
|
|
10
|
+
open?: boolean;
|
|
11
|
+
}) & {
|
|
12
|
+
onClose?: ((value: CrudModalResult) => any) | undefined;
|
|
13
|
+
"onUpdate:open"?: ((value: boolean | undefined) => any) | undefined;
|
|
14
|
+
}> & (typeof globalThis extends {
|
|
15
|
+
__VLS_PROPS_FALLBACK: infer P;
|
|
16
|
+
} ? P : {});
|
|
17
|
+
expose: (exposed: import("vue").ShallowUnwrapRef<{
|
|
18
|
+
setError: (message: string) => void;
|
|
19
|
+
}>) => void;
|
|
20
|
+
attrs: any;
|
|
21
|
+
slots: {
|
|
22
|
+
fields?: (props: {
|
|
23
|
+
state: import("@vue/reactivity").DistributeRef<import("vue").Reactive<T>>;
|
|
24
|
+
}) => any;
|
|
25
|
+
} & {
|
|
26
|
+
buttons?: (props: {}) => any;
|
|
27
|
+
};
|
|
28
|
+
emit: ((evt: "close", value: CrudModalResult) => void) & ((event: "update:open", value: boolean | undefined) => void);
|
|
29
|
+
}>) => import("vue").VNode & {
|
|
30
|
+
__ctx?: Awaited<typeof __VLS_setup>;
|
|
31
|
+
};
|
|
32
|
+
declare const _default: typeof __VLS_export;
|
|
33
|
+
export default _default;
|
|
34
|
+
type __VLS_PrettifyLocal<T> = (T extends any ? {
|
|
35
|
+
[K in keyof T]: T[K];
|
|
36
|
+
} : {
|
|
37
|
+
[K in keyof T as K]: T[K];
|
|
38
|
+
}) & {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Teleport to="body">
|
|
3
|
+
<Transition
|
|
4
|
+
appear
|
|
5
|
+
enter-active-class="transition-opacity duration-150 ease-out"
|
|
6
|
+
enter-from-class="opacity-0"
|
|
7
|
+
enter-to-class="opacity-100"
|
|
8
|
+
leave-active-class="transition-opacity duration-100 ease-in"
|
|
9
|
+
leave-from-class="opacity-100"
|
|
10
|
+
leave-to-class="opacity-0"
|
|
11
|
+
>
|
|
12
|
+
<div
|
|
13
|
+
v-if="state.isActive && state.blocking"
|
|
14
|
+
class="fixed inset-0 z-100 flex items-center justify-center cursor-progress"
|
|
15
|
+
:class="{
|
|
16
|
+
'bg-elevated/75': state.backdrop
|
|
17
|
+
}"
|
|
18
|
+
>
|
|
19
|
+
<Transition
|
|
20
|
+
enter-active-class="transition-all duration-300 ease-out"
|
|
21
|
+
leave-active-class="transition-all duration-200 ease-in"
|
|
22
|
+
enter-from-class="opacity-0 scale-95"
|
|
23
|
+
enter-to-class="opacity-100 scale-100"
|
|
24
|
+
leave-from-class="opacity-100 scale-100"
|
|
25
|
+
leave-to-class="opacity-0 scale-95"
|
|
26
|
+
>
|
|
27
|
+
<div
|
|
28
|
+
v-if="state.isVisible"
|
|
29
|
+
class="bg-default rounded-lg shadow-lg ring ring-default p-4 sm:p-6 flex flex-col items-center gap-2 min-w-30"
|
|
30
|
+
>
|
|
31
|
+
<UIcon
|
|
32
|
+
name="i-lucide-loader-circle"
|
|
33
|
+
class="size-8 text-primary animate-spin"
|
|
34
|
+
/>
|
|
35
|
+
<span
|
|
36
|
+
v-if="state.label"
|
|
37
|
+
class="text-sm text-muted font-medium"
|
|
38
|
+
>
|
|
39
|
+
{{ state.label }}
|
|
40
|
+
</span>
|
|
41
|
+
</div>
|
|
42
|
+
</Transition>
|
|
43
|
+
</div>
|
|
44
|
+
</Transition>
|
|
45
|
+
</Teleport>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<script setup>
|
|
49
|
+
import { useLoadingOverlay } from "../composables/useLoadingOverlay";
|
|
50
|
+
const { state } = useLoadingOverlay();
|
|
51
|
+
</script>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
declare const _default: typeof __VLS_export;
|
|
3
|
+
export default _default;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const useLoadingOverlay: () => {
|
|
2
|
+
state: Readonly<import("vue").Ref<{
|
|
3
|
+
readonly isActive: boolean;
|
|
4
|
+
readonly isVisible: boolean;
|
|
5
|
+
readonly backdrop: boolean;
|
|
6
|
+
readonly blocking: boolean;
|
|
7
|
+
readonly delay: number;
|
|
8
|
+
readonly label: string | null;
|
|
9
|
+
}, {
|
|
10
|
+
readonly isActive: boolean;
|
|
11
|
+
readonly isVisible: boolean;
|
|
12
|
+
readonly backdrop: boolean;
|
|
13
|
+
readonly blocking: boolean;
|
|
14
|
+
readonly delay: number;
|
|
15
|
+
readonly label: string | null;
|
|
16
|
+
}>>;
|
|
17
|
+
show: (options?: {
|
|
18
|
+
backdrop?: boolean;
|
|
19
|
+
blocking?: boolean;
|
|
20
|
+
delay?: number;
|
|
21
|
+
label?: string | null;
|
|
22
|
+
}) => void;
|
|
23
|
+
hide: () => void;
|
|
24
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useRuntimeConfig, useState } from "#app";
|
|
2
|
+
import { readonly } from "vue";
|
|
3
|
+
let timer = null;
|
|
4
|
+
export const useLoadingOverlay = () => {
|
|
5
|
+
const config = useRuntimeConfig();
|
|
6
|
+
const loadingDelay = config.public.modals.loadingDelay;
|
|
7
|
+
const state = useState("loading-overlay", () => ({
|
|
8
|
+
isActive: false,
|
|
9
|
+
isVisible: false,
|
|
10
|
+
backdrop: false,
|
|
11
|
+
blocking: true,
|
|
12
|
+
delay: loadingDelay,
|
|
13
|
+
label: "Loading..."
|
|
14
|
+
}));
|
|
15
|
+
function show(options) {
|
|
16
|
+
state.value.backdrop = options?.backdrop === true ? true : false;
|
|
17
|
+
state.value.blocking = options?.blocking === false ? false : true;
|
|
18
|
+
state.value.delay = options?.delay ?? loadingDelay;
|
|
19
|
+
state.value.label = options?.label === null ? null : "Loading...";
|
|
20
|
+
state.value.isActive = true;
|
|
21
|
+
timer = setTimeout(() => state.value.isVisible = true, state.value.delay);
|
|
22
|
+
}
|
|
23
|
+
function hide() {
|
|
24
|
+
if (timer) {
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
timer = null;
|
|
27
|
+
}
|
|
28
|
+
state.value.isActive = false;
|
|
29
|
+
state.value.isVisible = false;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
state: readonly(state),
|
|
33
|
+
show,
|
|
34
|
+
hide
|
|
35
|
+
};
|
|
36
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Component } from 'vue';
|
|
2
|
+
type RecordId = string | number;
|
|
3
|
+
type CrudModalMode = 'create' | 'read' | 'update';
|
|
4
|
+
export type CrudModalResult = {
|
|
5
|
+
success: boolean;
|
|
6
|
+
};
|
|
7
|
+
type CrudModalConfig<TRecord extends {
|
|
8
|
+
id: RecordId;
|
|
9
|
+
}> = {
|
|
10
|
+
components: Partial<Record<CrudModalMode, Component>>;
|
|
11
|
+
fetchData?: (id: RecordId) => Promise<TRecord>;
|
|
12
|
+
};
|
|
13
|
+
export declare function defineCrudModals<TRecord extends {
|
|
14
|
+
id: RecordId;
|
|
15
|
+
}>(config: CrudModalConfig<TRecord>): () => {
|
|
16
|
+
data: [TRecord | null] extends [import("vue").Ref<any, any>] ? import("@vue/shared").IfAny<import("vue").Ref<any, any> & TRecord, import("vue").Ref<import("vue").Ref<any, any> & TRecord, import("vue").Ref<any, any> & TRecord>, import("vue").Ref<any, any> & TRecord> : import("vue").Ref<import("vue").UnwrapRef<TRecord> | null, TRecord | import("vue").UnwrapRef<TRecord> | null>;
|
|
17
|
+
error: import("vue").Ref<string | null, string | null>;
|
|
18
|
+
isLoading: import("vue").Ref<boolean, boolean>;
|
|
19
|
+
isOpen: import("vue").Ref<boolean, boolean>;
|
|
20
|
+
openToCreate: (initialState?: Partial<TRecord>) => Promise<CrudModalResult | undefined>;
|
|
21
|
+
openToRead: (id: RecordId) => Promise<CrudModalResult | undefined>;
|
|
22
|
+
openToUpdate: (id: RecordId) => Promise<CrudModalResult | undefined>;
|
|
23
|
+
refresh: () => Promise<void>;
|
|
24
|
+
reset: () => void;
|
|
25
|
+
};
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createError } from "#app";
|
|
2
|
+
import { useLoadingOverlay } from "#imports";
|
|
3
|
+
import { useOverlay } from "@nuxt/ui/runtime/composables/useOverlay.js";
|
|
4
|
+
import { useToast } from "@nuxt/ui/runtime/composables/useToast.js";
|
|
5
|
+
import { ref } from "vue";
|
|
6
|
+
export function defineCrudModals(config) {
|
|
7
|
+
const overlay = useOverlay();
|
|
8
|
+
const data = ref(null);
|
|
9
|
+
const error = ref(null);
|
|
10
|
+
const isLoading = ref(false);
|
|
11
|
+
const isOpen = ref(false);
|
|
12
|
+
function resolveComponent(mode) {
|
|
13
|
+
if (!config.components[mode])
|
|
14
|
+
throw createError({
|
|
15
|
+
status: 500,
|
|
16
|
+
statusText: `No component registered for modal mode '${mode}'`
|
|
17
|
+
});
|
|
18
|
+
return config.components[mode];
|
|
19
|
+
}
|
|
20
|
+
async function fetchData(id) {
|
|
21
|
+
if (!config.fetchData) return;
|
|
22
|
+
setLoading(true);
|
|
23
|
+
try {
|
|
24
|
+
data.value = await config.fetchData(id);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
const toast = useToast();
|
|
27
|
+
toast.add({
|
|
28
|
+
title: "Failed to fetch record",
|
|
29
|
+
description: err instanceof Error ? err.message : "Unknown error",
|
|
30
|
+
color: "error"
|
|
31
|
+
});
|
|
32
|
+
} finally {
|
|
33
|
+
setLoading(false);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function open(mode, payload) {
|
|
37
|
+
reset();
|
|
38
|
+
const component = resolveComponent(mode);
|
|
39
|
+
if (mode !== "create") {
|
|
40
|
+
const { id } = payload;
|
|
41
|
+
if (!id) throw new Error(`Mode '${mode}' requires an id`);
|
|
42
|
+
await fetchData(id);
|
|
43
|
+
if (!data.value) return;
|
|
44
|
+
} else if (mode === "create") {
|
|
45
|
+
const { initialState } = payload;
|
|
46
|
+
data.value = initialState;
|
|
47
|
+
}
|
|
48
|
+
isOpen.value = true;
|
|
49
|
+
const promise = overlay.create(component).open({ mode, ...payload });
|
|
50
|
+
promise.finally(() => isOpen.value = false);
|
|
51
|
+
return promise;
|
|
52
|
+
}
|
|
53
|
+
function openToRead(id) {
|
|
54
|
+
return open("read", { id });
|
|
55
|
+
}
|
|
56
|
+
function openToUpdate(id) {
|
|
57
|
+
return open("update", { id });
|
|
58
|
+
}
|
|
59
|
+
function openToCreate(initialState) {
|
|
60
|
+
return open("create", { initialState });
|
|
61
|
+
}
|
|
62
|
+
async function refresh() {
|
|
63
|
+
if (!data.value?.id) return;
|
|
64
|
+
return fetchData(data.value.id);
|
|
65
|
+
}
|
|
66
|
+
function reset() {
|
|
67
|
+
data.value = null;
|
|
68
|
+
error.value = null;
|
|
69
|
+
isOpen.value = false;
|
|
70
|
+
}
|
|
71
|
+
function setLoading(loading) {
|
|
72
|
+
isLoading.value = loading;
|
|
73
|
+
if (!isOpen.value) {
|
|
74
|
+
const loadingOverlay = useLoadingOverlay();
|
|
75
|
+
if (loading) loadingOverlay.show();
|
|
76
|
+
else loadingOverlay.hide();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return () => ({
|
|
80
|
+
data,
|
|
81
|
+
error,
|
|
82
|
+
isLoading,
|
|
83
|
+
isOpen,
|
|
84
|
+
openToCreate,
|
|
85
|
+
openToRead,
|
|
86
|
+
openToUpdate,
|
|
87
|
+
refresh,
|
|
88
|
+
reset
|
|
89
|
+
});
|
|
90
|
+
}
|
package/dist/types.d.mts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nuxt-crud-modals",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Nuxt UI compatible module to handle modals for creating, editing and viewing id based records",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/tosling/nuxt-crud-modals.git"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/types.d.mts",
|
|
14
|
+
"import": "./dist/module.mjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"main": "./dist/module.mjs",
|
|
18
|
+
"typesVersions": {
|
|
19
|
+
"*": {
|
|
20
|
+
".": [
|
|
21
|
+
"./dist/types.d.mts"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"workspaces": [
|
|
29
|
+
"playground"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"prepack": "nuxt-module-build build",
|
|
33
|
+
"dev": "npm run dev:prepare && nuxt dev playground",
|
|
34
|
+
"dev:build": "nuxt build playground",
|
|
35
|
+
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
|
|
36
|
+
"release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
|
|
37
|
+
"lint": "eslint .",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:watch": "vitest watch",
|
|
40
|
+
"test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@nuxt/kit": "^4.4.2",
|
|
44
|
+
"defu": "^6.1.6"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@nuxt/devtools": "^3.2.4",
|
|
48
|
+
"@nuxt/eslint-config": "^1.15.2",
|
|
49
|
+
"@nuxt/module-builder": "^1.0.2",
|
|
50
|
+
"@nuxt/schema": "^4.4.2",
|
|
51
|
+
"@nuxt/test-utils": "^4.0.0",
|
|
52
|
+
"@types/node": "latest",
|
|
53
|
+
"changelogen": "^0.6.2",
|
|
54
|
+
"eslint": "^10.1.0",
|
|
55
|
+
"nuxt": "^4.4.2",
|
|
56
|
+
"typescript": "~6.0.2",
|
|
57
|
+
"vitest": "^4.1.2",
|
|
58
|
+
"vue-tsc": "^3.2.6"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@nuxt/ui": "^4.6.0",
|
|
62
|
+
"nuxt": "^4.4.2",
|
|
63
|
+
"zod": "^4.0.0"
|
|
64
|
+
}
|
|
65
|
+
}
|