@mapomodule/form 2.0.0-beta.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/README.md +155 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +34 -0
- package/dist/module.d.cts +2 -0
- package/dist/module.d.mts +2 -0
- package/dist/module.d.ts +2 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +18 -0
- package/dist/types.d.mts +13 -0
- package/package.json +78 -0
- package/src/__tests__/__mocks__/nuxt-imports.ts +5 -0
- package/src/__tests__/registry.test.ts +105 -0
- package/src/__tests__/useFormFromSchema.test.ts +415 -0
- package/src/__tests__/useMapoForm.test.ts +319 -0
- package/src/index.ts +49 -0
- package/src/module.ts +213 -0
- package/src/runtime/components/MapoFocusPortal.vue +154 -0
- package/src/runtime/components/MapoForm.vue +247 -0
- package/src/runtime/components/MapoFormField.vue +350 -0
- package/src/runtime/components/MapoFormFlatSection.vue +163 -0
- package/src/runtime/components/MapoFormGroup.vue +283 -0
- package/src/runtime/components/MapoFormTabs.vue +116 -0
- package/src/runtime/components/MapoUnknownField.vue +74 -0
- package/src/runtime/components/fields/MapoDateField.vue +98 -0
- package/src/runtime/components/fields/MapoDateTimeField.vue +192 -0
- package/src/runtime/components/fields/MapoFileField.vue +106 -0
- package/src/runtime/components/fields/MapoFksField.vue +151 -0
- package/src/runtime/components/fields/MapoMapField.vue +117 -0
- package/src/runtime/components/fields/MapoMapFieldClient.vue +92 -0
- package/src/runtime/components/fields/MapoRepeater.vue +553 -0
- package/src/runtime/components/fields/MapoRepeaterItem.vue +409 -0
- package/src/runtime/components/fields/MapoSeoPreview.vue +137 -0
- package/src/runtime/components/fields/MapoTimeField.vue +59 -0
- package/src/runtime/components/fields/MapoWygEditor.vue +256 -0
- package/src/runtime/components/fields/nui/NuiCheckbox.vue +8 -0
- package/src/runtime/components/fields/nui/NuiInput.vue +8 -0
- package/src/runtime/components/fields/nui/NuiSelectMenu.vue +27 -0
- package/src/runtime/components/fields/nui/NuiSlider.vue +8 -0
- package/src/runtime/components/fields/nui/NuiSwitch.vue +8 -0
- package/src/runtime/components/fields/nui/NuiTextarea.vue +8 -0
- package/src/runtime/components/fields/nui/useNuiFieldAttrs.ts +30 -0
- package/src/runtime/composables/defineFormField.ts +62 -0
- package/src/runtime/composables/useCurrentLang.ts +16 -0
- package/src/runtime/composables/useFocusMode.ts +86 -0
- package/src/runtime/composables/useFormDraft.ts +119 -0
- package/src/runtime/composables/useFormFromSchema.ts +253 -0
- package/src/runtime/composables/useMapoForm.ts +484 -0
- package/src/runtime/composables/useProgressiveDisclosure.ts +101 -0
- package/src/runtime/composables/useUnsavedChangesGuard.ts +58 -0
- package/src/runtime/devtools/DevtoolsFormsPage.vue +256 -0
- package/src/runtime/plugins/form-registry.ts +43 -0
- package/src/runtime/registry/defaults.ts +97 -0
- package/src/runtime/types/descriptor.ts +411 -0
- package/src/runtime/types/index.ts +40 -0
- package/src/runtime/types/layout.ts +22 -0
- package/src/runtime/types/registry.ts +98 -0
- package/src/runtime/utils/flattenFieldGroups.ts +86 -0
- package/src/vue-shim.d.ts +12 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { ref } from "vue";
|
|
3
|
+
import { useMapoForm } from "../runtime/composables/useMapoForm.js";
|
|
4
|
+
import { defaultRegistry } from "../runtime/registry/defaults.js";
|
|
5
|
+
import type { FieldDescriptor } from "../runtime/types/descriptor.js";
|
|
6
|
+
|
|
7
|
+
vi.mock("@mapomodule/store/runtime/stores/auth", () => ({
|
|
8
|
+
useAuthStore: () => ({ info: { id: 1, username: "testuser" } }),
|
|
9
|
+
}));
|
|
10
|
+
vi.mock("@mapomodule/store/runtime/stores/snack", () => ({
|
|
11
|
+
useSnackStore: () => ({ show: vi.fn() }),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
// provide/inject called outside setup() emits a warning; silence it in tests.
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
interface Article {
|
|
20
|
+
title: string;
|
|
21
|
+
slug: string;
|
|
22
|
+
is_draft: boolean;
|
|
23
|
+
count: number;
|
|
24
|
+
translations: Record<string, Partial<Article>>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeForm(overrides: Partial<Article> = {}) {
|
|
28
|
+
const model = ref<Article>({
|
|
29
|
+
title: "",
|
|
30
|
+
slug: "",
|
|
31
|
+
is_draft: false,
|
|
32
|
+
count: 0,
|
|
33
|
+
translations: {},
|
|
34
|
+
...overrides,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const fields = ref<FieldDescriptor<Article>[]>([
|
|
38
|
+
{ key: "title", type: "text" as const },
|
|
39
|
+
{ key: "slug", type: "text" as const },
|
|
40
|
+
{ key: "is_draft", type: "switch" as const },
|
|
41
|
+
{ key: "count", type: "number" as const },
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const form = useMapoForm({
|
|
45
|
+
model,
|
|
46
|
+
fields,
|
|
47
|
+
registry: defaultRegistry,
|
|
48
|
+
immediate: true,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return { model, form };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── isDirty ─────────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
describe("isDirty", () => {
|
|
57
|
+
it("is false on init", () => {
|
|
58
|
+
const { form } = makeForm();
|
|
59
|
+
expect(form.isDirty.value).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("is true after setFieldValue", () => {
|
|
63
|
+
const { form } = makeForm();
|
|
64
|
+
form.setFieldValue("title", "Hello");
|
|
65
|
+
expect(form.isDirty.value).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("resets to false after resetDirty", () => {
|
|
69
|
+
const { form } = makeForm();
|
|
70
|
+
form.setFieldValue("title", "Hello");
|
|
71
|
+
form.resetDirty();
|
|
72
|
+
expect(form.isDirty.value).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ─── setFieldValue / getFieldValue ───────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
describe("setFieldValue", () => {
|
|
79
|
+
it("writes the value to model", () => {
|
|
80
|
+
const { model, form } = makeForm();
|
|
81
|
+
form.setFieldValue("title", "Mapo");
|
|
82
|
+
expect(model.value.title).toBe("Mapo");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("writes nested path (dotted key)", () => {
|
|
86
|
+
const { model } = makeForm();
|
|
87
|
+
// meta.slug is intentionally not a key of Article — tests runtime dotted-path writes on arbitrary paths
|
|
88
|
+
const extendedFields = ref<FieldDescriptor<any>[]>([
|
|
89
|
+
{ key: "meta.slug", type: "text" as const },
|
|
90
|
+
]);
|
|
91
|
+
const extForm = useMapoForm({
|
|
92
|
+
model,
|
|
93
|
+
fields: extendedFields,
|
|
94
|
+
registry: defaultRegistry,
|
|
95
|
+
immediate: true,
|
|
96
|
+
});
|
|
97
|
+
extForm.setFieldValue("meta.slug", "hello");
|
|
98
|
+
expect((model.value as any).meta.slug).toBe("hello");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("getFieldValue", () => {
|
|
103
|
+
it("reads the current value", () => {
|
|
104
|
+
const { form } = makeForm({ title: "Existing" });
|
|
105
|
+
expect(form.getFieldValue({ key: "title", type: "text" })).toBe("Existing");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ─── getPatch ────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
describe("getPatch", () => {
|
|
112
|
+
it("returns empty on no changes", () => {
|
|
113
|
+
const { form } = makeForm();
|
|
114
|
+
expect(form.getPatch()).toEqual({});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("returns only changed fields", () => {
|
|
118
|
+
const { form } = makeForm({ title: "Old", slug: "old-slug" });
|
|
119
|
+
form.setFieldValue("title", "New");
|
|
120
|
+
const patch = form.getPatch();
|
|
121
|
+
expect(patch).toHaveProperty("title", "New");
|
|
122
|
+
expect(patch).not.toHaveProperty("slug");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ─── validateClient ───────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
describe("validateClient", () => {
|
|
129
|
+
it("returns valid=true when no validate callbacks", () => {
|
|
130
|
+
const { form } = makeForm();
|
|
131
|
+
const { valid } = form.validateClient();
|
|
132
|
+
expect(valid).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("returns valid=false when a validate callback returns error", () => {
|
|
136
|
+
const model = ref<Article>({
|
|
137
|
+
title: "",
|
|
138
|
+
slug: "",
|
|
139
|
+
is_draft: false,
|
|
140
|
+
count: 0,
|
|
141
|
+
translations: {},
|
|
142
|
+
});
|
|
143
|
+
const fields = ref<FieldDescriptor<Article>[]>([
|
|
144
|
+
{
|
|
145
|
+
key: "title",
|
|
146
|
+
type: "text" as const,
|
|
147
|
+
validate: (val: unknown) => (val ? null : "Required"),
|
|
148
|
+
},
|
|
149
|
+
]);
|
|
150
|
+
const form = useMapoForm({
|
|
151
|
+
model,
|
|
152
|
+
fields,
|
|
153
|
+
registry: defaultRegistry,
|
|
154
|
+
immediate: true,
|
|
155
|
+
});
|
|
156
|
+
const { valid, errors } = form.validateClient();
|
|
157
|
+
expect(valid).toBe(false);
|
|
158
|
+
expect(errors.title).toBe("Required");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("returns valid=true when validate callback returns null", () => {
|
|
162
|
+
const model = ref<Article>({
|
|
163
|
+
title: "Hello",
|
|
164
|
+
slug: "",
|
|
165
|
+
is_draft: false,
|
|
166
|
+
count: 0,
|
|
167
|
+
translations: {},
|
|
168
|
+
});
|
|
169
|
+
const fields = ref<FieldDescriptor<Article>[]>([
|
|
170
|
+
{
|
|
171
|
+
key: "title",
|
|
172
|
+
type: "text" as const,
|
|
173
|
+
validate: (val: unknown) => (val ? null : "Required"),
|
|
174
|
+
},
|
|
175
|
+
]);
|
|
176
|
+
const form = useMapoForm({
|
|
177
|
+
model,
|
|
178
|
+
fields,
|
|
179
|
+
registry: defaultRegistry,
|
|
180
|
+
immediate: true,
|
|
181
|
+
});
|
|
182
|
+
expect(form.validateClient().valid).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ─── i18n: translatable + initLang ───────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
describe("translatable fields", () => {
|
|
189
|
+
it("writes to translations.<lang>.<key>", () => {
|
|
190
|
+
const model = ref<Article>({
|
|
191
|
+
title: "",
|
|
192
|
+
slug: "",
|
|
193
|
+
is_draft: false,
|
|
194
|
+
count: 0,
|
|
195
|
+
translations: {},
|
|
196
|
+
});
|
|
197
|
+
const fields = ref<FieldDescriptor<Article>[]>([
|
|
198
|
+
{ key: "title", type: "text" as const, translatable: true },
|
|
199
|
+
]);
|
|
200
|
+
const currentLang = ref("it");
|
|
201
|
+
const form = useMapoForm({
|
|
202
|
+
model,
|
|
203
|
+
fields,
|
|
204
|
+
registry: defaultRegistry,
|
|
205
|
+
immediate: true,
|
|
206
|
+
currentLang,
|
|
207
|
+
});
|
|
208
|
+
form.setFieldValue("title", "Ciao");
|
|
209
|
+
expect(model.value.translations?.it?.title).toBe("Ciao");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("auto-initializes translations[lang] = {} (initLang)", () => {
|
|
213
|
+
const model = ref<Article>({
|
|
214
|
+
title: "",
|
|
215
|
+
slug: "",
|
|
216
|
+
is_draft: false,
|
|
217
|
+
count: 0,
|
|
218
|
+
translations: {},
|
|
219
|
+
});
|
|
220
|
+
const fields = ref<FieldDescriptor<Article>[]>([
|
|
221
|
+
{ key: "title", type: "text" as const, translatable: true },
|
|
222
|
+
]);
|
|
223
|
+
const currentLang = ref("de");
|
|
224
|
+
const form = useMapoForm({
|
|
225
|
+
model,
|
|
226
|
+
fields,
|
|
227
|
+
registry: defaultRegistry,
|
|
228
|
+
immediate: true,
|
|
229
|
+
currentLang,
|
|
230
|
+
});
|
|
231
|
+
form.setFieldValue("title", "Hallo");
|
|
232
|
+
expect(model.value.translations).toHaveProperty("de");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("propagates synci18n to all languages", () => {
|
|
236
|
+
const model = ref<Article>({
|
|
237
|
+
title: "",
|
|
238
|
+
slug: "",
|
|
239
|
+
is_draft: false,
|
|
240
|
+
count: 0,
|
|
241
|
+
translations: {},
|
|
242
|
+
});
|
|
243
|
+
const fields = ref<FieldDescriptor<Article>[]>([
|
|
244
|
+
{
|
|
245
|
+
key: "title",
|
|
246
|
+
type: "text" as const,
|
|
247
|
+
translatable: true,
|
|
248
|
+
synci18n: true,
|
|
249
|
+
},
|
|
250
|
+
]);
|
|
251
|
+
const currentLang = ref("it");
|
|
252
|
+
const form = useMapoForm({
|
|
253
|
+
model,
|
|
254
|
+
fields,
|
|
255
|
+
registry: defaultRegistry,
|
|
256
|
+
immediate: true,
|
|
257
|
+
currentLang,
|
|
258
|
+
languages: ["en", "it"],
|
|
259
|
+
});
|
|
260
|
+
form.setFieldValue("title", "Synced");
|
|
261
|
+
expect(model.value.translations?.en?.title).toBe("Synced");
|
|
262
|
+
expect(model.value.translations?.it?.title).toBe("Synced");
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// ─── submit ───────────────────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
describe("submit", () => {
|
|
269
|
+
it("calls handler and resets dirty on success", async () => {
|
|
270
|
+
const { form } = makeForm();
|
|
271
|
+
form.setFieldValue("title", "X");
|
|
272
|
+
const handler = vi.fn().mockResolvedValue(undefined);
|
|
273
|
+
await form.submit(handler);
|
|
274
|
+
expect(handler).toHaveBeenCalled();
|
|
275
|
+
expect(form.isDirty.value).toBe(false);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("does not call handler when validateClient fails", async () => {
|
|
279
|
+
const model = ref<Article>({
|
|
280
|
+
title: "",
|
|
281
|
+
slug: "",
|
|
282
|
+
is_draft: false,
|
|
283
|
+
count: 0,
|
|
284
|
+
translations: {},
|
|
285
|
+
});
|
|
286
|
+
const fields = ref<FieldDescriptor<Article>[]>([
|
|
287
|
+
{
|
|
288
|
+
key: "title",
|
|
289
|
+
type: "text" as const,
|
|
290
|
+
validate: (v: unknown) => (v ? null : "err"),
|
|
291
|
+
},
|
|
292
|
+
]);
|
|
293
|
+
const form = useMapoForm({
|
|
294
|
+
model,
|
|
295
|
+
fields,
|
|
296
|
+
registry: defaultRegistry,
|
|
297
|
+
immediate: true,
|
|
298
|
+
});
|
|
299
|
+
const handler = vi.fn();
|
|
300
|
+
await form.submit(handler);
|
|
301
|
+
expect(handler).not.toHaveBeenCalled();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("passes full model when isNew=true, patch when isNew=false", async () => {
|
|
305
|
+
const { form } = makeForm({ title: "Old" });
|
|
306
|
+
form.setFieldValue("title", "New");
|
|
307
|
+
const calls: unknown[] = [];
|
|
308
|
+
await form.submit(async (data) => {
|
|
309
|
+
calls.push(data);
|
|
310
|
+
}, false);
|
|
311
|
+
expect(calls[0]).toEqual({ title: "New" }); // diff only
|
|
312
|
+
|
|
313
|
+
form.setFieldValue("slug", "new-slug");
|
|
314
|
+
await form.submit(async (data) => {
|
|
315
|
+
calls.push(data);
|
|
316
|
+
}, true);
|
|
317
|
+
expect(calls[1]).toHaveProperty("slug", "new-slug"); // full model
|
|
318
|
+
});
|
|
319
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type { MapoFormOptions } from "./module.js";
|
|
2
|
+
export { useFormFromSchema } from "./runtime/composables/useFormFromSchema.js";
|
|
3
|
+
export type { UseFormFromSchemaOptions } from "./runtime/composables/useFormFromSchema.js";
|
|
4
|
+
export { useFormDraft } from "./runtime/composables/useFormDraft.js";
|
|
5
|
+
export type { UseFormDraftOptions } from "./runtime/composables/useFormDraft.js";
|
|
6
|
+
export { useFocusMode } from "./runtime/composables/useFocusMode.js";
|
|
7
|
+
export type { FocusTarget } from "./runtime/composables/useFocusMode.js";
|
|
8
|
+
export { useUnsavedChangesGuard } from "./runtime/composables/useUnsavedChangesGuard.js";
|
|
9
|
+
export type { UnsavedChangesGuardOptions } from "./runtime/composables/useUnsavedChangesGuard.js";
|
|
10
|
+
// Progressive disclosure DSL
|
|
11
|
+
export {
|
|
12
|
+
when,
|
|
13
|
+
whenAny,
|
|
14
|
+
whenNot,
|
|
15
|
+
matchesField,
|
|
16
|
+
isOneOf,
|
|
17
|
+
isNoneOf,
|
|
18
|
+
isNotEmpty,
|
|
19
|
+
isEmpty,
|
|
20
|
+
greaterThan,
|
|
21
|
+
lessThan,
|
|
22
|
+
matches,
|
|
23
|
+
} from "./runtime/composables/useProgressiveDisclosure.js";
|
|
24
|
+
export { flattenFieldGroups } from "./runtime/utils/flattenFieldGroups.js";
|
|
25
|
+
export type { FieldGroupDescriptor } from "./runtime/utils/flattenFieldGroups.js";
|
|
26
|
+
export { KnownFieldType } from "./runtime/types/index.js";
|
|
27
|
+
export type {
|
|
28
|
+
FieldType,
|
|
29
|
+
FieldAccessor,
|
|
30
|
+
FieldDescriptor,
|
|
31
|
+
AnyFieldDescriptor,
|
|
32
|
+
TextDescriptor,
|
|
33
|
+
NumberDescriptor,
|
|
34
|
+
BooleanDescriptor,
|
|
35
|
+
SelectDescriptor,
|
|
36
|
+
FksDescriptor,
|
|
37
|
+
DateDescriptor,
|
|
38
|
+
ColorDescriptor,
|
|
39
|
+
FileDescriptor,
|
|
40
|
+
EditorDescriptor,
|
|
41
|
+
SeoDescriptor,
|
|
42
|
+
MapDescriptor,
|
|
43
|
+
RepeaterDescriptor,
|
|
44
|
+
MediaDescriptor,
|
|
45
|
+
CustomDescriptor,
|
|
46
|
+
FieldComponentEntry,
|
|
47
|
+
FieldRegistry,
|
|
48
|
+
PartialFieldRegistry,
|
|
49
|
+
} from "./runtime/types/index.js";
|
package/src/module.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineNuxtModule,
|
|
3
|
+
addComponent,
|
|
4
|
+
addPlugin,
|
|
5
|
+
addTypeTemplate,
|
|
6
|
+
createResolver,
|
|
7
|
+
addImports,
|
|
8
|
+
extendPages,
|
|
9
|
+
} from "@nuxt/kit";
|
|
10
|
+
import type { NuxtModule } from "@nuxt/schema";
|
|
11
|
+
import type { PartialFieldRegistry } from "./runtime/types/index.js";
|
|
12
|
+
|
|
13
|
+
export interface MapoFormOptions {
|
|
14
|
+
fields?: PartialFieldRegistry;
|
|
15
|
+
groups?: { expanded?: boolean };
|
|
16
|
+
debounce?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default defineNuxtModule<MapoFormOptions>({
|
|
20
|
+
meta: {
|
|
21
|
+
name: "@mapomodule/form",
|
|
22
|
+
configKey: "mapoForm",
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
defaults: {
|
|
26
|
+
debounce: 300,
|
|
27
|
+
groups: { expanded: true },
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
async setup(options, nuxt) {
|
|
31
|
+
const resolver = createResolver(import.meta.url);
|
|
32
|
+
|
|
33
|
+
// Only serializable values can go into runtimeConfig.public.
|
|
34
|
+
// `mapping` and `accessor` contain functions → not SSR-serializable.
|
|
35
|
+
// To extend mapping/accessor, use an app plugin that accesses $mapoFormRegistry:
|
|
36
|
+
// export default defineNuxtPlugin(({ $mapoFormRegistry }) => {
|
|
37
|
+
// $mapoFormRegistry.mapping['my-type'] = () => import('~/components/MyField.vue')
|
|
38
|
+
// })
|
|
39
|
+
// `defaults` guarantees groups/debounce at runtime; fall back explicitly
|
|
40
|
+
// so the assignment type is fully defined.
|
|
41
|
+
nuxt.options.runtimeConfig.public.mapoForm = {
|
|
42
|
+
groups: { expanded: options.groups?.expanded ?? true },
|
|
43
|
+
debounce: options.debounce ?? 300,
|
|
44
|
+
// attrs is JSON-serialisable (plain objects) — merged in the plugin
|
|
45
|
+
fields: { attrs: options.fields?.attrs ?? {} },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Plugin: create and inject the global field registry
|
|
49
|
+
addPlugin(resolver.resolve("./runtime/plugins/form-registry"));
|
|
50
|
+
|
|
51
|
+
// Augment NuxtApp with $mapoFormRegistry so consumers get type-safe access.
|
|
52
|
+
// nuxt-module-builder leaves runtime/*.d.ts empty, so InjectionType cannot
|
|
53
|
+
// infer the plugin's provide shape — we inject the declaration explicitly.
|
|
54
|
+
addTypeTemplate({
|
|
55
|
+
filename: "types/mapo-form-registry.d.ts",
|
|
56
|
+
getContents: () => `
|
|
57
|
+
import type { FieldRegistry } from '@mapomodule/form/types';
|
|
58
|
+
declare module '#app' {
|
|
59
|
+
interface NuxtApp {
|
|
60
|
+
$mapoFormRegistry: FieldRegistry;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
declare module 'vue' {
|
|
64
|
+
interface ComponentCustomProperties {
|
|
65
|
+
$mapoFormRegistry: FieldRegistry;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export {};
|
|
69
|
+
`,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Devtools tab + route (development only)
|
|
73
|
+
if (nuxt.options.dev) {
|
|
74
|
+
// The route is registered inside the consumer app so the page can call useNuxtApp() normally.
|
|
75
|
+
extendPages((pages) => {
|
|
76
|
+
pages.push({
|
|
77
|
+
name: "mapo-devtools-forms",
|
|
78
|
+
path: "/_mapo/devtools/forms",
|
|
79
|
+
file: resolver.resolve("./runtime/devtools/DevtoolsFormsPage.vue"),
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Registra il tab via hook nativo di Nuxt DevTools (no dipendenza @nuxt/devtools-kit)
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
85
|
+
(nuxt as any).hook(
|
|
86
|
+
"devtools:customTabs",
|
|
87
|
+
(tabs: Array<Record<string, unknown>>) => {
|
|
88
|
+
tabs.push({
|
|
89
|
+
name: "mapo-forms",
|
|
90
|
+
title: "Mapo Forms",
|
|
91
|
+
icon: "i-lucide-layout-list",
|
|
92
|
+
view: { type: "iframe", src: "/_mapo/devtools/forms" },
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Form shell components
|
|
99
|
+
const shellComponents = [
|
|
100
|
+
"MapoForm",
|
|
101
|
+
"MapoFormField",
|
|
102
|
+
"MapoFormGroup",
|
|
103
|
+
"MapoFormTabs",
|
|
104
|
+
"MapoUnknownField",
|
|
105
|
+
"MapoFocusPortal",
|
|
106
|
+
];
|
|
107
|
+
for (const name of shellComponents) {
|
|
108
|
+
addComponent({
|
|
109
|
+
name,
|
|
110
|
+
filePath: resolver.resolve(`./runtime/components/${name}.vue`),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Public auto-imported composables
|
|
115
|
+
addImports([
|
|
116
|
+
{
|
|
117
|
+
name: "useMapoForm",
|
|
118
|
+
from: resolver.resolve("./runtime/composables/useMapoForm"),
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "useCurrentLang",
|
|
122
|
+
from: resolver.resolve("./runtime/composables/useCurrentLang"),
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "provideCurrentLang",
|
|
126
|
+
from: resolver.resolve("./runtime/composables/useCurrentLang"),
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "useFormFromSchema",
|
|
130
|
+
from: resolver.resolve("./runtime/composables/useFormFromSchema"),
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: "useFormDraft",
|
|
134
|
+
from: resolver.resolve("./runtime/composables/useFormDraft"),
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "useFocusMode",
|
|
138
|
+
from: resolver.resolve("./runtime/composables/useFocusMode"),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: "defineFormField",
|
|
142
|
+
from: resolver.resolve("./runtime/composables/defineFormField"),
|
|
143
|
+
},
|
|
144
|
+
// Progressive disclosure DSL
|
|
145
|
+
{
|
|
146
|
+
name: "when",
|
|
147
|
+
from: resolver.resolve(
|
|
148
|
+
"./runtime/composables/useProgressiveDisclosure",
|
|
149
|
+
),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: "whenAny",
|
|
153
|
+
from: resolver.resolve(
|
|
154
|
+
"./runtime/composables/useProgressiveDisclosure",
|
|
155
|
+
),
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "whenNot",
|
|
159
|
+
from: resolver.resolve(
|
|
160
|
+
"./runtime/composables/useProgressiveDisclosure",
|
|
161
|
+
),
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "matchesField",
|
|
165
|
+
from: resolver.resolve(
|
|
166
|
+
"./runtime/composables/useProgressiveDisclosure",
|
|
167
|
+
),
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: "isOneOf",
|
|
171
|
+
from: resolver.resolve(
|
|
172
|
+
"./runtime/composables/useProgressiveDisclosure",
|
|
173
|
+
),
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: "isNoneOf",
|
|
177
|
+
from: resolver.resolve(
|
|
178
|
+
"./runtime/composables/useProgressiveDisclosure",
|
|
179
|
+
),
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "isNotEmpty",
|
|
183
|
+
from: resolver.resolve(
|
|
184
|
+
"./runtime/composables/useProgressiveDisclosure",
|
|
185
|
+
),
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "isEmpty",
|
|
189
|
+
from: resolver.resolve(
|
|
190
|
+
"./runtime/composables/useProgressiveDisclosure",
|
|
191
|
+
),
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: "greaterThan",
|
|
195
|
+
from: resolver.resolve(
|
|
196
|
+
"./runtime/composables/useProgressiveDisclosure",
|
|
197
|
+
),
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: "lessThan",
|
|
201
|
+
from: resolver.resolve(
|
|
202
|
+
"./runtime/composables/useProgressiveDisclosure",
|
|
203
|
+
),
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: "matches",
|
|
207
|
+
from: resolver.resolve(
|
|
208
|
+
"./runtime/composables/useProgressiveDisclosure",
|
|
209
|
+
),
|
|
210
|
+
},
|
|
211
|
+
]);
|
|
212
|
+
},
|
|
213
|
+
}) satisfies NuxtModule<MapoFormOptions>;
|