@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,154 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* MapoFocusPortal — Renders full-page editing for a repeater item.
|
|
4
|
+
*
|
|
5
|
+
* Mount only one instance in layout/default or in MapoRootComponents.
|
|
6
|
+
* It activates automatically when `useFocusMode().enter()` is called.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // In layouts/default.vue
|
|
10
|
+
* <MapoFocusPortal />
|
|
11
|
+
*/
|
|
12
|
+
import { computed, ref, onMounted, onUnmounted } from "vue";
|
|
13
|
+
import { watch } from "vue";
|
|
14
|
+
import { useFocusMode } from "../composables/useFocusMode.js";
|
|
15
|
+
import MapoForm from "./MapoForm.vue";
|
|
16
|
+
|
|
17
|
+
const { focusTarget, exit } = useFocusMode();
|
|
18
|
+
|
|
19
|
+
const isOpen = computed(() => focusTarget.value !== null);
|
|
20
|
+
const canUseDocument = typeof document !== "undefined";
|
|
21
|
+
|
|
22
|
+
// Close on ESC.
|
|
23
|
+
function onKeyDown(e: KeyboardEvent) {
|
|
24
|
+
if (e.key === "Escape" && isOpen.value) exit();
|
|
25
|
+
}
|
|
26
|
+
onMounted(() => window.addEventListener("keydown", onKeyDown));
|
|
27
|
+
onUnmounted(() => window.removeEventListener("keydown", onKeyDown));
|
|
28
|
+
|
|
29
|
+
// Lock body scroll while the portal is open.
|
|
30
|
+
watch(isOpen, (open) => {
|
|
31
|
+
if (!canUseDocument) return;
|
|
32
|
+
document.body.style.overflow = open ? "hidden" : "";
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Local model for the sub-form, kept in two-way sync with the focus target.
|
|
36
|
+
const localModel = ref<Record<string, unknown>>({});
|
|
37
|
+
|
|
38
|
+
watch(
|
|
39
|
+
() => focusTarget.value?.model,
|
|
40
|
+
(val) => {
|
|
41
|
+
if (val) localModel.value = { ...val };
|
|
42
|
+
},
|
|
43
|
+
{ immediate: true },
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
function onUpdate(val: Record<string, unknown>) {
|
|
47
|
+
localModel.value = val;
|
|
48
|
+
focusTarget.value?.onUpdate(val);
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<Teleport to="body">
|
|
54
|
+
<Transition name="mapo-focus-portal">
|
|
55
|
+
<div
|
|
56
|
+
v-if="isOpen && focusTarget"
|
|
57
|
+
class="mapo-focus-portal fixed inset-0 z-[9999] flex flex-col bg-white"
|
|
58
|
+
role="dialog"
|
|
59
|
+
aria-modal="true"
|
|
60
|
+
>
|
|
61
|
+
<!-- ── Header ────────────────────────────────────────────────────── -->
|
|
62
|
+
<div
|
|
63
|
+
class="flex shrink-0 items-center gap-3 border-b border-gray-200 px-6 py-3"
|
|
64
|
+
>
|
|
65
|
+
<!-- Breadcrumb -->
|
|
66
|
+
<nav class="flex min-w-0 flex-1 items-center gap-1 text-sm">
|
|
67
|
+
<template v-for="(crumb, i) in focusTarget.breadcrumb" :key="i">
|
|
68
|
+
<span
|
|
69
|
+
:class="
|
|
70
|
+
i < focusTarget.breadcrumb.length - 1
|
|
71
|
+
? 'text-gray-400'
|
|
72
|
+
: 'font-semibold text-gray-800'
|
|
73
|
+
"
|
|
74
|
+
class="truncate"
|
|
75
|
+
>
|
|
76
|
+
{{ crumb }}
|
|
77
|
+
</span>
|
|
78
|
+
<UIcon
|
|
79
|
+
v-if="i < focusTarget.breadcrumb.length - 1"
|
|
80
|
+
name="i-lucide-chevron-right"
|
|
81
|
+
class="h-3.5 w-3.5 shrink-0 text-gray-300"
|
|
82
|
+
/>
|
|
83
|
+
</template>
|
|
84
|
+
</nav>
|
|
85
|
+
|
|
86
|
+
<!-- Close button -->
|
|
87
|
+
<UButton
|
|
88
|
+
variant="ghost"
|
|
89
|
+
color="neutral"
|
|
90
|
+
icon="i-lucide-x"
|
|
91
|
+
label="Close"
|
|
92
|
+
size="sm"
|
|
93
|
+
@click="exit"
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<!-- ── Body: scrollable sub-form ────────────────────────────────── -->
|
|
98
|
+
<div class="min-h-0 flex-1 overflow-y-auto">
|
|
99
|
+
<div class="mx-auto max-w-3xl px-6 py-8">
|
|
100
|
+
<MapoForm
|
|
101
|
+
:model-value="localModel"
|
|
102
|
+
:fields="focusTarget.fields as any"
|
|
103
|
+
:errors="focusTarget.errors.value as any"
|
|
104
|
+
:languages="focusTarget.languages as any"
|
|
105
|
+
:current-lang="focusTarget.currentLang"
|
|
106
|
+
:registry="focusTarget.registry as any"
|
|
107
|
+
@update:model-value="onUpdate"
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<!-- ── Footer ───────────────────────────────────────────────────── -->
|
|
113
|
+
<div
|
|
114
|
+
class="flex shrink-0 items-center justify-end gap-3 border-t border-gray-200 px-6 py-3"
|
|
115
|
+
>
|
|
116
|
+
<p class="mr-auto text-xs text-gray-400">
|
|
117
|
+
<UIcon name="i-lucide-keyboard" class="mr-1 inline h-3 w-3" />
|
|
118
|
+
Press
|
|
119
|
+
<kbd class="rounded bg-gray-100 px-1 py-0.5 font-mono text-xs"
|
|
120
|
+
>ESC</kbd
|
|
121
|
+
>
|
|
122
|
+
to return to the form
|
|
123
|
+
</p>
|
|
124
|
+
<UButton variant="ghost" color="neutral" @click="exit">
|
|
125
|
+
Close
|
|
126
|
+
</UButton>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</Transition>
|
|
130
|
+
</Teleport>
|
|
131
|
+
</template>
|
|
132
|
+
|
|
133
|
+
<style scoped>
|
|
134
|
+
.mapo-focus-portal-enter-active {
|
|
135
|
+
transition:
|
|
136
|
+
opacity 200ms ease,
|
|
137
|
+
transform 200ms ease;
|
|
138
|
+
}
|
|
139
|
+
.mapo-focus-portal-leave-active {
|
|
140
|
+
transition:
|
|
141
|
+
opacity 150ms ease,
|
|
142
|
+
transform 150ms ease;
|
|
143
|
+
}
|
|
144
|
+
.mapo-focus-portal-enter-from,
|
|
145
|
+
.mapo-focus-portal-leave-to {
|
|
146
|
+
opacity: 0;
|
|
147
|
+
transform: scale(0.98);
|
|
148
|
+
}
|
|
149
|
+
.mapo-focus-portal-enter-to,
|
|
150
|
+
.mapo-focus-portal-leave-from {
|
|
151
|
+
opacity: 1;
|
|
152
|
+
transform: scale(1);
|
|
153
|
+
}
|
|
154
|
+
</style>
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
<script setup lang="ts" generic="T extends object">
|
|
2
|
+
import { computed, useSlots, watchEffect, inject, onUnmounted } from "vue";
|
|
3
|
+
import type { Ref } from "vue";
|
|
4
|
+
import { useNuxtApp, useRuntimeConfig } from "#app";
|
|
5
|
+
import type {
|
|
6
|
+
AnyFieldDescriptor,
|
|
7
|
+
FieldRegistry,
|
|
8
|
+
MapoFormRuntimeConfig,
|
|
9
|
+
} from "../types/index.js";
|
|
10
|
+
import type { GroupEntry, TabEntry } from "../types/layout.js";
|
|
11
|
+
import { useMapoForm, injectMapoForm } from "../composables/useMapoForm.js";
|
|
12
|
+
import { provideCurrentLang } from "../composables/useCurrentLang.js";
|
|
13
|
+
import MapoFormGroup from "./MapoFormGroup.vue";
|
|
14
|
+
import MapoFormTabs from "./MapoFormTabs.vue";
|
|
15
|
+
import MapoFormFlatSection from "./MapoFormFlatSection.vue";
|
|
16
|
+
|
|
17
|
+
const props = withDefaults(
|
|
18
|
+
defineProps<{
|
|
19
|
+
modelValue: T;
|
|
20
|
+
fields: AnyFieldDescriptor<T>[];
|
|
21
|
+
errors?: Record<string, string[]>;
|
|
22
|
+
languages?: string[];
|
|
23
|
+
currentLang?: string;
|
|
24
|
+
readonly?: boolean;
|
|
25
|
+
immediate?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Field component registry. When omitted, it is auto-injected from
|
|
28
|
+
* `$mapoFormRegistry` (provided by the `@mapomodule/form` plugin).
|
|
29
|
+
* Pass it explicitly only for headless forms or one-off registry swaps.
|
|
30
|
+
*/
|
|
31
|
+
registry?: FieldRegistry;
|
|
32
|
+
}>(),
|
|
33
|
+
{
|
|
34
|
+
errors: () => ({}),
|
|
35
|
+
languages: () => [],
|
|
36
|
+
currentLang: "",
|
|
37
|
+
readonly: false,
|
|
38
|
+
immediate: false,
|
|
39
|
+
registry: undefined,
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Auto-inject the registry from the global plugin to remove the boilerplate
|
|
44
|
+
// `useNuxtApp().$mapoFormRegistry` + `:registry=…` from every page.
|
|
45
|
+
const injectedRegistry = computed<FieldRegistry>(() => {
|
|
46
|
+
if (props.registry) return props.registry;
|
|
47
|
+
const nuxt = useNuxtApp() as unknown as { $mapoFormRegistry?: FieldRegistry };
|
|
48
|
+
if (!nuxt.$mapoFormRegistry) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
"[MapoForm] No registry available: pass the `registry` prop or " +
|
|
51
|
+
"make sure the @mapomodule/form module is installed.",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return nuxt.$mapoFormRegistry;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const emit = defineEmits<{ "update:modelValue": [value: T] }>();
|
|
58
|
+
|
|
59
|
+
// Slots are forwarded generically to inner layers; props vary per slot
|
|
60
|
+
// (field.* carry a descriptor, group.* carry none), so they are loosely typed.
|
|
61
|
+
// Slot names are dynamic (`field.{key}`, `field.{key}.label`, `group.{name}`…)
|
|
62
|
+
// and each kind carries different bindings, so the props stay loosely typed.
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
defineSlots<Record<string, (props: Record<string, any>) => unknown>>();
|
|
65
|
+
|
|
66
|
+
// ─── Setup form ──────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
const model = computed({
|
|
69
|
+
get: () => props.modelValue,
|
|
70
|
+
set: (val) => emit("update:modelValue", val),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const currentLangRef = computed(() => props.currentLang);
|
|
74
|
+
const errorsRef = computed(() => props.errors);
|
|
75
|
+
|
|
76
|
+
const globalDebounce: number =
|
|
77
|
+
(useRuntimeConfig().public.mapoForm as MapoFormRuntimeConfig | undefined)
|
|
78
|
+
?.debounce ?? 300;
|
|
79
|
+
|
|
80
|
+
const form = useMapoForm({
|
|
81
|
+
model: model as unknown as Ref<T>,
|
|
82
|
+
fields: computed(() => props.fields),
|
|
83
|
+
errors: errorsRef,
|
|
84
|
+
languages: props.languages,
|
|
85
|
+
currentLang: currentLangRef,
|
|
86
|
+
immediate: props.immediate,
|
|
87
|
+
debounce: globalDebounce,
|
|
88
|
+
registry: injectedRegistry.value,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Inherit the parent form's submitted state (provided automatically by useMapoForm)
|
|
92
|
+
// so that form.submit() on the parent also shows errors in this nested MapoForm.
|
|
93
|
+
const parentCtx = injectMapoForm();
|
|
94
|
+
watchEffect(() => {
|
|
95
|
+
form.readonly.value = props.readonly;
|
|
96
|
+
if (parentCtx) form.ctx.submitted.value = parentCtx.submitted.value;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
defineExpose({
|
|
100
|
+
submit: form.submit,
|
|
101
|
+
validateClient: form.validateClient,
|
|
102
|
+
resetDirty: form.resetDirty,
|
|
103
|
+
getPatch: form.getPatch,
|
|
104
|
+
isDirty: form.isDirty,
|
|
105
|
+
isLoading: form.isLoading,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
type ValidateClientFn = () => {
|
|
109
|
+
valid: boolean;
|
|
110
|
+
errors: Record<string, string>;
|
|
111
|
+
};
|
|
112
|
+
type RegisterFn = (fn: ValidateClientFn) => () => void;
|
|
113
|
+
const registerValidator = inject<RegisterFn | null>(
|
|
114
|
+
"mapoDetailRegisterValidator",
|
|
115
|
+
null,
|
|
116
|
+
);
|
|
117
|
+
if (registerValidator) {
|
|
118
|
+
const unregister = registerValidator(form.validateClient);
|
|
119
|
+
onUnmounted(unregister);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Forward only field-specific slots to inner layers, avoiding host-level slots
|
|
123
|
+
// such as header/footer/actions where they have no meaning.
|
|
124
|
+
const slots = useSlots();
|
|
125
|
+
const fieldSlotNames = computed(() =>
|
|
126
|
+
Object.keys(slots).filter(
|
|
127
|
+
(name) =>
|
|
128
|
+
name.startsWith("field.") ||
|
|
129
|
+
name.startsWith("group.") ||
|
|
130
|
+
name.startsWith("tab.") ||
|
|
131
|
+
name === "tab-bar-end",
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
provideCurrentLang(currentLangRef);
|
|
135
|
+
|
|
136
|
+
// ─── Group fields by tab/group ───────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
function getOrCreateTab(map: Map<string, TabEntry>, name: string): TabEntry {
|
|
139
|
+
if (!map.has(name)) {
|
|
140
|
+
map.set(name, { name, groups: new Map(), children: new Map() });
|
|
141
|
+
}
|
|
142
|
+
return map.get(name)!;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const grouped = computed<Map<string, TabEntry>>(() => {
|
|
146
|
+
const tabs = new Map<string, TabEntry>();
|
|
147
|
+
|
|
148
|
+
// The layout tree is type-agnostic: cast the generic descriptors to the bare
|
|
149
|
+
// AnyFieldDescriptor used by the (non-generic) MapoFormGroup/Tabs/FlatSection layers.
|
|
150
|
+
for (const field of props.fields as AnyFieldDescriptor[]) {
|
|
151
|
+
// Normalise: 'a/b', ['a','b'], or 'a' all become a string[]
|
|
152
|
+
const rawTab = field.tab;
|
|
153
|
+
const tabPath: string[] = !rawTab
|
|
154
|
+
? ["__default__"]
|
|
155
|
+
: Array.isArray(rawTab)
|
|
156
|
+
? rawTab
|
|
157
|
+
: rawTab.split("/");
|
|
158
|
+
const groupName = field.group ?? "__flat__";
|
|
159
|
+
|
|
160
|
+
// Walk/create the nested TabEntry tree
|
|
161
|
+
// tabPath always has at least one segment (split never returns []).
|
|
162
|
+
let currentMap = tabs;
|
|
163
|
+
for (let i = 0; i < tabPath.length - 1; i++) {
|
|
164
|
+
const parent = getOrCreateTab(currentMap, tabPath[i]!);
|
|
165
|
+
currentMap = parent.children;
|
|
166
|
+
}
|
|
167
|
+
const leaf = getOrCreateTab(currentMap, tabPath[tabPath.length - 1]!);
|
|
168
|
+
if (!leaf.groups.has(groupName)) {
|
|
169
|
+
leaf.groups.set(groupName, { fields: [], subtabs: new Map() });
|
|
170
|
+
}
|
|
171
|
+
const grp = leaf.groups.get(groupName)!;
|
|
172
|
+
if (field.subtab) {
|
|
173
|
+
if (!grp.subtabs.has(field.subtab)) {
|
|
174
|
+
grp.subtabs.set(field.subtab, []);
|
|
175
|
+
}
|
|
176
|
+
grp.subtabs.get(field.subtab)!.push(field);
|
|
177
|
+
} else {
|
|
178
|
+
grp.fields.push(field);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return tabs;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const hasTabs = computed(() => {
|
|
186
|
+
const keys = [...grouped.value.keys()];
|
|
187
|
+
return keys.length > 1 || (keys.length === 1 && keys[0] !== "__default__");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const defaultGroups = computed(
|
|
191
|
+
() =>
|
|
192
|
+
grouped.value.get("__default__")?.groups ?? new Map<string, GroupEntry>(),
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const tabList = computed<TabEntry[]>(() => [...grouped.value.values()]);
|
|
196
|
+
</script>
|
|
197
|
+
|
|
198
|
+
<template>
|
|
199
|
+
<div class="mapo-form space-y-5">
|
|
200
|
+
<!-- All field slots (field.<key>, field.<key>.append, .prepend, .label, …)
|
|
201
|
+
are forwarded generically to inner layers (Tabs → Group → Field). -->
|
|
202
|
+
<MapoFormTabs v-if="hasTabs" :tabs="tabList">
|
|
203
|
+
<template
|
|
204
|
+
v-for="slotName in fieldSlotNames"
|
|
205
|
+
:key="slotName"
|
|
206
|
+
#[slotName]="slotProps"
|
|
207
|
+
>
|
|
208
|
+
<slot :name="slotName" v-bind="slotProps ?? {}" />
|
|
209
|
+
</template>
|
|
210
|
+
</MapoFormTabs>
|
|
211
|
+
|
|
212
|
+
<template v-else>
|
|
213
|
+
<template v-for="[groupName, group] of defaultGroups" :key="groupName">
|
|
214
|
+
<template v-if="groupName !== '__flat__'">
|
|
215
|
+
<slot :name="`group.${groupName}.before`" />
|
|
216
|
+
<MapoFormGroup
|
|
217
|
+
:name="groupName"
|
|
218
|
+
:fields="group.fields"
|
|
219
|
+
:subtabs="group.subtabs"
|
|
220
|
+
>
|
|
221
|
+
<template
|
|
222
|
+
v-for="slotName in fieldSlotNames"
|
|
223
|
+
:key="slotName"
|
|
224
|
+
#[slotName]="slotProps"
|
|
225
|
+
>
|
|
226
|
+
<slot :name="slotName" v-bind="slotProps ?? {}" />
|
|
227
|
+
</template>
|
|
228
|
+
</MapoFormGroup>
|
|
229
|
+
<slot :name="`group.${groupName}.after`" />
|
|
230
|
+
</template>
|
|
231
|
+
|
|
232
|
+
<MapoFormFlatSection
|
|
233
|
+
v-else
|
|
234
|
+
:fields="group.fields as AnyFieldDescriptor[]"
|
|
235
|
+
>
|
|
236
|
+
<template
|
|
237
|
+
v-for="slotName in fieldSlotNames"
|
|
238
|
+
:key="slotName"
|
|
239
|
+
#[slotName]="slotProps"
|
|
240
|
+
>
|
|
241
|
+
<slot :name="slotName" v-bind="slotProps ?? {}" />
|
|
242
|
+
</template>
|
|
243
|
+
</MapoFormFlatSection>
|
|
244
|
+
</template>
|
|
245
|
+
</template>
|
|
246
|
+
</div>
|
|
247
|
+
</template>
|