@10x-media/form-builder 0.1.0-beta.6 → 0.1.0-beta.8
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/CHANGELOG.md +84 -0
- package/dist/actions/body/serializeBody.d.ts +3 -1
- package/dist/actions/body/serializeBody.js.map +1 -1
- package/dist/actions/buildActionBlocks.js +12 -9
- package/dist/actions/buildActionBlocks.js.map +1 -1
- package/dist/actions/builtin/confirmation.js +13 -68
- package/dist/actions/builtin/confirmation.js.map +1 -1
- package/dist/actions/builtin/emailAction.d.ts +17 -0
- package/dist/actions/builtin/emailAction.js +83 -0
- package/dist/actions/builtin/emailAction.js.map +1 -0
- package/dist/actions/builtin/emailTeam.js +14 -74
- package/dist/actions/builtin/emailTeam.js.map +1 -1
- package/dist/actions/builtin/index.d.ts +9 -12
- package/dist/actions/builtin/index.js +11 -11
- package/dist/actions/builtin/index.js.map +1 -1
- package/dist/actions/defineAction.d.ts +2 -2
- package/dist/actions/defineAction.js.map +1 -1
- package/dist/actions/emailRecipients.js +75 -31
- package/dist/actions/emailRecipients.js.map +1 -1
- package/dist/actions/registry.d.ts +2 -1
- package/dist/actions/registry.js +2 -6
- package/dist/actions/registry.js.map +1 -1
- package/dist/actions/task.js +5 -3
- package/dist/actions/task.js.map +1 -1
- package/dist/calc/evaluate.js +1 -2
- package/dist/calc/evaluate.js.map +1 -1
- package/dist/calc/normalizeCalc.js +1 -2
- package/dist/calc/normalizeCalc.js.map +1 -1
- package/dist/calc/types.js.map +1 -1
- package/dist/collections/buttonFields.js +1 -1
- package/dist/collections/formSubmissions.js +2 -1
- package/dist/collections/formSubmissions.js.map +1 -1
- package/dist/collections/forms.js +32 -100
- package/dist/collections/forms.js.map +1 -1
- package/dist/collections/formsEndpoints.js +101 -0
- package/dist/collections/formsEndpoints.js.map +1 -0
- package/dist/conditions/evaluate.js +1 -1
- package/dist/conditions/evaluate.js.map +1 -1
- package/dist/conditions/fieldTypes.d.ts +3 -1
- package/dist/conditions/fieldTypes.js +4 -1
- package/dist/conditions/fieldTypes.js.map +1 -1
- package/dist/conditions/normalizeConditions.js +18 -5
- package/dist/conditions/normalizeConditions.js.map +1 -1
- package/dist/consent/captureConsent.d.ts +7 -0
- package/dist/consent/captureConsent.js +7 -0
- package/dist/consent/captureConsent.js.map +1 -1
- package/dist/consent/consentSourcesField.d.ts +4 -0
- package/dist/consent/consentSourcesField.js +5 -1
- package/dist/consent/consentSourcesField.js.map +1 -1
- package/dist/consent/resolveConsentEntries.d.ts +1 -1
- package/dist/consent/resolveConsentEntries.js +1 -4
- package/dist/consent/resolveConsentEntries.js.map +1 -1
- package/dist/consent/resolveConsentSourcesRequest.js +0 -1
- package/dist/consent/resolveConsentSourcesRequest.js.map +1 -1
- package/dist/consent/resolveConsentStatements.d.ts +1 -1
- package/dist/consent/resolveConsentStatements.js.map +1 -1
- package/dist/consent/types.d.ts +9 -1
- package/dist/email/departmentsField.js +1 -1
- package/dist/exports/react.d.ts +5 -4
- package/dist/exports/react.js +3 -2
- package/dist/exports/rsc.js +1 -1
- package/dist/exports/types.d.ts +1 -1
- package/dist/fields/builtin/email.js +1 -1
- package/dist/fields/builtin/email.js.map +1 -1
- package/dist/fields/builtin/message.js +1 -1
- package/dist/fields/builtin/repeater.js +1 -1
- package/dist/fields/builtin/select.js +1 -1
- package/dist/fields/registry.d.ts +2 -1
- package/dist/fields/registry.js +2 -6
- package/dist/fields/registry.js.map +1 -1
- package/dist/fields/sharedConfig.js +1 -1
- package/dist/flow/normalizeFlow.js +7 -3
- package/dist/flow/normalizeFlow.js.map +1 -1
- package/dist/index.d.ts +4 -201
- package/dist/index.js +11 -4
- package/dist/index.js.map +1 -1
- package/dist/options.d.ts +229 -0
- package/dist/plugin/applyRegistryConfig.d.ts +11 -0
- package/dist/plugin/applyRegistryConfig.js +20 -0
- package/dist/plugin/applyRegistryConfig.js.map +1 -0
- package/dist/plugin/registerCollections.js +2 -1
- package/dist/plugin/registerCollections.js.map +1 -1
- package/dist/poll/buildPollOptionSourceFields.js +2 -2
- package/dist/poll/buildPollOptionSourceFields.js.map +1 -1
- package/dist/poll/definePollOptionSource.d.ts +2 -2
- package/dist/poll/definePollOptionSource.js.map +1 -1
- package/dist/poll/pollTypeRegistry.js +2 -4
- package/dist/poll/pollTypeRegistry.js.map +1 -1
- package/dist/poll/registry.d.ts +2 -1
- package/dist/poll/registry.js +2 -6
- package/dist/poll/registry.js.map +1 -1
- package/dist/poll/resolvePollOptionsRequest.js +0 -1
- package/dist/poll/resolvePollOptionsRequest.js.map +1 -1
- package/dist/prefill/valuesFromSearchParams.js +2 -7
- package/dist/prefill/valuesFromSearchParams.js.map +1 -1
- package/dist/react/Form.d.ts +39 -3
- package/dist/react/Form.js +148 -164
- package/dist/react/Form.js.map +1 -1
- package/dist/react/FormContext.d.ts +7 -1
- package/dist/react/FormContext.js.map +1 -1
- package/dist/react/FormFields.d.ts +18 -0
- package/dist/react/FormFields.js +114 -0
- package/dist/react/FormFields.js.map +1 -0
- package/dist/react/FormLayout.d.ts +4 -2
- package/dist/react/FormLayout.js +3 -2
- package/dist/react/FormLayout.js.map +1 -1
- package/dist/react/Poll.js +15 -5
- package/dist/react/Poll.js.map +1 -1
- package/dist/react/presentation/DialogSurface.js +1 -4
- package/dist/react/presentation/DialogSurface.js.map +1 -1
- package/dist/react/renderers/calculation.js +1 -3
- package/dist/react/renderers/calculation.js.map +1 -1
- package/dist/react/renderers/checkbox.js +1 -3
- package/dist/react/renderers/checkbox.js.map +1 -1
- package/dist/react/renderers/consent.js +2 -3
- package/dist/react/renderers/consent.js.map +1 -1
- package/dist/react/renderers/date.js +1 -3
- package/dist/react/renderers/date.js.map +1 -1
- package/dist/react/renderers/email.js +1 -3
- package/dist/react/renderers/email.js.map +1 -1
- package/dist/react/renderers/file.js +6 -7
- package/dist/react/renderers/file.js.map +1 -1
- package/dist/react/renderers/message.js +8 -2
- package/dist/react/renderers/message.js.map +1 -1
- package/dist/react/renderers/number.js +1 -3
- package/dist/react/renderers/number.js.map +1 -1
- package/dist/react/renderers/region.js +1 -3
- package/dist/react/renderers/region.js.map +1 -1
- package/dist/react/renderers/repeater.js +20 -4
- package/dist/react/renderers/repeater.js.map +1 -1
- package/dist/react/renderers/select.js +1 -3
- package/dist/react/renderers/select.js.map +1 -1
- package/dist/react/renderers/text.js +1 -3
- package/dist/react/renderers/text.js.map +1 -1
- package/dist/react/renderers/textarea.js +1 -3
- package/dist/react/renderers/textarea.js.map +1 -1
- package/dist/react/state.d.ts +3 -0
- package/dist/react/state.js +1 -0
- package/dist/react/state.js.map +1 -1
- package/dist/spam/constants.js +7 -1
- package/dist/spam/constants.js.map +1 -1
- package/dist/spam/resolveSpam.js +2 -1
- package/dist/spam/resolveSpam.js.map +1 -1
- package/dist/spam/spamGuard.js +22 -6
- package/dist/spam/spamGuard.js.map +1 -1
- package/dist/spam/types.d.ts +17 -2
- package/dist/submissions/coerceBoolean.js +14 -0
- package/dist/submissions/coerceBoolean.js.map +1 -0
- package/dist/submissions/runSubmission.js +7 -7
- package/dist/submissions/runSubmission.js.map +1 -1
- package/dist/submissions/validateSubmission.js +3 -2
- package/dist/submissions/validateSubmission.js.map +1 -1
- package/dist/translations/de.js +10 -0
- package/dist/translations/de.js.map +1 -1
- package/dist/translations/en.js +10 -0
- package/dist/translations/en.js.map +1 -1
- package/dist/translations/keys.d.ts +10 -0
- package/dist/translations/keys.js +10 -0
- package/dist/translations/keys.js.map +1 -1
- package/dist/translations/server.js +8 -1
- package/dist/translations/server.js.map +1 -1
- package/dist/uploads/captureFileRef.d.ts +10 -3
- package/dist/uploads/captureFileRef.js +18 -9
- package/dist/uploads/captureFileRef.js.map +1 -1
- package/dist/validation/builtin/email.js +1 -1
- package/dist/validation/builtin/email.js.map +1 -1
- package/dist/validation/builtin/notAlreadySubmitted.js +1 -0
- package/dist/validation/builtin/notAlreadySubmitted.js.map +1 -1
- package/dist/validation/emailPattern.js +10 -0
- package/dist/validation/emailPattern.js.map +1 -0
- package/dist/validation/registry.d.ts +2 -1
- package/dist/validation/registry.js +2 -9
- package/dist/validation/registry.js.map +1 -1
- package/package.json +4 -4
package/dist/react/Form.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AnyFormFieldDefinition } from "../fields/types.js";
|
|
2
|
+
import { BodyConverter } from "../actions/body/converters.js";
|
|
2
3
|
import { FormButtonSettings, FormDocument, FormPollSettings, FormResponseSettings } from "../form/types.js";
|
|
3
4
|
import { RendererTranslate } from "./contract.js";
|
|
4
5
|
import { FormEventSink } from "../events/types.js";
|
|
@@ -11,6 +12,21 @@ import { SubmitHandler } from "./submitForm.js";
|
|
|
11
12
|
import { ReactNode } from "react";
|
|
12
13
|
|
|
13
14
|
//#region src/react/Form.d.ts
|
|
15
|
+
/**
|
|
16
|
+
* The success response passed to `onSuccess`, recall-resolved and (for a message) serialized with the
|
|
17
|
+
* form's active converters, so a host can render or toast the resolved response without re-deriving it.
|
|
18
|
+
*/
|
|
19
|
+
type FormSuccessResponse = {
|
|
20
|
+
type: 'message';
|
|
21
|
+
html?: string;
|
|
22
|
+
} | {
|
|
23
|
+
type: 'redirect';
|
|
24
|
+
url?: string;
|
|
25
|
+
};
|
|
26
|
+
/** The second argument to `onSuccess`: the resolved success response (an object, so it can grow). */
|
|
27
|
+
type FormSuccessResult = {
|
|
28
|
+
response?: FormSuccessResponse;
|
|
29
|
+
};
|
|
14
30
|
type FormProps = {
|
|
15
31
|
form: FormDocument;
|
|
16
32
|
fieldTypes?: AnyFormFieldDefinition[];
|
|
@@ -18,8 +34,24 @@ type FormProps = {
|
|
|
18
34
|
renderers?: RenderersConfig;
|
|
19
35
|
apiRoute?: string;
|
|
20
36
|
onSubmit?: SubmitHandler;
|
|
21
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Called after a successful submission with the submission id and the resolved success response
|
|
39
|
+
* (recall-applied, serialized with `converters`), so a host can toast or render it. Fires in both
|
|
40
|
+
* `successBehavior` modes and on the custom-`children` path.
|
|
41
|
+
*/
|
|
42
|
+
onSuccess?: (submissionId?: string, result?: FormSuccessResult) => void;
|
|
22
43
|
onError?: (message: string) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Custom Lexical block converters (e.g. host `icon`/`badge` blocks) spread over the defaults for the
|
|
46
|
+
* client serializer, so those blocks survive in the success message and in the `onSuccess` response.
|
|
47
|
+
*/
|
|
48
|
+
converters?: Record<string, BodyConverter>;
|
|
49
|
+
/**
|
|
50
|
+
* What happens on a successful submit. `'replace'` (default) swaps the form for the success screen;
|
|
51
|
+
* `'reset'` clears the fields in place and shows no success screen, so a host can toast via `onSuccess`
|
|
52
|
+
* and keep the form usable.
|
|
53
|
+
*/
|
|
54
|
+
successBehavior?: 'replace' | 'reset';
|
|
23
55
|
events?: FormEventSink;
|
|
24
56
|
t?: RendererTranslate;
|
|
25
57
|
locale?: string;
|
|
@@ -43,7 +75,8 @@ type FormProps = {
|
|
|
43
75
|
name?: string;
|
|
44
76
|
}; /** A token from your captcha widget; verified server-side when a captcha provider is configured. */
|
|
45
77
|
captchaToken?: string; /** Custom layout: render fields with `useField`/`useFormState` instead of the auto-rendered field loop. */
|
|
46
|
-
children?: ReactNode; /**
|
|
78
|
+
children?: ReactNode; /** Chrome rendered inside the form, above the fields, in default mode (e.g. `<FormSteps />`). */
|
|
79
|
+
header?: ReactNode; /** Additional CSS class names applied to the root `<form>` element (and the success node). */
|
|
47
80
|
className?: string; /** Replace the default submit button entirely. Receives the resolved label and submitting state. */
|
|
48
81
|
renderSubmit?: (props: SubmitButtonRenderProps) => ReactNode; /** Replace the default "Next" button in multi-step forms. */
|
|
49
82
|
renderNext?: (props: NextButtonRenderProps) => ReactNode; /** Replace the default "Back" button in multi-step forms. */
|
|
@@ -62,6 +95,8 @@ declare const Form: ({
|
|
|
62
95
|
onSubmit,
|
|
63
96
|
onSuccess,
|
|
64
97
|
onError,
|
|
98
|
+
converters,
|
|
99
|
+
successBehavior,
|
|
65
100
|
events,
|
|
66
101
|
t,
|
|
67
102
|
locale,
|
|
@@ -79,6 +114,7 @@ declare const Form: ({
|
|
|
79
114
|
honeypot,
|
|
80
115
|
captchaToken,
|
|
81
116
|
children,
|
|
117
|
+
header,
|
|
82
118
|
className,
|
|
83
119
|
renderSubmit,
|
|
84
120
|
renderNext,
|
|
@@ -88,5 +124,5 @@ declare const Form: ({
|
|
|
88
124
|
backButtonClassName
|
|
89
125
|
}: FormProps) => import("react/jsx-runtime").JSX.Element;
|
|
90
126
|
//#endregion
|
|
91
|
-
export { Form, FormProps };
|
|
127
|
+
export { Form, FormProps, FormSuccessResponse, FormSuccessResult };
|
|
92
128
|
//# sourceMappingURL=Form.d.ts.map
|
package/dist/react/Form.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { interpolate } from "../recall/interpolate.js";
|
|
3
2
|
import { keys } from "../translations/keys.js";
|
|
3
|
+
import { interpolate } from "../recall/interpolate.js";
|
|
4
4
|
import { serializeBody } from "../actions/body/serializeBody.js";
|
|
5
5
|
import { noopEventSink } from "../events/noopSink.js";
|
|
6
|
-
import { CAPTCHA_TOKEN_KEY } from "../spam/constants.js";
|
|
6
|
+
import { CAPTCHA_TOKEN_KEY, HONEYPOT_VALUE_KEY } from "../spam/constants.js";
|
|
7
7
|
import { fieldKey, isNamedField } from "../fields/fieldKey.js";
|
|
8
8
|
import { calcExpressionOf, computeCalcFields } from "../calc/computeCalcFields.js";
|
|
9
9
|
import { evaluateCondition } from "../conditions/evaluate.js";
|
|
@@ -16,82 +16,24 @@ import { makeTranslate } from "../translations/makeTranslate.js";
|
|
|
16
16
|
import { emitFormEvent } from "./events.js";
|
|
17
17
|
import { FormContext } from "./FormContext.js";
|
|
18
18
|
import { FormControls } from "./FormControls.js";
|
|
19
|
-
import {
|
|
19
|
+
import { FormFields } from "./FormFields.js";
|
|
20
20
|
import { Honeypot } from "./Honeypot.js";
|
|
21
21
|
import { defaultPresentations } from "./presentation/presentations.js";
|
|
22
22
|
import { resolvePresentations } from "./presentation/registry.js";
|
|
23
|
-
import { applyRecall } from "./recall.js";
|
|
24
23
|
import { resolveRenderers } from "./registry.js";
|
|
25
|
-
import { useField } from "./useField.js";
|
|
26
24
|
import { defaultRenderers } from "./renderers/index.js";
|
|
27
25
|
import { buildFieldTypeRegistry, buildValidationRuleRegistry, visibleFields } from "./resolveForm.js";
|
|
28
26
|
import { formReducer, initialFormState, seedFieldValues } from "./state.js";
|
|
29
27
|
import { submitForm } from "./submitForm.js";
|
|
30
28
|
import { validateFieldValue } from "./validateField.js";
|
|
31
29
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
32
|
-
import {
|
|
30
|
+
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
|
|
33
31
|
//#region src/react/Form.tsx
|
|
34
32
|
const isEmpty = (value) => value == null || value === "" || Array.isArray(value) && value.length === 0;
|
|
35
33
|
/** A stored button label counts only when it is a non-empty string; anything else falls through. */
|
|
36
34
|
const storedLabel = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
|
|
37
|
-
const FIELD_WIDTHS = new Set([
|
|
38
|
-
"full",
|
|
39
|
-
"half",
|
|
40
|
-
"third",
|
|
41
|
-
"twoThirds"
|
|
42
|
-
]);
|
|
43
|
-
const FieldHost = ({ field, renderer, locale, t }) => {
|
|
44
|
-
const id = useId();
|
|
45
|
-
const { value, errors, setValue, onBlur } = useField(field.name);
|
|
46
|
-
return createElement(renderer, {
|
|
47
|
-
field,
|
|
48
|
-
id,
|
|
49
|
-
name: field.name,
|
|
50
|
-
value,
|
|
51
|
-
onChange: setValue,
|
|
52
|
-
onBlur,
|
|
53
|
-
errors,
|
|
54
|
-
required: Boolean(field.required),
|
|
55
|
-
locale,
|
|
56
|
-
t
|
|
57
|
-
});
|
|
58
|
-
};
|
|
59
|
-
/** Hosts a derived (calc) field: read-only, value supplied from `effectiveValues`, never bound via `useField`. */
|
|
60
|
-
const CalcFieldHost = ({ field, renderer, value, locale, t }) => {
|
|
61
|
-
return createElement(renderer, {
|
|
62
|
-
field,
|
|
63
|
-
id: useId(),
|
|
64
|
-
name: field.name,
|
|
65
|
-
value,
|
|
66
|
-
onChange: () => {},
|
|
67
|
-
onBlur: () => {},
|
|
68
|
-
errors: [],
|
|
69
|
-
required: false,
|
|
70
|
-
disabled: true,
|
|
71
|
-
locale,
|
|
72
|
-
t
|
|
73
|
-
});
|
|
74
|
-
};
|
|
75
|
-
/**
|
|
76
|
-
* Hosts a nameless (bare) display block, e.g. a message: no `useField` binding at all. The
|
|
77
|
-
* renderer reads only the instance and the form context; `name` is the row key for consistency.
|
|
78
|
-
*/
|
|
79
|
-
const StaticFieldHost = ({ field, renderer, locale, t }) => {
|
|
80
|
-
return createElement(renderer, {
|
|
81
|
-
field,
|
|
82
|
-
id: useId(),
|
|
83
|
-
name: fieldKey(field),
|
|
84
|
-
value: void 0,
|
|
85
|
-
onChange: () => {},
|
|
86
|
-
onBlur: () => {},
|
|
87
|
-
errors: [],
|
|
88
|
-
required: false,
|
|
89
|
-
locale,
|
|
90
|
-
t
|
|
91
|
-
});
|
|
92
|
-
};
|
|
93
35
|
/** The headless form controller: state, progressive client validation, conditional visibility, submission, events. */
|
|
94
|
-
const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSuccess, onError, events, t, locale = "en", layout, submitLabel, nextLabel, prevLabel, closeLabel
|
|
36
|
+
const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSuccess, onError, converters, successBehavior = "replace", events, t, locale = "en", layout, submitLabel, nextLabel, prevLabel, closeLabel, successMessage, presentation, presentations, onClose, title, initialValues, honeypot, captchaToken, children, header, className, renderSubmit, renderNext, renderBack, submitButtonClassName, nextButtonClassName, backButtonClassName }) => {
|
|
95
37
|
const honeypotName = honeypot === false ? null : honeypot?.name ?? "website";
|
|
96
38
|
const honeypotRef = useRef(null);
|
|
97
39
|
const registry = useMemo(() => buildFieldTypeRegistry(fieldTypes), [fieldTypes]);
|
|
@@ -101,6 +43,8 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
|
|
|
101
43
|
const activePresentation = typeof presentation === "object" ? presentation : presentationRegistry.get(presentation ?? "page") ?? presentationRegistry.get("page") ?? defaultPresentationDescriptors.page;
|
|
102
44
|
const fieldsByName = useMemo(() => new Map(form.fields.filter(isNamedField).map((field) => [field.name, field])), [form.fields]);
|
|
103
45
|
const translate = useMemo(() => t ?? makeTranslate(en), [t]);
|
|
46
|
+
const resolvedCloseLabel = closeLabel ?? translate(keys.formClose);
|
|
47
|
+
const resolvedSuccessMessage = successMessage ?? translate(keys.formSuccess);
|
|
104
48
|
const docButtons = form.buttons;
|
|
105
49
|
const labels = useMemo(() => ({
|
|
106
50
|
prev: prevLabel ?? storedLabel(docButtons?.prevLabel) ?? translate(keys.formBack),
|
|
@@ -141,6 +85,7 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
|
|
|
141
85
|
const startedRef = useRef(false);
|
|
142
86
|
const submittedRef = useRef(false);
|
|
143
87
|
const submittingRef = useRef(false);
|
|
88
|
+
const advancingRef = useRef(false);
|
|
144
89
|
const flowRef = useRef(flow);
|
|
145
90
|
flowRef.current = flow;
|
|
146
91
|
const dispatch = useCallback((action) => {
|
|
@@ -210,47 +155,111 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
|
|
|
210
155
|
field: field.name,
|
|
211
156
|
value: recall(field.name)
|
|
212
157
|
}));
|
|
213
|
-
|
|
158
|
+
/**
|
|
159
|
+
* The recall-resolved success response: a redirect's url, or the response message serialized with
|
|
160
|
+
* the active `converters` (so host blocks survive). Shared by the success screen and the value
|
|
161
|
+
* handed to `onSuccess`, so both stay identical.
|
|
162
|
+
*/
|
|
163
|
+
const resolveSuccessResponse = () => {
|
|
164
|
+
const response = form.response;
|
|
165
|
+
if (response?.type === "redirect") return {
|
|
166
|
+
type: "redirect",
|
|
167
|
+
url: response.redirect?.url ?? void 0
|
|
168
|
+
};
|
|
169
|
+
const message = response?.type === "message" || response?.type == null ? response?.message : void 0;
|
|
170
|
+
if (!message) return;
|
|
171
|
+
return {
|
|
172
|
+
type: "message",
|
|
173
|
+
html: serializeBody(message, {
|
|
174
|
+
values: formattedValues(),
|
|
175
|
+
descriptors: descriptorsFor(answerableFields()),
|
|
176
|
+
converters
|
|
177
|
+
})
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
const stepKeys = flow && currentStepId ? stepFieldNames(flow, currentStepId) : [];
|
|
181
|
+
const stepKeySet = new Set(stepKeys);
|
|
182
|
+
const stepVisible = visible.filter((field) => stepKeySet.has(fieldKey(field)));
|
|
183
|
+
const validateRepeaterSubFields = async (repeaters) => {
|
|
184
|
+
const errors = {};
|
|
185
|
+
for (const field of repeaters.filter((f) => f.blockType === "repeater" && isNamedField(f))) {
|
|
186
|
+
const rows = Array.isArray(effectiveValues[field.name]) ? effectiveValues[field.name] : [];
|
|
187
|
+
const subFields = (Array.isArray(field.subFields) ? field.subFields : []).filter(isNamedField);
|
|
188
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
|
|
189
|
+
const row = rows[rowIndex] ?? {};
|
|
190
|
+
for (const subField of subFields) {
|
|
191
|
+
if (!evaluateCondition(subField.visibleWhen, row)) continue;
|
|
192
|
+
if (!evaluateCondition(subField.validateWhen, row)) continue;
|
|
193
|
+
const subResult = await validateFieldValue({
|
|
194
|
+
field: subField,
|
|
195
|
+
value: row[subField.name],
|
|
196
|
+
registry,
|
|
197
|
+
ruleRegistry,
|
|
198
|
+
answers: row,
|
|
199
|
+
locale,
|
|
200
|
+
t: translate
|
|
201
|
+
});
|
|
202
|
+
const compositeKey = `${field.name}[${rowIndex}].${subField.name}`;
|
|
203
|
+
if (subResult.errors.length > 0) errors[compositeKey] = subResult.errors;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return errors;
|
|
208
|
+
};
|
|
214
209
|
const goNext = async () => {
|
|
215
|
-
if (!flow || !currentStepId) return;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
210
|
+
if (!flow || !currentStepId || advancingRef.current) return;
|
|
211
|
+
advancingRef.current = true;
|
|
212
|
+
try {
|
|
213
|
+
const results = await Promise.all(stepVisible.filter((field) => !calcExpressionOf(field)).filter((field) => registry.get(field.blockType)?.value !== "none").filter(isNamedField).filter((field) => evaluateCondition(field.validateWhen, effectiveValues)).map(async (field) => ({
|
|
219
214
|
field,
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
215
|
+
...await validateFieldValue({
|
|
216
|
+
field,
|
|
217
|
+
value: effectiveValues[field.name],
|
|
218
|
+
registry,
|
|
219
|
+
ruleRegistry,
|
|
220
|
+
answers: effectiveValues,
|
|
221
|
+
locale,
|
|
222
|
+
t: translate
|
|
223
|
+
})
|
|
224
|
+
})));
|
|
225
|
+
let hasError = false;
|
|
226
|
+
for (const result of results) {
|
|
227
|
+
rawDispatch({
|
|
228
|
+
type: "TOUCH",
|
|
229
|
+
name: result.field.name
|
|
230
|
+
});
|
|
231
|
+
rawDispatch({
|
|
232
|
+
type: "SET_FIELD_ISSUES",
|
|
233
|
+
name: result.field.name,
|
|
234
|
+
errors: result.errors
|
|
235
|
+
});
|
|
236
|
+
if (result.errors.length > 0) hasError = true;
|
|
237
|
+
}
|
|
238
|
+
const repeaterErrors = await validateRepeaterSubFields(stepVisible);
|
|
239
|
+
for (const [compositeKey, errs] of Object.entries(repeaterErrors)) {
|
|
240
|
+
rawDispatch({
|
|
241
|
+
type: "SET_FIELD_ISSUES",
|
|
242
|
+
name: compositeKey,
|
|
243
|
+
errors: errs
|
|
244
|
+
});
|
|
245
|
+
hasError = true;
|
|
246
|
+
}
|
|
247
|
+
if (hasError) return;
|
|
248
|
+
const next = resolveNextStepId(flow, currentStepId, effectiveValues);
|
|
249
|
+
if (!next) return;
|
|
250
|
+
emitFormEvent(sinkRef.current, formIdRef.current, {
|
|
251
|
+
type: "step.completed",
|
|
252
|
+
stepId: currentStepId
|
|
233
253
|
});
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
254
|
+
setHistory((prev) => [...prev, currentStepId]);
|
|
255
|
+
setCurrentStepId(next);
|
|
256
|
+
emitFormEvent(sinkRef.current, formIdRef.current, {
|
|
257
|
+
type: "step.viewed",
|
|
258
|
+
stepId: next
|
|
238
259
|
});
|
|
239
|
-
|
|
260
|
+
} finally {
|
|
261
|
+
advancingRef.current = false;
|
|
240
262
|
}
|
|
241
|
-
if (hasError) return;
|
|
242
|
-
const next = resolveNextStepId(flow, currentStepId, effectiveValues);
|
|
243
|
-
if (!next) return;
|
|
244
|
-
emitFormEvent(sinkRef.current, formIdRef.current, {
|
|
245
|
-
type: "step.completed",
|
|
246
|
-
stepId: currentStepId
|
|
247
|
-
});
|
|
248
|
-
setHistory((prev) => [...prev, currentStepId]);
|
|
249
|
-
setCurrentStepId(next);
|
|
250
|
-
emitFormEvent(sinkRef.current, formIdRef.current, {
|
|
251
|
-
type: "step.viewed",
|
|
252
|
-
stepId: next
|
|
253
|
-
});
|
|
254
263
|
};
|
|
255
264
|
const goBack = () => {
|
|
256
265
|
const prev = history[history.length - 1];
|
|
@@ -306,28 +315,7 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
|
|
|
306
315
|
message: firstError
|
|
307
316
|
});
|
|
308
317
|
}
|
|
309
|
-
|
|
310
|
-
const rows = Array.isArray(effectiveValues[field.name]) ? effectiveValues[field.name] : [];
|
|
311
|
-
const subFields = (Array.isArray(field.subFields) ? field.subFields : []).filter(isNamedField);
|
|
312
|
-
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
|
|
313
|
-
const row = rows[rowIndex] ?? {};
|
|
314
|
-
for (const subField of subFields) {
|
|
315
|
-
if (!evaluateCondition(subField.visibleWhen, row)) continue;
|
|
316
|
-
if (!evaluateCondition(subField.validateWhen, row)) continue;
|
|
317
|
-
const subResult = await validateFieldValue({
|
|
318
|
-
field: subField,
|
|
319
|
-
value: row[subField.name],
|
|
320
|
-
registry,
|
|
321
|
-
ruleRegistry,
|
|
322
|
-
answers: row,
|
|
323
|
-
locale,
|
|
324
|
-
t: translate
|
|
325
|
-
});
|
|
326
|
-
const compositeKey = `${field.name}[${rowIndex}].${subField.name}`;
|
|
327
|
-
if (subResult.errors.length > 0) errors[compositeKey] = subResult.errors;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
318
|
+
Object.assign(errors, await validateRepeaterSubFields(visible));
|
|
331
319
|
rawDispatch({
|
|
332
320
|
type: "SET_ALL_ISSUES",
|
|
333
321
|
errors
|
|
@@ -341,7 +329,7 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
|
|
|
341
329
|
if (honeypotName) {
|
|
342
330
|
const decoy = honeypotRef.current?.value ?? "";
|
|
343
331
|
if (decoy !== "") values.push({
|
|
344
|
-
field:
|
|
332
|
+
field: HONEYPOT_VALUE_KEY,
|
|
345
333
|
value: decoy
|
|
346
334
|
});
|
|
347
335
|
}
|
|
@@ -360,13 +348,28 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
|
|
|
360
348
|
submittingRef.current = false;
|
|
361
349
|
if (result.ok) {
|
|
362
350
|
submittedRef.current = true;
|
|
363
|
-
rawDispatch({ type: "SUBMIT_SUCCESS" });
|
|
364
351
|
emitFormEvent(sinkRef.current, formIdRef.current, {
|
|
365
352
|
type: "submission.created",
|
|
366
353
|
submissionId: result.submissionId
|
|
367
354
|
});
|
|
368
|
-
onSuccess?.(result.submissionId);
|
|
369
|
-
if (
|
|
355
|
+
onSuccess?.(result.submissionId, { response: resolveSuccessResponse() });
|
|
356
|
+
if (successBehavior === "reset") {
|
|
357
|
+
rawDispatch({
|
|
358
|
+
type: "RESET",
|
|
359
|
+
values: {
|
|
360
|
+
...seedFieldValues(form.fields),
|
|
361
|
+
...initialValues ?? {}
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
if (flow) {
|
|
365
|
+
setCurrentStepId(firstStepId(flow));
|
|
366
|
+
setHistory([]);
|
|
367
|
+
}
|
|
368
|
+
startedRef.current = false;
|
|
369
|
+
} else {
|
|
370
|
+
rawDispatch({ type: "SUBMIT_SUCCESS" });
|
|
371
|
+
if (activePresentation.dismissOnSuccess) handleClose();
|
|
372
|
+
}
|
|
370
373
|
const redirectUrl = form.response?.type === "redirect" ? form.response.redirect?.url : void 0;
|
|
371
374
|
if (typeof redirectUrl === "string" && redirectUrl.length > 0 && typeof window !== "undefined") window.location.assign(redirectUrl);
|
|
372
375
|
} else {
|
|
@@ -374,7 +377,7 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
|
|
|
374
377
|
type: "SET_ALL_ISSUES",
|
|
375
378
|
errors: result.fieldErrors
|
|
376
379
|
});
|
|
377
|
-
const message = result.message ??
|
|
380
|
+
const message = result.message ?? translate(keys.formSubmitFailed);
|
|
378
381
|
rawDispatch({
|
|
379
382
|
type: "SUBMIT_ERROR",
|
|
380
383
|
message
|
|
@@ -382,6 +385,13 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
|
|
|
382
385
|
onError?.(message);
|
|
383
386
|
}
|
|
384
387
|
};
|
|
388
|
+
const handleKeyDown = (event) => {
|
|
389
|
+
if (!flow || event.key !== "Enter") return;
|
|
390
|
+
const target = event.target;
|
|
391
|
+
if (target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement || target instanceof HTMLButtonElement) return;
|
|
392
|
+
if (target instanceof HTMLInputElement && (target.type === "submit" || target.type === "button")) return;
|
|
393
|
+
event.preventDefault();
|
|
394
|
+
};
|
|
385
395
|
const contextValue = {
|
|
386
396
|
form,
|
|
387
397
|
state,
|
|
@@ -410,7 +420,10 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
|
|
|
410
420
|
rendererRegistry,
|
|
411
421
|
labels,
|
|
412
422
|
t: translate,
|
|
413
|
-
effectiveValues
|
|
423
|
+
effectiveValues,
|
|
424
|
+
recall,
|
|
425
|
+
renderedFields: (flow ? stepVisible : visible).filter((field) => field.hidden !== true && field.calcDisplay !== false),
|
|
426
|
+
converters
|
|
414
427
|
};
|
|
415
428
|
const PresentationWrapper = activePresentation.Wrapper;
|
|
416
429
|
const wrap = (content) => PresentationWrapper ? /* @__PURE__ */ jsx(PresentationWrapper, {
|
|
@@ -418,7 +431,7 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
|
|
|
418
431
|
open: true,
|
|
419
432
|
onClose: handleClose,
|
|
420
433
|
title,
|
|
421
|
-
closeLabel,
|
|
434
|
+
closeLabel: resolvedCloseLabel,
|
|
422
435
|
children: content
|
|
423
436
|
}) : content;
|
|
424
437
|
if (children !== void 0) return /* @__PURE__ */ jsx(FormContext.Provider, {
|
|
@@ -427,6 +440,7 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
|
|
|
427
440
|
className: cn("fb-form-root", className),
|
|
428
441
|
noValidate: true,
|
|
429
442
|
onSubmit: handleSubmit,
|
|
443
|
+
onKeyDown: handleKeyDown,
|
|
430
444
|
"data-fb-presentation": activePresentation.name,
|
|
431
445
|
"data-fb-density": activePresentation.density,
|
|
432
446
|
children: [honeypotName ? /* @__PURE__ */ jsx(Honeypot, {
|
|
@@ -436,11 +450,8 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
|
|
|
436
450
|
}))
|
|
437
451
|
});
|
|
438
452
|
if (state.submitted) {
|
|
439
|
-
const
|
|
440
|
-
const responseHtml =
|
|
441
|
-
values: formattedValues(),
|
|
442
|
-
descriptors: descriptorsFor(answerableFields())
|
|
443
|
-
}) : void 0;
|
|
453
|
+
const successResponse = resolveSuccessResponse();
|
|
454
|
+
const responseHtml = successResponse?.type === "message" ? successResponse.html : void 0;
|
|
444
455
|
return /* @__PURE__ */ jsx(FormContext.Provider, {
|
|
445
456
|
value: contextValue,
|
|
446
457
|
children: wrap(responseHtml ? /* @__PURE__ */ jsx("div", {
|
|
@@ -454,17 +465,17 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
|
|
|
454
465
|
className: cn("fb-form__success", className),
|
|
455
466
|
"data-fb-presentation": activePresentation.name,
|
|
456
467
|
"data-fb-density": activePresentation.density,
|
|
457
|
-
children: interpolate(
|
|
468
|
+
children: interpolate(resolvedSuccessMessage, recall)
|
|
458
469
|
}))
|
|
459
470
|
});
|
|
460
471
|
}
|
|
461
|
-
const rendered = (flow ? stepVisible : visible).filter((field) => field.hidden !== true && field.calcDisplay !== false);
|
|
462
472
|
return /* @__PURE__ */ jsx(FormContext.Provider, {
|
|
463
473
|
value: contextValue,
|
|
464
474
|
children: wrap(/* @__PURE__ */ jsxs("form", {
|
|
465
475
|
className: cn("fb-form-root", className),
|
|
466
476
|
noValidate: true,
|
|
467
477
|
onSubmit: handleSubmit,
|
|
478
|
+
onKeyDown: handleKeyDown,
|
|
468
479
|
"data-fb-presentation": activePresentation.name,
|
|
469
480
|
"data-fb-density": activePresentation.density,
|
|
470
481
|
children: [
|
|
@@ -472,35 +483,8 @@ const Form = ({ form, fieldTypes, rules, renderers, apiRoute, onSubmit, onSucces
|
|
|
472
483
|
name: honeypotName,
|
|
473
484
|
inputRef: honeypotRef
|
|
474
485
|
}) : null,
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
children: rendered.map((field) => {
|
|
478
|
-
const renderer = rendererRegistry.get(field.blockType);
|
|
479
|
-
if (!renderer) return null;
|
|
480
|
-
const width = typeof field.width === "string" && FIELD_WIDTHS.has(field.width) ? field.width : void 0;
|
|
481
|
-
const recalledField = applyRecall(field, recall);
|
|
482
|
-
return /* @__PURE__ */ jsx("div", {
|
|
483
|
-
...widthProps(width),
|
|
484
|
-
children: !isNamedField(recalledField) ? /* @__PURE__ */ jsx(StaticFieldHost, {
|
|
485
|
-
field: recalledField,
|
|
486
|
-
renderer,
|
|
487
|
-
locale,
|
|
488
|
-
t: translate
|
|
489
|
-
}) : calcExpressionOf(recalledField) ? /* @__PURE__ */ jsx(CalcFieldHost, {
|
|
490
|
-
field: recalledField,
|
|
491
|
-
renderer,
|
|
492
|
-
value: effectiveValues[recalledField.name],
|
|
493
|
-
locale,
|
|
494
|
-
t: translate
|
|
495
|
-
}) : /* @__PURE__ */ jsx(FieldHost, {
|
|
496
|
-
field: recalledField,
|
|
497
|
-
renderer,
|
|
498
|
-
locale,
|
|
499
|
-
t: translate
|
|
500
|
-
})
|
|
501
|
-
}, fieldKey(field));
|
|
502
|
-
})
|
|
503
|
-
}),
|
|
486
|
+
header,
|
|
487
|
+
/* @__PURE__ */ jsx(FormFields, { layout }),
|
|
504
488
|
state.submitError ? /* @__PURE__ */ jsx("p", {
|
|
505
489
|
role: "alert",
|
|
506
490
|
className: "fb-form__submit-error",
|