@teamnovu/nuxt-statamic-formbuilder 0.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/README.md +13 -0
- package/dist/module.d.mts +14 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +83 -0
- package/dist/runtime/components/FormBuilderFields.d.vue.ts +34 -0
- package/dist/runtime/components/FormBuilderFields.vue +86 -0
- package/dist/runtime/components/FormBuilderFields.vue.d.ts +34 -0
- package/dist/runtime/components/StatamicFormBuilder.d.vue.ts +26 -0
- package/dist/runtime/components/StatamicFormBuilder.vue +54 -0
- package/dist/runtime/components/StatamicFormBuilder.vue.d.ts +26 -0
- package/dist/runtime/composables/useFieldConditions.d.ts +16 -0
- package/dist/runtime/composables/useFieldConditions.js +230 -0
- package/dist/runtime/composables/useFormBuilder.d.ts +11 -0
- package/dist/runtime/composables/useFormBuilder.js +68 -0
- package/dist/runtime/composables/useFormFieldRegistry.d.ts +8 -0
- package/dist/runtime/composables/useFormFieldRegistry.js +14 -0
- package/dist/runtime/composables/useFormValidation.d.ts +11 -0
- package/dist/runtime/composables/useFormValidation.js +166 -0
- package/dist/runtime/composables/useTranslatableInput.d.ts +8 -0
- package/dist/runtime/composables/useTranslatableInput.js +14 -0
- package/dist/runtime/fieldConfigs.d.ts +91 -0
- package/dist/runtime/fieldConfigs.js +0 -0
- package/dist/runtime/injectionKeys.d.ts +6 -0
- package/dist/runtime/injectionKeys.js +1 -0
- package/dist/runtime/locales/de.json +29 -0
- package/dist/runtime/locales/en.json +29 -0
- package/dist/runtime/locales/fr.json +29 -0
- package/dist/runtime/styles/form-builder.css +1 -0
- package/dist/runtime/types.d.ts +126 -0
- package/dist/runtime/types.js +22 -0
- package/dist/runtime/utils/graphqlFragment.d.ts +5 -0
- package/dist/runtime/utils/graphqlFragment.js +37 -0
- package/dist/runtime/utils/removePTags.d.ts +1 -0
- package/dist/runtime/utils/removePTags.js +4 -0
- package/dist/runtime/utils/submitStatamicForm.d.ts +7 -0
- package/dist/runtime/utils/submitStatamicForm.js +64 -0
- package/dist/runtime/utils/toFormBuilderConfiguration.d.ts +7 -0
- package/dist/runtime/utils/toFormBuilderConfiguration.js +37 -0
- package/dist/types.d.mts +5 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @teamnovu/nuxt-statamic-formbuilder
|
|
2
|
+
|
|
3
|
+
Core Nuxt module: form configuration types, Valibot validation from Statamic rules, field conditions, field registry, Statamic CSRF submit, and `StatamicFormBuilder`.
|
|
4
|
+
|
|
5
|
+
See the [monorepo README](../../README.md) for install and usage.
|
|
6
|
+
|
|
7
|
+
## Security / validation
|
|
8
|
+
|
|
9
|
+
Client-side Valibot rules are for UX only. Authoritative validation is Statamic form/field `validate` on the server. Most PHP fieldtypes currently ship empty `rules()` — editors must set validation in the form blueprint (or accept weak server checks). Do not treat the Nuxt UI as a trust boundary.
|
|
10
|
+
|
|
11
|
+
## Texts vs i18n
|
|
12
|
+
|
|
13
|
+
Bundled locales cover field validation messages and toast titles. Submit / loading / success / error body copy belongs on CMS `FormBuilderTexts` (see monorepo README).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
export { BaseFieldConfig, DefaultInputField, DisplayTextFieldConfig, FormBuilderConfiguration, FormBuilderField, FormBuilderFieldBase, FormBuilderFieldType, FormBuilderRequestState, FormBuilderSubmitter, FormBuilderTexts, FormFieldOption, InputCheckboxesFieldConfig, InputDateFieldConfig, InputEmailFieldConfig, InputFileUploadFieldConfig, InputFormBuilderField, InputNumberFieldConfig, InputPhoneFieldConfig, InputRadioButtonsFieldConfig, InputRules, InputSelectFieldConfig, InputSliderFieldConfig, InputSwitchFieldConfig, InputTextFieldConfig, InputTextareaFieldConfig, TranslatableInput, UseFormBuilderOptions, ValidationTrigger, defaultInputField, formBuilderStateKey, isDisplayOrSpacerField, isFileUploadField, isInputField, isMultiValueField } from '../dist/runtime/types.js';
|
|
3
|
+
|
|
4
|
+
interface ModuleOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Base URL of the Statamic backend used for form submissions.
|
|
7
|
+
* Falls back to `runtimeConfig.public.BACKEND_URL` when unset.
|
|
8
|
+
*/
|
|
9
|
+
serverUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
12
|
+
|
|
13
|
+
export { _default as default };
|
|
14
|
+
export type { ModuleOptions };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { defineNuxtModule, createResolver, addImports, addComponent } from '@nuxt/kit';
|
|
2
|
+
export { formBuilderStateKey, isDisplayOrSpacerField, isFileUploadField, isInputField, isMultiValueField } from '../dist/runtime/types.js';
|
|
3
|
+
|
|
4
|
+
const module$1 = defineNuxtModule({
|
|
5
|
+
meta: {
|
|
6
|
+
name: "@teamnovu/nuxt-statamic-formbuilder",
|
|
7
|
+
configKey: "formBuilder"
|
|
8
|
+
},
|
|
9
|
+
defaults: {
|
|
10
|
+
serverUrl: void 0
|
|
11
|
+
},
|
|
12
|
+
setup(options, nuxt) {
|
|
13
|
+
const resolver = createResolver(import.meta.url);
|
|
14
|
+
nuxt.options.runtimeConfig.public.formBuilder = {
|
|
15
|
+
serverUrl: options.serverUrl ?? nuxt.options.runtimeConfig.public.BACKEND_URL ?? ""
|
|
16
|
+
};
|
|
17
|
+
const composables = [
|
|
18
|
+
"useFormBuilder",
|
|
19
|
+
"useFormValidation",
|
|
20
|
+
"useFieldConditions",
|
|
21
|
+
"useTranslatableInput",
|
|
22
|
+
"useFormFieldRegistry"
|
|
23
|
+
];
|
|
24
|
+
for (const name of composables) {
|
|
25
|
+
addImports({
|
|
26
|
+
name,
|
|
27
|
+
from: resolver.resolve(`./runtime/composables/${name}`)
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
addImports({
|
|
31
|
+
name: "registerFormField",
|
|
32
|
+
from: resolver.resolve("./runtime/composables/useFormFieldRegistry")
|
|
33
|
+
});
|
|
34
|
+
addImports({
|
|
35
|
+
name: "resolveFormField",
|
|
36
|
+
from: resolver.resolve("./runtime/composables/useFormFieldRegistry")
|
|
37
|
+
});
|
|
38
|
+
addImports({
|
|
39
|
+
name: "toFormBuilderConfiguration",
|
|
40
|
+
from: resolver.resolve("./runtime/utils/toFormBuilderConfiguration")
|
|
41
|
+
});
|
|
42
|
+
addImports({
|
|
43
|
+
name: "COMPONENT_FORM_BUILDER_FRAGMENT",
|
|
44
|
+
from: resolver.resolve("./runtime/utils/graphqlFragment")
|
|
45
|
+
});
|
|
46
|
+
addImports({
|
|
47
|
+
name: "removePTags",
|
|
48
|
+
from: resolver.resolve("./runtime/utils/removePTags")
|
|
49
|
+
});
|
|
50
|
+
for (const name of [
|
|
51
|
+
"isDisplayOrSpacerField",
|
|
52
|
+
"isInputField",
|
|
53
|
+
"isFileUploadField",
|
|
54
|
+
"isMultiValueField",
|
|
55
|
+
"formBuilderStateKey"
|
|
56
|
+
]) {
|
|
57
|
+
addImports({
|
|
58
|
+
name,
|
|
59
|
+
from: resolver.resolve("./runtime/types")
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
addComponent({
|
|
63
|
+
name: "FormBuilderFields",
|
|
64
|
+
filePath: resolver.resolve("./runtime/components/FormBuilderFields.vue")
|
|
65
|
+
});
|
|
66
|
+
addComponent({
|
|
67
|
+
name: "StatamicFormBuilder",
|
|
68
|
+
filePath: resolver.resolve("./runtime/components/StatamicFormBuilder.vue")
|
|
69
|
+
});
|
|
70
|
+
nuxt.hook("i18n:registerModule", (register) => {
|
|
71
|
+
register({
|
|
72
|
+
langDir: resolver.resolve("./runtime/locales"),
|
|
73
|
+
locales: [
|
|
74
|
+
{ code: "en", file: "en.json" },
|
|
75
|
+
{ code: "de", file: "de.json" },
|
|
76
|
+
{ code: "fr", file: "fr.json" }
|
|
77
|
+
]
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export { module$1 as default };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { FormBuilderConfiguration, FormBuilderField } from '../types.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
form: FormBuilderConfiguration;
|
|
4
|
+
fields: Array<FormBuilderField>;
|
|
5
|
+
error?: Record<string, unknown>;
|
|
6
|
+
styleConfig?: Record<string, unknown>;
|
|
7
|
+
validationErrors?: Record<string, string | undefined>;
|
|
8
|
+
};
|
|
9
|
+
declare var __VLS_2: string, __VLS_3: {
|
|
10
|
+
field: FormBuilderField;
|
|
11
|
+
config: import("../fieldConfigs.js").BaseFieldConfig | import("../fieldConfigs.js").InputTextFieldConfig | import("../fieldConfigs.js").InputTextareaFieldConfig | import("../fieldConfigs.js").InputEmailFieldConfig | import("../fieldConfigs.js").InputPhoneFieldConfig | import("../fieldConfigs.js").InputNumberFieldConfig | import("../fieldConfigs.js").InputSelectFieldConfig | import("../fieldConfigs.js").InputDateFieldConfig | import("../fieldConfigs.js").InputSliderFieldConfig | import("../fieldConfigs.js").InputCheckboxesFieldConfig | import("../fieldConfigs.js").InputRadioButtonsFieldConfig | import("../fieldConfigs.js").InputSwitchFieldConfig | import("../fieldConfigs.js").InputFileUploadFieldConfig | import("../fieldConfigs.js").DisplayTextFieldConfig;
|
|
12
|
+
styleConfig: Record<string, unknown>;
|
|
13
|
+
input: {
|
|
14
|
+
name: string;
|
|
15
|
+
id: string;
|
|
16
|
+
ariaLabel: string;
|
|
17
|
+
rules: {
|
|
18
|
+
required: boolean | undefined;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
error: {} | null;
|
|
22
|
+
};
|
|
23
|
+
type __VLS_Slots = {} & {
|
|
24
|
+
[K in NonNullable<typeof __VLS_2>]?: (props: typeof __VLS_3) => any;
|
|
25
|
+
};
|
|
26
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
27
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
28
|
+
declare const _default: typeof __VLS_export;
|
|
29
|
+
export default _default;
|
|
30
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
31
|
+
new (): {
|
|
32
|
+
$slots: S;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<template
|
|
3
|
+
v-for="field in fields"
|
|
4
|
+
:key="field.handle"
|
|
5
|
+
>
|
|
6
|
+
<slot
|
|
7
|
+
:name="field.handle"
|
|
8
|
+
v-bind="slotBindings(field)"
|
|
9
|
+
>
|
|
10
|
+
<Transition name="form-field">
|
|
11
|
+
<component
|
|
12
|
+
:is="resolveFormField(field.type)"
|
|
13
|
+
v-if="resolveFormField(field.type) && isVisible(field) && field.type !== 'spacer'"
|
|
14
|
+
:class="getFormClasses(field.width)"
|
|
15
|
+
v-bind="slotBindings(field)"
|
|
16
|
+
:is-range="field.type === 'input_daterange'"
|
|
17
|
+
/>
|
|
18
|
+
<div
|
|
19
|
+
v-else-if="field.type === 'spacer' && isVisible(field)"
|
|
20
|
+
:aria-hidden="true"
|
|
21
|
+
:class="getFormClasses(field.width)"
|
|
22
|
+
class="group"
|
|
23
|
+
/>
|
|
24
|
+
<span
|
|
25
|
+
v-else-if="isVisible(field) && !resolveFormField(field.type) && field.type !== 'spacer'"
|
|
26
|
+
:class="getFormClasses(field.width)"
|
|
27
|
+
class="group"
|
|
28
|
+
>
|
|
29
|
+
Undefined field type {{ field.type }}
|
|
30
|
+
</span>
|
|
31
|
+
</Transition>
|
|
32
|
+
</slot>
|
|
33
|
+
</template>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup>
|
|
37
|
+
import { inject, ref } from "vue";
|
|
38
|
+
import { formBuilderStateKey } from "../injectionKeys";
|
|
39
|
+
import { useFieldConditions } from "../composables/useFieldConditions";
|
|
40
|
+
import { resolveFormField } from "../composables/useFormFieldRegistry";
|
|
41
|
+
const props = defineProps({
|
|
42
|
+
form: { type: Object, required: true },
|
|
43
|
+
fields: { type: Array, required: true },
|
|
44
|
+
error: { type: Object, required: false },
|
|
45
|
+
styleConfig: { type: Object, required: false },
|
|
46
|
+
validationErrors: { type: Object, required: false }
|
|
47
|
+
});
|
|
48
|
+
const state = inject(formBuilderStateKey, ref({}));
|
|
49
|
+
const { isVisible } = useFieldConditions(
|
|
50
|
+
() => props.fields,
|
|
51
|
+
state
|
|
52
|
+
);
|
|
53
|
+
const slotBindings = (field) => ({
|
|
54
|
+
field,
|
|
55
|
+
config: field.config,
|
|
56
|
+
styleConfig: props.styleConfig ?? {},
|
|
57
|
+
input: {
|
|
58
|
+
name: field.handle,
|
|
59
|
+
id: field.handle,
|
|
60
|
+
ariaLabel: field.display ?? field.handle,
|
|
61
|
+
rules: {
|
|
62
|
+
required: props.form?.rules?.[field.handle]?.includes("required")
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
error: props.validationErrors?.[field.handle] ?? props.error?.[field.handle] ?? null
|
|
66
|
+
});
|
|
67
|
+
const getFormClasses = (width) => {
|
|
68
|
+
const classes = ["col-span-12"];
|
|
69
|
+
if (width === 25) {
|
|
70
|
+
classes.push("@md:col-span-3");
|
|
71
|
+
} else if (width === 33) {
|
|
72
|
+
classes.push("@md:col-span-4");
|
|
73
|
+
} else if (width === 50) {
|
|
74
|
+
classes.push("@md:col-span-6");
|
|
75
|
+
} else if (width === 66) {
|
|
76
|
+
classes.push("@md:col-span-8");
|
|
77
|
+
} else if (width === 75) {
|
|
78
|
+
classes.push("@md:col-span-9");
|
|
79
|
+
}
|
|
80
|
+
return classes;
|
|
81
|
+
};
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<style scoped>
|
|
85
|
+
.form-field-enter-active,.form-field-leave-active{overflow:hidden;transition:height .35s ease,opacity .35s ease,padding-bottom .35s ease}.form-field-enter-from,.form-field-leave-to{height:0;opacity:0;padding-bottom:0}.form-field-enter-to,.form-field-leave-from{height:calc-size(auto,size);opacity:1}
|
|
86
|
+
</style>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { FormBuilderConfiguration, FormBuilderField } from '../types.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
form: FormBuilderConfiguration;
|
|
4
|
+
fields: Array<FormBuilderField>;
|
|
5
|
+
error?: Record<string, unknown>;
|
|
6
|
+
styleConfig?: Record<string, unknown>;
|
|
7
|
+
validationErrors?: Record<string, string | undefined>;
|
|
8
|
+
};
|
|
9
|
+
declare var __VLS_2: string, __VLS_3: {
|
|
10
|
+
field: FormBuilderField;
|
|
11
|
+
config: import("../fieldConfigs.js").BaseFieldConfig | import("../fieldConfigs.js").InputTextFieldConfig | import("../fieldConfigs.js").InputTextareaFieldConfig | import("../fieldConfigs.js").InputEmailFieldConfig | import("../fieldConfigs.js").InputPhoneFieldConfig | import("../fieldConfigs.js").InputNumberFieldConfig | import("../fieldConfigs.js").InputSelectFieldConfig | import("../fieldConfigs.js").InputDateFieldConfig | import("../fieldConfigs.js").InputSliderFieldConfig | import("../fieldConfigs.js").InputCheckboxesFieldConfig | import("../fieldConfigs.js").InputRadioButtonsFieldConfig | import("../fieldConfigs.js").InputSwitchFieldConfig | import("../fieldConfigs.js").InputFileUploadFieldConfig | import("../fieldConfigs.js").DisplayTextFieldConfig;
|
|
12
|
+
styleConfig: Record<string, unknown>;
|
|
13
|
+
input: {
|
|
14
|
+
name: string;
|
|
15
|
+
id: string;
|
|
16
|
+
ariaLabel: string;
|
|
17
|
+
rules: {
|
|
18
|
+
required: boolean | undefined;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
error: {} | null;
|
|
22
|
+
};
|
|
23
|
+
type __VLS_Slots = {} & {
|
|
24
|
+
[K in NonNullable<typeof __VLS_2>]?: (props: typeof __VLS_3) => any;
|
|
25
|
+
};
|
|
26
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
27
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
28
|
+
declare const _default: typeof __VLS_export;
|
|
29
|
+
export default _default;
|
|
30
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
31
|
+
new (): {
|
|
32
|
+
$slots: S;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FormBuilderConfiguration, FormBuilderSubmitter, FormBuilderTexts } from '../types.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
form: FormBuilderConfiguration;
|
|
4
|
+
texts: FormBuilderTexts;
|
|
5
|
+
serverUrl?: string;
|
|
6
|
+
styleConfig?: Record<string, unknown>;
|
|
7
|
+
submitter?: FormBuilderSubmitter;
|
|
8
|
+
/**
|
|
9
|
+
* Override the FormBuilder component (defaults to globally registered `FormBuilder`
|
|
10
|
+
* from a UI layer such as `@teamnovu/nuxt-statamic-formbuilder-nuxt-ui`).
|
|
11
|
+
*/
|
|
12
|
+
formComponent?: unknown;
|
|
13
|
+
};
|
|
14
|
+
declare var __VLS_10: string, __VLS_11: any;
|
|
15
|
+
type __VLS_Slots = {} & {
|
|
16
|
+
[K in NonNullable<typeof __VLS_10>]?: (props: typeof __VLS_11) => any;
|
|
17
|
+
};
|
|
18
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
20
|
+
declare const _default: typeof __VLS_export;
|
|
21
|
+
export default _default;
|
|
22
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
23
|
+
new (): {
|
|
24
|
+
$slots: S;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component
|
|
3
|
+
:is="formBuilderComponent"
|
|
4
|
+
v-if="formBuilderComponent && form"
|
|
5
|
+
:server-url="resolvedServerUrl"
|
|
6
|
+
:form="form"
|
|
7
|
+
:texts="texts"
|
|
8
|
+
:style-config="styleConfig"
|
|
9
|
+
:submitter="submitter"
|
|
10
|
+
>
|
|
11
|
+
<template
|
|
12
|
+
v-for="(_, name) in $slots"
|
|
13
|
+
:key="name"
|
|
14
|
+
#[name]="slotData"
|
|
15
|
+
>
|
|
16
|
+
<slot
|
|
17
|
+
:name="name"
|
|
18
|
+
v-bind="slotData || {}"
|
|
19
|
+
/>
|
|
20
|
+
</template>
|
|
21
|
+
</component>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup>
|
|
25
|
+
import { useRuntimeConfig } from "#imports";
|
|
26
|
+
import { computed, resolveComponent } from "vue";
|
|
27
|
+
const props = defineProps({
|
|
28
|
+
form: { type: Object, required: true },
|
|
29
|
+
texts: { type: Object, required: true },
|
|
30
|
+
serverUrl: { type: String, required: false },
|
|
31
|
+
styleConfig: { type: Object, required: false },
|
|
32
|
+
submitter: { type: Function, required: false },
|
|
33
|
+
formComponent: { type: null, required: false }
|
|
34
|
+
});
|
|
35
|
+
const runtimeConfig = useRuntimeConfig();
|
|
36
|
+
const resolvedServerUrl = computed(
|
|
37
|
+
() => props.serverUrl || runtimeConfig.public.formBuilder?.serverUrl || runtimeConfig.public.BACKEND_URL || ""
|
|
38
|
+
);
|
|
39
|
+
const formBuilderComponent = computed(() => {
|
|
40
|
+
if (props.formComponent) {
|
|
41
|
+
return props.formComponent;
|
|
42
|
+
}
|
|
43
|
+
const resolved = resolveComponent("FormBuilder");
|
|
44
|
+
if (typeof resolved === "string") {
|
|
45
|
+
if (import.meta.dev) {
|
|
46
|
+
console.warn(
|
|
47
|
+
"[StatamicFormBuilder] No FormBuilder component found. Install a UI layer (e.g. @teamnovu/nuxt-statamic-formbuilder-nuxt-ui) or pass formComponent."
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return resolved;
|
|
53
|
+
});
|
|
54
|
+
</script>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FormBuilderConfiguration, FormBuilderSubmitter, FormBuilderTexts } from '../types.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
form: FormBuilderConfiguration;
|
|
4
|
+
texts: FormBuilderTexts;
|
|
5
|
+
serverUrl?: string;
|
|
6
|
+
styleConfig?: Record<string, unknown>;
|
|
7
|
+
submitter?: FormBuilderSubmitter;
|
|
8
|
+
/**
|
|
9
|
+
* Override the FormBuilder component (defaults to globally registered `FormBuilder`
|
|
10
|
+
* from a UI layer such as `@teamnovu/nuxt-statamic-formbuilder-nuxt-ui`).
|
|
11
|
+
*/
|
|
12
|
+
formComponent?: unknown;
|
|
13
|
+
};
|
|
14
|
+
declare var __VLS_10: string, __VLS_11: any;
|
|
15
|
+
type __VLS_Slots = {} & {
|
|
16
|
+
[K in NonNullable<typeof __VLS_10>]?: (props: typeof __VLS_11) => any;
|
|
17
|
+
};
|
|
18
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
20
|
+
declare const _default: typeof __VLS_export;
|
|
21
|
+
export default _default;
|
|
22
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
23
|
+
new (): {
|
|
24
|
+
$slots: S;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { MaybeRefOrGetter } from 'vue';
|
|
2
|
+
import type { FormBuilderField } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Evaluates Statamic field conditions against the current form values.
|
|
5
|
+
*
|
|
6
|
+
* @param fields - The form fields array (MaybeRefOrGetter for reactivity).
|
|
7
|
+
* @param formValues - The live form state (MaybeRefOrGetter for reactivity).
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const { isVisible } = useFieldConditions(fields, state)
|
|
11
|
+
* // In template: v-if="isVisible(field)"
|
|
12
|
+
*/
|
|
13
|
+
export declare function useFieldConditions(fields: MaybeRefOrGetter<FormBuilderField[]>, formValues: MaybeRefOrGetter<Record<string, unknown>>): {
|
|
14
|
+
isVisible: (field: FormBuilderField) => boolean;
|
|
15
|
+
visibilityMap: import("vue").ComputedRef<Record<string, boolean>>;
|
|
16
|
+
};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { computed, toValue } from "vue";
|
|
2
|
+
const CONDITION_KEYS = [
|
|
3
|
+
"if",
|
|
4
|
+
"unless"
|
|
5
|
+
];
|
|
6
|
+
const OPERATORS = [
|
|
7
|
+
"equals",
|
|
8
|
+
"not",
|
|
9
|
+
"contains",
|
|
10
|
+
"contains_any",
|
|
11
|
+
"===",
|
|
12
|
+
"!==",
|
|
13
|
+
">",
|
|
14
|
+
">=",
|
|
15
|
+
"<",
|
|
16
|
+
"<=",
|
|
17
|
+
"custom"
|
|
18
|
+
];
|
|
19
|
+
const OPERATOR_ALIASES = {
|
|
20
|
+
"is": "equals",
|
|
21
|
+
"==": "equals",
|
|
22
|
+
"isnt": "not",
|
|
23
|
+
"!=": "not",
|
|
24
|
+
"includes": "contains",
|
|
25
|
+
"includes_any": "contains_any"
|
|
26
|
+
};
|
|
27
|
+
const NUMBER_OPS = /* @__PURE__ */ new Set([">", ">=", "<", "<="]);
|
|
28
|
+
function dataGet(obj, path) {
|
|
29
|
+
return path.split(".").reduce((acc, key) => {
|
|
30
|
+
if (acc == null || typeof acc !== "object") return void 0;
|
|
31
|
+
return acc[key];
|
|
32
|
+
}, obj);
|
|
33
|
+
}
|
|
34
|
+
function isEmpty(value) {
|
|
35
|
+
if (value == null) return true;
|
|
36
|
+
if (typeof value === "string") return value.trim() === "";
|
|
37
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
38
|
+
if (typeof value === "object") return Object.keys(value).length === 0;
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const ALL_OPERATORS = [...OPERATORS, ...Object.keys(OPERATOR_ALIASES)].sort((a, b) => b.length - a.length);
|
|
42
|
+
function escapeRegex(str) {
|
|
43
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
44
|
+
}
|
|
45
|
+
function normalizeConditionString(value) {
|
|
46
|
+
if (value === null) return "null";
|
|
47
|
+
if (value === "") return "empty";
|
|
48
|
+
return String(value);
|
|
49
|
+
}
|
|
50
|
+
function resolveAlias(op) {
|
|
51
|
+
return OPERATOR_ALIASES[op] ?? op;
|
|
52
|
+
}
|
|
53
|
+
function parseOperator(rhs) {
|
|
54
|
+
const str = normalizeConditionString(rhs);
|
|
55
|
+
for (const op of ALL_OPERATORS) {
|
|
56
|
+
if (new RegExp(`^${escapeRegex(op)} [^=]`).test(str)) {
|
|
57
|
+
return resolveAlias(op);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return "equals";
|
|
61
|
+
}
|
|
62
|
+
function parseValue(rhs) {
|
|
63
|
+
let str = normalizeConditionString(rhs);
|
|
64
|
+
for (const op of ALL_OPERATORS) {
|
|
65
|
+
if (new RegExp(`^${escapeRegex(op)} [^=]`).test(str)) {
|
|
66
|
+
str = str.replace(new RegExp(`^${escapeRegex(op)}\\s*`), "");
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return str;
|
|
71
|
+
}
|
|
72
|
+
function scopeField(field, prefix) {
|
|
73
|
+
if (field.startsWith("$root.") || field.startsWith("root.") || field.startsWith("$parent.")) {
|
|
74
|
+
return field;
|
|
75
|
+
}
|
|
76
|
+
return prefix ? prefix + field : field;
|
|
77
|
+
}
|
|
78
|
+
function parseConditions(raw, prefix) {
|
|
79
|
+
return Object.entries(raw).map(([field, rhs]) => ({
|
|
80
|
+
field: scopeField(field, prefix),
|
|
81
|
+
operator: parseOperator(rhs),
|
|
82
|
+
value: parseValue(rhs)
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
function normalizeOperator(op) {
|
|
86
|
+
switch (op) {
|
|
87
|
+
case "equals":
|
|
88
|
+
return "==";
|
|
89
|
+
case "not":
|
|
90
|
+
return "!=";
|
|
91
|
+
case "contains":
|
|
92
|
+
return "includes";
|
|
93
|
+
case "contains_any":
|
|
94
|
+
return "includes_any";
|
|
95
|
+
default:
|
|
96
|
+
return op;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function resolveField(field, formValues) {
|
|
100
|
+
if (field.startsWith("$parent.")) {
|
|
101
|
+
return void 0;
|
|
102
|
+
}
|
|
103
|
+
if (field.startsWith("$root.") || field.startsWith("root.")) {
|
|
104
|
+
return dataGet(formValues, field.replace(/^\$?root\./, ""));
|
|
105
|
+
}
|
|
106
|
+
return dataGet(formValues, field);
|
|
107
|
+
}
|
|
108
|
+
function testCondition(condition, formValues) {
|
|
109
|
+
const op = normalizeOperator(condition.operator);
|
|
110
|
+
const rawLhs = resolveField(condition.field, formValues);
|
|
111
|
+
const rawRhs = condition.value;
|
|
112
|
+
if (op === "includes_any") {
|
|
113
|
+
const needles = rawRhs.split(",").map((s) => s.trim());
|
|
114
|
+
if (Array.isArray(rawLhs)) {
|
|
115
|
+
return rawLhs.some((v) => needles.includes(String(v)));
|
|
116
|
+
}
|
|
117
|
+
return new RegExp(needles.map(escapeRegex).join("|")).test(String(rawLhs ?? ""));
|
|
118
|
+
}
|
|
119
|
+
if (op === "includes") {
|
|
120
|
+
if (Array.isArray(rawLhs)) return rawLhs.includes(rawRhs);
|
|
121
|
+
if (rawLhs == null || typeof rawLhs === "object") return false;
|
|
122
|
+
return String(rawLhs).includes(rawRhs);
|
|
123
|
+
}
|
|
124
|
+
if (rawRhs === "empty") {
|
|
125
|
+
const lhsEmpty = isEmpty(rawLhs);
|
|
126
|
+
switch (op) {
|
|
127
|
+
case "==":
|
|
128
|
+
return lhsEmpty;
|
|
129
|
+
case "!=":
|
|
130
|
+
return !lhsEmpty;
|
|
131
|
+
case "===":
|
|
132
|
+
return lhsEmpty;
|
|
133
|
+
case "!==":
|
|
134
|
+
return !lhsEmpty;
|
|
135
|
+
default:
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
let rhs;
|
|
140
|
+
switch (rawRhs) {
|
|
141
|
+
case "null":
|
|
142
|
+
rhs = null;
|
|
143
|
+
break;
|
|
144
|
+
case "true":
|
|
145
|
+
rhs = true;
|
|
146
|
+
break;
|
|
147
|
+
case "false":
|
|
148
|
+
rhs = false;
|
|
149
|
+
break;
|
|
150
|
+
default:
|
|
151
|
+
rhs = rawRhs;
|
|
152
|
+
}
|
|
153
|
+
if (NUMBER_OPS.has(op)) {
|
|
154
|
+
const lhsNum = Number(rawLhs);
|
|
155
|
+
const rhsNum = Number(rhs);
|
|
156
|
+
switch (op) {
|
|
157
|
+
case ">":
|
|
158
|
+
return lhsNum > rhsNum;
|
|
159
|
+
case ">=":
|
|
160
|
+
return lhsNum >= rhsNum;
|
|
161
|
+
case "<":
|
|
162
|
+
return lhsNum < rhsNum;
|
|
163
|
+
case "<=":
|
|
164
|
+
return lhsNum <= rhsNum;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (rawLhs !== null && typeof rawLhs === "object") return false;
|
|
168
|
+
let lhs = rawLhs;
|
|
169
|
+
if (typeof lhs === "string") {
|
|
170
|
+
const trimmed = lhs.trim();
|
|
171
|
+
lhs = trimmed === "" ? null : trimmed;
|
|
172
|
+
}
|
|
173
|
+
if (typeof rhs === "string") rhs = rhs.trim();
|
|
174
|
+
switch (op) {
|
|
175
|
+
// Loose equality intentional — mirrors the original eval() behaviour,
|
|
176
|
+
// e.g. null == "null" should be false but null == null should be true.
|
|
177
|
+
// eslint-disable-next-line eqeqeq
|
|
178
|
+
case "==":
|
|
179
|
+
return lhs == rhs;
|
|
180
|
+
// eslint-disable-next-line eqeqeq
|
|
181
|
+
case "!=":
|
|
182
|
+
return lhs != rhs;
|
|
183
|
+
case "===":
|
|
184
|
+
return lhs === rhs;
|
|
185
|
+
case "!==":
|
|
186
|
+
return lhs !== rhs;
|
|
187
|
+
default:
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function evaluateFieldConditions(field, formValues) {
|
|
192
|
+
const condKey = CONDITION_KEYS.find(
|
|
193
|
+
(k) => field[k] != null
|
|
194
|
+
);
|
|
195
|
+
if (!condKey) return true;
|
|
196
|
+
const raw = field[condKey];
|
|
197
|
+
if (typeof raw === "string") {
|
|
198
|
+
if (import.meta.dev) {
|
|
199
|
+
console.warn(
|
|
200
|
+
`[useFieldConditions] Custom string conditions are not supported (field: "${field.handle}", condition: "${raw}"). Field will always be shown.`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
if (typeof raw !== "object" || raw === null) return true;
|
|
206
|
+
const passOnAny = condKey.includes("any");
|
|
207
|
+
const showOnPass = !condKey.includes("unless") && !condKey.includes("hide_when");
|
|
208
|
+
const conditions = parseConditions(
|
|
209
|
+
raw,
|
|
210
|
+
field.prefix
|
|
211
|
+
);
|
|
212
|
+
const passes = passOnAny ? conditions.some((c) => testCondition(c, formValues)) : conditions.every((c) => testCondition(c, formValues));
|
|
213
|
+
return showOnPass ? passes : !passes;
|
|
214
|
+
}
|
|
215
|
+
export function useFieldConditions(fields, formValues) {
|
|
216
|
+
const visibilityMap = computed(() => {
|
|
217
|
+
const values = toValue(formValues);
|
|
218
|
+
return toValue(fields).reduce((acc, field) => {
|
|
219
|
+
acc[field.handle] = evaluateFieldConditions(field, values);
|
|
220
|
+
return acc;
|
|
221
|
+
}, {});
|
|
222
|
+
});
|
|
223
|
+
function isVisible(field) {
|
|
224
|
+
return evaluateFieldConditions(field, toValue(formValues));
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
isVisible,
|
|
228
|
+
visibilityMap
|
|
229
|
+
};
|
|
230
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Ref } from 'vue';
|
|
2
|
+
import type { FormBuilderConfiguration, FormBuilderField, FormBuilderRequestState, UseFormBuilderOptions } from '../types.js';
|
|
3
|
+
export declare function useFormBuilder(formBuilderConfiguration: FormBuilderConfiguration, serverUrlOrOptions?: string | UseFormBuilderOptions): {
|
|
4
|
+
requestState: Ref<FormBuilderRequestState, FormBuilderRequestState>;
|
|
5
|
+
submit: (_event?: unknown, stateRef?: Ref<Record<string, unknown>>) => Promise<unknown>;
|
|
6
|
+
reset: () => void;
|
|
7
|
+
state: Ref<Record<string, unknown>, Record<string, unknown>>;
|
|
8
|
+
fields: import("vue").ComputedRef<FormBuilderField[]>;
|
|
9
|
+
serverUrl: import("vue").ComputedRef<string>;
|
|
10
|
+
};
|
|
11
|
+
export default useFormBuilder;
|