@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.
Files changed (174) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/dist/actions/body/serializeBody.d.ts +3 -1
  3. package/dist/actions/body/serializeBody.js.map +1 -1
  4. package/dist/actions/buildActionBlocks.js +12 -9
  5. package/dist/actions/buildActionBlocks.js.map +1 -1
  6. package/dist/actions/builtin/confirmation.js +13 -68
  7. package/dist/actions/builtin/confirmation.js.map +1 -1
  8. package/dist/actions/builtin/emailAction.d.ts +17 -0
  9. package/dist/actions/builtin/emailAction.js +83 -0
  10. package/dist/actions/builtin/emailAction.js.map +1 -0
  11. package/dist/actions/builtin/emailTeam.js +14 -74
  12. package/dist/actions/builtin/emailTeam.js.map +1 -1
  13. package/dist/actions/builtin/index.d.ts +9 -12
  14. package/dist/actions/builtin/index.js +11 -11
  15. package/dist/actions/builtin/index.js.map +1 -1
  16. package/dist/actions/defineAction.d.ts +2 -2
  17. package/dist/actions/defineAction.js.map +1 -1
  18. package/dist/actions/emailRecipients.js +75 -31
  19. package/dist/actions/emailRecipients.js.map +1 -1
  20. package/dist/actions/registry.d.ts +2 -1
  21. package/dist/actions/registry.js +2 -6
  22. package/dist/actions/registry.js.map +1 -1
  23. package/dist/actions/task.js +5 -3
  24. package/dist/actions/task.js.map +1 -1
  25. package/dist/calc/evaluate.js +1 -2
  26. package/dist/calc/evaluate.js.map +1 -1
  27. package/dist/calc/normalizeCalc.js +1 -2
  28. package/dist/calc/normalizeCalc.js.map +1 -1
  29. package/dist/calc/types.js.map +1 -1
  30. package/dist/collections/buttonFields.js +1 -1
  31. package/dist/collections/formSubmissions.js +2 -1
  32. package/dist/collections/formSubmissions.js.map +1 -1
  33. package/dist/collections/forms.js +32 -100
  34. package/dist/collections/forms.js.map +1 -1
  35. package/dist/collections/formsEndpoints.js +101 -0
  36. package/dist/collections/formsEndpoints.js.map +1 -0
  37. package/dist/conditions/evaluate.js +1 -1
  38. package/dist/conditions/evaluate.js.map +1 -1
  39. package/dist/conditions/fieldTypes.d.ts +3 -1
  40. package/dist/conditions/fieldTypes.js +4 -1
  41. package/dist/conditions/fieldTypes.js.map +1 -1
  42. package/dist/conditions/normalizeConditions.js +18 -5
  43. package/dist/conditions/normalizeConditions.js.map +1 -1
  44. package/dist/consent/captureConsent.d.ts +7 -0
  45. package/dist/consent/captureConsent.js +7 -0
  46. package/dist/consent/captureConsent.js.map +1 -1
  47. package/dist/consent/consentSourcesField.d.ts +4 -0
  48. package/dist/consent/consentSourcesField.js +5 -1
  49. package/dist/consent/consentSourcesField.js.map +1 -1
  50. package/dist/consent/resolveConsentEntries.d.ts +1 -1
  51. package/dist/consent/resolveConsentEntries.js +1 -4
  52. package/dist/consent/resolveConsentEntries.js.map +1 -1
  53. package/dist/consent/resolveConsentSourcesRequest.js +0 -1
  54. package/dist/consent/resolveConsentSourcesRequest.js.map +1 -1
  55. package/dist/consent/resolveConsentStatements.d.ts +1 -1
  56. package/dist/consent/resolveConsentStatements.js.map +1 -1
  57. package/dist/consent/types.d.ts +9 -1
  58. package/dist/email/departmentsField.js +1 -1
  59. package/dist/exports/react.d.ts +5 -4
  60. package/dist/exports/react.js +3 -2
  61. package/dist/exports/rsc.js +1 -1
  62. package/dist/exports/types.d.ts +1 -1
  63. package/dist/fields/builtin/email.js +1 -1
  64. package/dist/fields/builtin/email.js.map +1 -1
  65. package/dist/fields/builtin/message.js +1 -1
  66. package/dist/fields/builtin/repeater.js +1 -1
  67. package/dist/fields/builtin/select.js +1 -1
  68. package/dist/fields/registry.d.ts +2 -1
  69. package/dist/fields/registry.js +2 -6
  70. package/dist/fields/registry.js.map +1 -1
  71. package/dist/fields/sharedConfig.js +1 -1
  72. package/dist/flow/normalizeFlow.js +7 -3
  73. package/dist/flow/normalizeFlow.js.map +1 -1
  74. package/dist/index.d.ts +4 -201
  75. package/dist/index.js +11 -4
  76. package/dist/index.js.map +1 -1
  77. package/dist/options.d.ts +229 -0
  78. package/dist/plugin/applyRegistryConfig.d.ts +11 -0
  79. package/dist/plugin/applyRegistryConfig.js +20 -0
  80. package/dist/plugin/applyRegistryConfig.js.map +1 -0
  81. package/dist/plugin/registerCollections.js +2 -1
  82. package/dist/plugin/registerCollections.js.map +1 -1
  83. package/dist/poll/buildPollOptionSourceFields.js +2 -2
  84. package/dist/poll/buildPollOptionSourceFields.js.map +1 -1
  85. package/dist/poll/definePollOptionSource.d.ts +2 -2
  86. package/dist/poll/definePollOptionSource.js.map +1 -1
  87. package/dist/poll/pollTypeRegistry.js +2 -4
  88. package/dist/poll/pollTypeRegistry.js.map +1 -1
  89. package/dist/poll/registry.d.ts +2 -1
  90. package/dist/poll/registry.js +2 -6
  91. package/dist/poll/registry.js.map +1 -1
  92. package/dist/poll/resolvePollOptionsRequest.js +0 -1
  93. package/dist/poll/resolvePollOptionsRequest.js.map +1 -1
  94. package/dist/prefill/valuesFromSearchParams.js +2 -7
  95. package/dist/prefill/valuesFromSearchParams.js.map +1 -1
  96. package/dist/react/Form.d.ts +39 -3
  97. package/dist/react/Form.js +148 -164
  98. package/dist/react/Form.js.map +1 -1
  99. package/dist/react/FormContext.d.ts +7 -1
  100. package/dist/react/FormContext.js.map +1 -1
  101. package/dist/react/FormFields.d.ts +18 -0
  102. package/dist/react/FormFields.js +114 -0
  103. package/dist/react/FormFields.js.map +1 -0
  104. package/dist/react/FormLayout.d.ts +4 -2
  105. package/dist/react/FormLayout.js +3 -2
  106. package/dist/react/FormLayout.js.map +1 -1
  107. package/dist/react/Poll.js +15 -5
  108. package/dist/react/Poll.js.map +1 -1
  109. package/dist/react/presentation/DialogSurface.js +1 -4
  110. package/dist/react/presentation/DialogSurface.js.map +1 -1
  111. package/dist/react/renderers/calculation.js +1 -3
  112. package/dist/react/renderers/calculation.js.map +1 -1
  113. package/dist/react/renderers/checkbox.js +1 -3
  114. package/dist/react/renderers/checkbox.js.map +1 -1
  115. package/dist/react/renderers/consent.js +2 -3
  116. package/dist/react/renderers/consent.js.map +1 -1
  117. package/dist/react/renderers/date.js +1 -3
  118. package/dist/react/renderers/date.js.map +1 -1
  119. package/dist/react/renderers/email.js +1 -3
  120. package/dist/react/renderers/email.js.map +1 -1
  121. package/dist/react/renderers/file.js +6 -7
  122. package/dist/react/renderers/file.js.map +1 -1
  123. package/dist/react/renderers/message.js +8 -2
  124. package/dist/react/renderers/message.js.map +1 -1
  125. package/dist/react/renderers/number.js +1 -3
  126. package/dist/react/renderers/number.js.map +1 -1
  127. package/dist/react/renderers/region.js +1 -3
  128. package/dist/react/renderers/region.js.map +1 -1
  129. package/dist/react/renderers/repeater.js +20 -4
  130. package/dist/react/renderers/repeater.js.map +1 -1
  131. package/dist/react/renderers/select.js +1 -3
  132. package/dist/react/renderers/select.js.map +1 -1
  133. package/dist/react/renderers/text.js +1 -3
  134. package/dist/react/renderers/text.js.map +1 -1
  135. package/dist/react/renderers/textarea.js +1 -3
  136. package/dist/react/renderers/textarea.js.map +1 -1
  137. package/dist/react/state.d.ts +3 -0
  138. package/dist/react/state.js +1 -0
  139. package/dist/react/state.js.map +1 -1
  140. package/dist/spam/constants.js +7 -1
  141. package/dist/spam/constants.js.map +1 -1
  142. package/dist/spam/resolveSpam.js +2 -1
  143. package/dist/spam/resolveSpam.js.map +1 -1
  144. package/dist/spam/spamGuard.js +22 -6
  145. package/dist/spam/spamGuard.js.map +1 -1
  146. package/dist/spam/types.d.ts +17 -2
  147. package/dist/submissions/coerceBoolean.js +14 -0
  148. package/dist/submissions/coerceBoolean.js.map +1 -0
  149. package/dist/submissions/runSubmission.js +7 -7
  150. package/dist/submissions/runSubmission.js.map +1 -1
  151. package/dist/submissions/validateSubmission.js +3 -2
  152. package/dist/submissions/validateSubmission.js.map +1 -1
  153. package/dist/translations/de.js +10 -0
  154. package/dist/translations/de.js.map +1 -1
  155. package/dist/translations/en.js +10 -0
  156. package/dist/translations/en.js.map +1 -1
  157. package/dist/translations/keys.d.ts +10 -0
  158. package/dist/translations/keys.js +10 -0
  159. package/dist/translations/keys.js.map +1 -1
  160. package/dist/translations/server.js +8 -1
  161. package/dist/translations/server.js.map +1 -1
  162. package/dist/uploads/captureFileRef.d.ts +10 -3
  163. package/dist/uploads/captureFileRef.js +18 -9
  164. package/dist/uploads/captureFileRef.js.map +1 -1
  165. package/dist/validation/builtin/email.js +1 -1
  166. package/dist/validation/builtin/email.js.map +1 -1
  167. package/dist/validation/builtin/notAlreadySubmitted.js +1 -0
  168. package/dist/validation/builtin/notAlreadySubmitted.js.map +1 -1
  169. package/dist/validation/emailPattern.js +10 -0
  170. package/dist/validation/emailPattern.js.map +1 -0
  171. package/dist/validation/registry.d.ts +2 -1
  172. package/dist/validation/registry.js +2 -9
  173. package/dist/validation/registry.js.map +1 -1
  174. package/package.json +4 -4
@@ -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
- onSuccess?: (submissionId?: string) => void;
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; /** Additional CSS class names applied to the root `<form>` element (and the success node). */
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
@@ -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 { FormLayout, widthProps } from "./FormLayout.js";
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 { createElement, useCallback, useEffect, useId, useMemo, useReducer, useRef, useState } from "react";
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 = "Close", successMessage = "Thank you.", presentation, presentations, onClose, title, initialValues, honeypot, captchaToken, children, className, renderSubmit, renderNext, renderBack, submitButtonClassName, nextButtonClassName, backButtonClassName }) => {
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
- const stepVisible = (flow && currentStepId ? stepFieldNames(flow, currentStepId) : []).map((key) => visible.find((field) => fieldKey(field) === key)).filter((field) => Boolean(field));
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
- 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) => ({
217
- field,
218
- ...await validateFieldValue({
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
- value: effectiveValues[field.name],
221
- registry,
222
- ruleRegistry,
223
- answers: effectiveValues,
224
- locale,
225
- t: translate
226
- })
227
- })));
228
- let hasError = false;
229
- for (const result of results) {
230
- rawDispatch({
231
- type: "TOUCH",
232
- name: result.field.name
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
- rawDispatch({
235
- type: "SET_FIELD_ISSUES",
236
- name: result.field.name,
237
- errors: result.errors
254
+ setHistory((prev) => [...prev, currentStepId]);
255
+ setCurrentStepId(next);
256
+ emitFormEvent(sinkRef.current, formIdRef.current, {
257
+ type: "step.viewed",
258
+ stepId: next
238
259
  });
239
- if (result.errors.length > 0) hasError = true;
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
- for (const field of visible.filter((f) => f.blockType === "repeater" && isNamedField(f))) {
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: honeypotName,
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 (activePresentation.dismissOnSuccess) handleClose();
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 ?? "Submission failed";
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 responseMessage = form.response?.type === "message" || form.response?.type == null ? form.response?.message : void 0;
440
- const responseHtml = responseMessage ? serializeBody(responseMessage, {
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(successMessage, recall)
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
- /* @__PURE__ */ jsx(FormLayout, {
476
- enabled: layout !== false,
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",