@wix/headless-forms 0.0.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 +46 -0
- package/cjs/dist/react/Form.d.ts +802 -0
- package/cjs/dist/react/Form.js +776 -0
- package/cjs/dist/react/Phone.d.ts +47 -0
- package/cjs/dist/react/Phone.js +56 -0
- package/cjs/dist/react/constants/calling-country-codes.d.ts +242 -0
- package/cjs/dist/react/constants/calling-country-codes.js +242 -0
- package/cjs/dist/react/context/FieldContext.d.ts +12 -0
- package/cjs/dist/react/context/FieldContext.js +16 -0
- package/cjs/dist/react/context/FieldLayoutContext.d.ts +12 -0
- package/cjs/dist/react/context/FieldLayoutContext.js +21 -0
- package/cjs/dist/react/core/Form.d.ts +342 -0
- package/cjs/dist/react/core/Form.js +278 -0
- package/cjs/dist/react/index.d.ts +3 -0
- package/cjs/dist/react/index.js +42 -0
- package/cjs/dist/react/types.d.ts +3 -0
- package/cjs/dist/react/types.js +2 -0
- package/cjs/dist/react/utils.d.ts +13 -0
- package/cjs/dist/react/utils.js +20 -0
- package/cjs/dist/services/form-service.d.ts +114 -0
- package/cjs/dist/services/form-service.js +152 -0
- package/cjs/dist/services/index.d.ts +1 -0
- package/cjs/dist/services/index.js +17 -0
- package/cjs/package.json +3 -0
- package/dist/react/Form.d.ts +802 -0
- package/dist/react/Form.js +740 -0
- package/dist/react/Phone.d.ts +47 -0
- package/dist/react/Phone.js +50 -0
- package/dist/react/constants/calling-country-codes.d.ts +242 -0
- package/dist/react/constants/calling-country-codes.js +241 -0
- package/dist/react/context/FieldContext.d.ts +12 -0
- package/dist/react/context/FieldContext.js +9 -0
- package/dist/react/context/FieldLayoutContext.d.ts +12 -0
- package/dist/react/context/FieldLayoutContext.js +13 -0
- package/dist/react/core/Form.d.ts +342 -0
- package/dist/react/core/Form.js +269 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.js +3 -0
- package/dist/react/types.d.ts +3 -0
- package/dist/react/types.js +1 -0
- package/dist/react/utils.d.ts +13 -0
- package/dist/react/utils.js +17 -0
- package/dist/services/form-service.d.ts +114 -0
- package/dist/services/form-service.js +148 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/package.json +62 -0
- package/react/package.json +4 -0
- package/services/package.json +4 -0
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState, useCallback } from 'react';
|
|
3
|
+
import { AsChildSlot } from '@wix/headless-utils/react';
|
|
4
|
+
import { useForm, FormProvider, } from '@wix/form-public';
|
|
5
|
+
import { Root as CoreRoot, Loading as CoreLoading, LoadingError as CoreLoadingError, Error as CoreError, Submitted as CoreSubmitted, Fields as CoreFields, Field as CoreField, } from './core/Form.js';
|
|
6
|
+
import { FieldContext, useFieldContext, } from './context/FieldContext.js';
|
|
7
|
+
import { FieldLayoutProvider, useFieldLayout, } from './context/FieldLayoutContext.js';
|
|
8
|
+
var TestIds;
|
|
9
|
+
(function (TestIds) {
|
|
10
|
+
TestIds["formRoot"] = "form-root";
|
|
11
|
+
TestIds["form"] = "form";
|
|
12
|
+
TestIds["formLoading"] = "form-loading";
|
|
13
|
+
TestIds["formLoadingError"] = "form-loading-error";
|
|
14
|
+
TestIds["formError"] = "form-error";
|
|
15
|
+
TestIds["formSubmitted"] = "form-submitted";
|
|
16
|
+
TestIds["fieldRoot"] = "field-root";
|
|
17
|
+
TestIds["fieldLabel"] = "field-label";
|
|
18
|
+
TestIds["fieldInputWrapper"] = "field-input-wrapper";
|
|
19
|
+
TestIds["fieldInput"] = "field-input";
|
|
20
|
+
TestIds["fieldError"] = "field-error";
|
|
21
|
+
})(TestIds || (TestIds = {}));
|
|
22
|
+
/**
|
|
23
|
+
* Root component that provides all necessary service contexts for a complete form experience.
|
|
24
|
+
* This component sets up the Form service and provides context to child components.
|
|
25
|
+
* Must be used as the top-level component for all form functionality.
|
|
26
|
+
*
|
|
27
|
+
* @component
|
|
28
|
+
* @param {RootProps} props - The component props
|
|
29
|
+
* @param {React.ReactNode} props.children - Child components that will have access to form context
|
|
30
|
+
* @param {FormServiceConfig} props.formServiceConfig - Form service configuration object
|
|
31
|
+
* @param {boolean} [props.asChild] - Whether to render as a child component
|
|
32
|
+
* @param {string} [props.className] - CSS classes to apply to the root element
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* import { Form } from '@wix/headless-forms/react';
|
|
36
|
+
* import { loadFormServiceConfig } from '@wix/headless-forms/services';
|
|
37
|
+
*
|
|
38
|
+
* const FIELD_MAP = {
|
|
39
|
+
* TEXT_INPUT: TextInput,
|
|
40
|
+
* TEXT_AREA: TextArea,
|
|
41
|
+
* CHECKBOX: Checkbox,
|
|
42
|
+
* // ... other field components
|
|
43
|
+
* };
|
|
44
|
+
*
|
|
45
|
+
* // Pattern 1: Pre-loaded form data (SSR/SSG)
|
|
46
|
+
* function FormPage({ formServiceConfig }) {
|
|
47
|
+
* return (
|
|
48
|
+
* <Form.Root formServiceConfig={formServiceConfig}>
|
|
49
|
+
* <Form.Loading className="flex justify-center p-4" />
|
|
50
|
+
* <Form.LoadingError className="text-destructive px-4 py-3 rounded mb-4" />
|
|
51
|
+
* <Form.Fields fieldMap={FIELD_MAP} />
|
|
52
|
+
* <Form.Error className="text-destructive p-4 rounded-lg mb-4" />
|
|
53
|
+
* <Form.Submitted className="text-green-500 p-4 rounded-lg mb-4" />
|
|
54
|
+
* </Form.Root>
|
|
55
|
+
* );
|
|
56
|
+
* }
|
|
57
|
+
*
|
|
58
|
+
* // Pattern 2: Lazy loading with formId (Client-side)
|
|
59
|
+
* function DynamicFormPage({ formId }) {
|
|
60
|
+
* return (
|
|
61
|
+
* <Form.Root formServiceConfig={{ formId }}>
|
|
62
|
+
* <Form.Loading className="flex justify-center p-4" />
|
|
63
|
+
* <Form.LoadingError className="text-destructive px-4 py-3 rounded mb-4" />
|
|
64
|
+
* <Form.Fields fieldMap={FIELD_MAP} />
|
|
65
|
+
* <Form.Error className="text-destructive p-4 rounded-lg mb-4" />
|
|
66
|
+
* <Form.Submitted className="text-green-500 p-4 rounded-lg mb-4" />
|
|
67
|
+
* </Form.Root>
|
|
68
|
+
* );
|
|
69
|
+
* }
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export const Root = React.forwardRef((props, ref) => {
|
|
73
|
+
const { children, formServiceConfig, asChild, ...otherProps } = props;
|
|
74
|
+
return (_jsx(CoreRoot, { formServiceConfig: formServiceConfig, children: _jsx(RootContent, { asChild: asChild, ref: ref, ...otherProps, children: children }) }));
|
|
75
|
+
});
|
|
76
|
+
/**
|
|
77
|
+
* Internal component to handle the Root content with service access.
|
|
78
|
+
* This component wraps the children with the necessary div container and applies styling.
|
|
79
|
+
*
|
|
80
|
+
* @internal
|
|
81
|
+
* @param {RootContentProps} props - Component props
|
|
82
|
+
* @param {React.ReactNode} props.children - Child components to render
|
|
83
|
+
* @param {string} [props.className] - CSS classes to apply to the container
|
|
84
|
+
* @param {boolean} [props.asChild] - Whether to render as a child component
|
|
85
|
+
* @returns {JSX.Element} The wrapped content
|
|
86
|
+
*/
|
|
87
|
+
const RootContent = React.forwardRef((props, ref) => {
|
|
88
|
+
const { asChild, children, className, ...otherProps } = props;
|
|
89
|
+
return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.formRoot, customElement: children, customElementProps: {}, ...otherProps, children: _jsx("div", { children: children }) }));
|
|
90
|
+
});
|
|
91
|
+
/**
|
|
92
|
+
* Component that renders content during loading state.
|
|
93
|
+
* Only displays its children when the form is currently loading.
|
|
94
|
+
*
|
|
95
|
+
* @component
|
|
96
|
+
* @param {LoadingProps} props - The component props
|
|
97
|
+
* @param {boolean} [props.asChild] - Whether to render as a child component
|
|
98
|
+
* @param {React.ReactNode} [props.children] - Content to display during loading state
|
|
99
|
+
* @param {string} [props.className] - CSS classes to apply to the default element
|
|
100
|
+
* @example
|
|
101
|
+
* ```tsx
|
|
102
|
+
* import { Form } from '@wix/headless-forms/react';
|
|
103
|
+
*
|
|
104
|
+
* // Default usage with className
|
|
105
|
+
* function FormLoading() {
|
|
106
|
+
* return (
|
|
107
|
+
* <Form.Loading className="flex justify-center p-4" />
|
|
108
|
+
* );
|
|
109
|
+
* }
|
|
110
|
+
*
|
|
111
|
+
* // Custom content
|
|
112
|
+
* function CustomFormLoading() {
|
|
113
|
+
* return (
|
|
114
|
+
* <Form.Loading>
|
|
115
|
+
* <div className="flex justify-center items-center p-4">
|
|
116
|
+
* <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
|
117
|
+
* <span className="ml-2 text-foreground font-paragraph">Loading form...</span>
|
|
118
|
+
* </div>
|
|
119
|
+
* </Form.Loading>
|
|
120
|
+
* );
|
|
121
|
+
* }
|
|
122
|
+
*
|
|
123
|
+
* // With asChild for custom components
|
|
124
|
+
* function CustomFormLoadingAsChild() {
|
|
125
|
+
* return (
|
|
126
|
+
* <Form.Loading asChild>
|
|
127
|
+
* <div className="custom-loading-container">
|
|
128
|
+
* <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
|
129
|
+
* <span className="ml-2 text-foreground font-paragraph">Loading form...</span>
|
|
130
|
+
* </div>
|
|
131
|
+
* </Form.Loading>
|
|
132
|
+
* );
|
|
133
|
+
* }
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export const Loading = React.forwardRef((props, ref) => {
|
|
137
|
+
const { asChild, children, className, ...otherProps } = props;
|
|
138
|
+
return (_jsx(CoreLoading, { children: ({ isLoading }) => {
|
|
139
|
+
if (!isLoading)
|
|
140
|
+
return null;
|
|
141
|
+
return (_jsx(AsChildSlot, { "data-testid": TestIds.formLoading, ref: ref, asChild: asChild, className: className, customElement: children, content: "Loading form...", ...otherProps, children: _jsx("div", { children: "Loading form..." }) }));
|
|
142
|
+
} }));
|
|
143
|
+
});
|
|
144
|
+
/**
|
|
145
|
+
* Component that renders content when there's an error loading the form.
|
|
146
|
+
* Only displays its children when an error has occurred.
|
|
147
|
+
*
|
|
148
|
+
* @component
|
|
149
|
+
* @param {LoadingErrorProps} props - The component props
|
|
150
|
+
* @param {boolean} [props.asChild] - Whether to render as a child component
|
|
151
|
+
* @param {React.ReactNode} [props.children] - Content to display during error state
|
|
152
|
+
* @param {string} [props.className] - CSS classes to apply to the default element
|
|
153
|
+
* @example
|
|
154
|
+
* ```tsx
|
|
155
|
+
* import { Form } from '@wix/headless-forms/react';
|
|
156
|
+
*
|
|
157
|
+
* // Default usage with className
|
|
158
|
+
* function FormLoadingError() {
|
|
159
|
+
* return (
|
|
160
|
+
* <Form.LoadingError className="text-destructive px-4 py-3 rounded mb-4" />
|
|
161
|
+
* );
|
|
162
|
+
* }
|
|
163
|
+
*
|
|
164
|
+
* // Custom content
|
|
165
|
+
* function CustomLoadingError() {
|
|
166
|
+
* return (
|
|
167
|
+
* <Form.LoadingError>
|
|
168
|
+
* <div className="bg-destructive/10 border border-destructive text-destructive px-4 py-3 rounded mb-4">
|
|
169
|
+
* <h3 className="font-heading text-lg">Error loading form</h3>
|
|
170
|
+
* <p className="font-paragraph">Something went wrong. Please try again.</p>
|
|
171
|
+
* </div>
|
|
172
|
+
* </Form.LoadingError>
|
|
173
|
+
* );
|
|
174
|
+
* }
|
|
175
|
+
*
|
|
176
|
+
* // With asChild for custom components
|
|
177
|
+
* function CustomLoadingErrorAsChild() {
|
|
178
|
+
* return (
|
|
179
|
+
* <Form.LoadingError asChild>
|
|
180
|
+
* {React.forwardRef<HTMLDivElement, { error: string | null; hasError: boolean }>(
|
|
181
|
+
* ({ error }, ref) => (
|
|
182
|
+
* <div ref={ref} className="custom-error-container">
|
|
183
|
+
* <h3 className="font-heading">Error Loading Form</h3>
|
|
184
|
+
* <p className="font-paragraph">{error}</p>
|
|
185
|
+
* </div>
|
|
186
|
+
* )
|
|
187
|
+
* )}
|
|
188
|
+
* </Form.LoadingError>
|
|
189
|
+
* );
|
|
190
|
+
* }
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
export const LoadingError = React.forwardRef((props, ref) => {
|
|
194
|
+
const { asChild, children, className, ...otherProps } = props;
|
|
195
|
+
return (_jsx(CoreLoadingError, { children: ({ error, hasError }) => {
|
|
196
|
+
if (!hasError)
|
|
197
|
+
return null;
|
|
198
|
+
const errorData = { error, hasError };
|
|
199
|
+
return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.formLoadingError, customElement: children, customElementProps: errorData, content: error, ...otherProps, children: _jsx("div", { children: error }) }));
|
|
200
|
+
} }));
|
|
201
|
+
});
|
|
202
|
+
/**
|
|
203
|
+
* Component that renders content when there's an error during form submission.
|
|
204
|
+
* Only displays its children when a submission error has occurred.
|
|
205
|
+
*
|
|
206
|
+
* @component
|
|
207
|
+
* @param {ErrorProps} props - The component props
|
|
208
|
+
* @param {boolean} [props.asChild] - Whether to render as a child component
|
|
209
|
+
* @param {React.ReactNode} [props.children] - Content to display during submit error state
|
|
210
|
+
* @param {string} [props.className] - CSS classes to apply to the default element
|
|
211
|
+
* @example
|
|
212
|
+
* ```tsx
|
|
213
|
+
* import { Form } from '@wix/headless-forms/react';
|
|
214
|
+
*
|
|
215
|
+
* // Default usage with className
|
|
216
|
+
* function FormError() {
|
|
217
|
+
* return <Form.Error className="text-destructive p-4 rounded-lg mb-4" />;
|
|
218
|
+
* }
|
|
219
|
+
*
|
|
220
|
+
* // Custom content
|
|
221
|
+
* function CustomFormError() {
|
|
222
|
+
* return (
|
|
223
|
+
* <Form.Error>
|
|
224
|
+
* <div className="bg-destructive/10 border border-destructive text-destructive p-4 rounded-lg mb-4">
|
|
225
|
+
* <h3 className="font-heading text-lg">Submission Failed</h3>
|
|
226
|
+
* <p className="font-paragraph">Please check your input and try again.</p>
|
|
227
|
+
* </div>
|
|
228
|
+
* </Form.Error>
|
|
229
|
+
* );
|
|
230
|
+
* }
|
|
231
|
+
*
|
|
232
|
+
* // With asChild for custom components
|
|
233
|
+
* function CustomFormErrorAsChild() {
|
|
234
|
+
* return (
|
|
235
|
+
* <Form.Error asChild>
|
|
236
|
+
* {React.forwardRef<HTMLDivElement, { error: string | null; hasError: boolean }>(
|
|
237
|
+
* ({ error }, ref) => (
|
|
238
|
+
* <div ref={ref} className="custom-error-container">
|
|
239
|
+
* <h3 className="font-heading">Submission Failed</h3>
|
|
240
|
+
* <p className="font-paragraph">{error}</p>
|
|
241
|
+
* </div>
|
|
242
|
+
* )
|
|
243
|
+
* )}
|
|
244
|
+
* </Form.Error>
|
|
245
|
+
* );
|
|
246
|
+
* }
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
export const Error = React.forwardRef((props, ref) => {
|
|
250
|
+
const { asChild, children, className, ...otherProps } = props;
|
|
251
|
+
return (_jsx(CoreError, { children: ({ error, hasError }) => {
|
|
252
|
+
if (!hasError)
|
|
253
|
+
return null;
|
|
254
|
+
const errorData = { error, hasError };
|
|
255
|
+
return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.formError, customElement: children, customElementProps: errorData, content: error, ...otherProps, children: _jsx("div", { className: "text-destructive text-sm sm:text-base", children: error }) }));
|
|
256
|
+
} }));
|
|
257
|
+
});
|
|
258
|
+
/**
|
|
259
|
+
* Component that renders content after successful form submission.
|
|
260
|
+
* Only displays its children when the form has been successfully submitted.
|
|
261
|
+
*
|
|
262
|
+
* @component
|
|
263
|
+
* @param {SubmittedProps} props - The component props
|
|
264
|
+
* @param {boolean} [props.asChild] - Whether to render as a child component
|
|
265
|
+
* @param {React.ReactNode} [props.children] - Content to display after successful submission
|
|
266
|
+
* @param {string} [props.className] - CSS classes to apply to the default element
|
|
267
|
+
* @example
|
|
268
|
+
* ```tsx
|
|
269
|
+
* import { Form } from '@wix/headless-forms/react';
|
|
270
|
+
*
|
|
271
|
+
* // Default usage with className
|
|
272
|
+
* function FormSubmitted() {
|
|
273
|
+
* return <Form.Submitted className="text-green-500 p-4 rounded-lg mb-4" />;
|
|
274
|
+
* }
|
|
275
|
+
*
|
|
276
|
+
* // Custom content
|
|
277
|
+
* function CustomFormSubmitted() {
|
|
278
|
+
* return (
|
|
279
|
+
* <Form.Submitted>
|
|
280
|
+
* <div className="bg-green-50 border border-green-200 text-green-800 p-6 rounded-lg mb-4">
|
|
281
|
+
* <h2 className="font-heading text-xl mb-2">Thank You!</h2>
|
|
282
|
+
* <p className="font-paragraph">Your form has been submitted successfully.</p>
|
|
283
|
+
* </div>
|
|
284
|
+
* </Form.Submitted>
|
|
285
|
+
* );
|
|
286
|
+
* }
|
|
287
|
+
*
|
|
288
|
+
* // With asChild for custom components
|
|
289
|
+
* function CustomFormSubmittedAsChild() {
|
|
290
|
+
* return (
|
|
291
|
+
* <Form.Submitted asChild>
|
|
292
|
+
* {React.forwardRef<HTMLDivElement, { isSubmitted: boolean; message: string }>(
|
|
293
|
+
* ({ message }, ref) => (
|
|
294
|
+
* <div ref={ref} className="custom-success-container">
|
|
295
|
+
* <h2 className="font-heading">Thank You!</h2>
|
|
296
|
+
* <p className="font-paragraph">{message}</p>
|
|
297
|
+
* </div>
|
|
298
|
+
* )
|
|
299
|
+
* )}
|
|
300
|
+
* </Form.Submitted>
|
|
301
|
+
* );
|
|
302
|
+
* }
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
export const Submitted = React.forwardRef((props, ref) => {
|
|
306
|
+
const { asChild, children, className, ...otherProps } = props;
|
|
307
|
+
return (_jsx(CoreSubmitted, { children: ({ isSubmitted, message }) => {
|
|
308
|
+
if (!isSubmitted)
|
|
309
|
+
return null;
|
|
310
|
+
const submittedData = { isSubmitted, message };
|
|
311
|
+
return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.formSubmitted, customElement: children, customElementProps: submittedData, content: message, ...otherProps, children: _jsx("div", { className: "text-green-500 text-sm sm:text-base", children: message }) }));
|
|
312
|
+
} }));
|
|
313
|
+
});
|
|
314
|
+
/**
|
|
315
|
+
* Fields component for rendering a form with custom field renderers.
|
|
316
|
+
* This component handles the rendering of form fields based on the provided fieldMap.
|
|
317
|
+
* Must be used within Form.Root to access form context.
|
|
318
|
+
*
|
|
319
|
+
* @component
|
|
320
|
+
* @param {FieldsProps} props - Component props
|
|
321
|
+
* @param {FieldMap} props.fieldMap - A mapping of field types to their corresponding React components
|
|
322
|
+
* @param {string} props.rowGapClassname - CSS class name for gap between rows
|
|
323
|
+
* @param {string} props.columnGapClassname - CSS class name for gap between columns
|
|
324
|
+
* @example
|
|
325
|
+
* ```tsx
|
|
326
|
+
* import { Form } from '@wix/headless-forms/react';
|
|
327
|
+
* import { TextInput, TextArea, Checkbox } from './field-components';
|
|
328
|
+
*
|
|
329
|
+
* const FIELD_MAP = {
|
|
330
|
+
* TEXT_INPUT: TextInput,
|
|
331
|
+
* TEXT_AREA: TextArea,
|
|
332
|
+
* CHECKBOX: Checkbox,
|
|
333
|
+
* NUMBER_INPUT: NumberInput,
|
|
334
|
+
* // ... remaining field components
|
|
335
|
+
* };
|
|
336
|
+
*
|
|
337
|
+
* function ContactForm({ formServiceConfig }) {
|
|
338
|
+
* return (
|
|
339
|
+
* <Form.Root formServiceConfig={formServiceConfig}>
|
|
340
|
+
* <Form.Loading className="flex justify-center p-4" />
|
|
341
|
+
* <Form.LoadingError className="text-destructive px-4 py-3 rounded mb-4" />
|
|
342
|
+
* <Form.Fields
|
|
343
|
+
* fieldMap={FIELD_MAP}
|
|
344
|
+
* rowGapClassname="gap-y-4"
|
|
345
|
+
* columnGapClassname="gap-x-2"
|
|
346
|
+
* />
|
|
347
|
+
* </Form.Root>
|
|
348
|
+
* );
|
|
349
|
+
* }
|
|
350
|
+
* ```
|
|
351
|
+
*/
|
|
352
|
+
/**
|
|
353
|
+
* Fields component for rendering a form with custom field renderers.
|
|
354
|
+
* It maps each field type from the form configuration to its corresponding React component
|
|
355
|
+
* and renders them in the order and layout defined by the form structure.
|
|
356
|
+
*
|
|
357
|
+
* The component automatically handles:
|
|
358
|
+
* - Field validation and error display
|
|
359
|
+
* - Form state management
|
|
360
|
+
* - Field value updates
|
|
361
|
+
* - Grid layout with configurable row and column gaps
|
|
362
|
+
*
|
|
363
|
+
* Must be used within Form.Root to access form context.
|
|
364
|
+
*
|
|
365
|
+
* @component
|
|
366
|
+
* @param {FieldsProps} props - The component props
|
|
367
|
+
* @param {FieldMap} props.fieldMap - A mapping of field types to their corresponding React components. Each key represents a field type (e.g., 'TEXT_INPUT', 'CHECKBOX') and the value is the React component that should render that field type.
|
|
368
|
+
* @param {string} props.rowGapClassname - CSS class name for gap between form rows
|
|
369
|
+
* @param {string} props.columnGapClassname - CSS class name for gap between form columns
|
|
370
|
+
*
|
|
371
|
+
* @example
|
|
372
|
+
* ```tsx
|
|
373
|
+
* import { Form } from '@wix/headless-forms/react';
|
|
374
|
+
* import { loadFormServiceConfig } from '@wix/headless-forms/services';
|
|
375
|
+
* import {
|
|
376
|
+
* TextInput,
|
|
377
|
+
* TextArea,
|
|
378
|
+
* PhoneInput,
|
|
379
|
+
* MultilineAddress,
|
|
380
|
+
* DateInput,
|
|
381
|
+
* DatePicker,
|
|
382
|
+
* DateTimeInput,
|
|
383
|
+
* FileUpload,
|
|
384
|
+
* NumberInput,
|
|
385
|
+
* Checkbox,
|
|
386
|
+
* Signature,
|
|
387
|
+
* RatingInput,
|
|
388
|
+
* RadioGroup,
|
|
389
|
+
* CheckboxGroup,
|
|
390
|
+
* Dropdown,
|
|
391
|
+
* Tags,
|
|
392
|
+
* TimeInput,
|
|
393
|
+
* RichText,
|
|
394
|
+
* SubmitButton,
|
|
395
|
+
* ProductList,
|
|
396
|
+
* FixedPayment,
|
|
397
|
+
* PaymentInput,
|
|
398
|
+
* Donation,
|
|
399
|
+
* Appointment,
|
|
400
|
+
* ImageChoice
|
|
401
|
+
* } from './components';
|
|
402
|
+
*
|
|
403
|
+
* // Define your field mapping - this tells the Fields component which React component to use for each field type
|
|
404
|
+
* const FIELD_MAP = {
|
|
405
|
+
* TEXT_INPUT: TextInput,
|
|
406
|
+
* TEXT_AREA: TextArea,
|
|
407
|
+
* PHONE_INPUT: PhoneInput,
|
|
408
|
+
* MULTILINE_ADDRESS: MultilineAddress,
|
|
409
|
+
* DATE_INPUT: DateInput,
|
|
410
|
+
* DATE_PICKER: DatePicker,
|
|
411
|
+
* DATE_TIME_INPUT: DateTimeInput,
|
|
412
|
+
* FILE_UPLOAD: FileUpload,
|
|
413
|
+
* NUMBER_INPUT: NumberInput,
|
|
414
|
+
* CHECKBOX: Checkbox,
|
|
415
|
+
* SIGNATURE: Signature,
|
|
416
|
+
* RATING_INPUT: RatingInput,
|
|
417
|
+
* RADIO_GROUP: RadioGroup,
|
|
418
|
+
* CHECKBOX_GROUP: CheckboxGroup,
|
|
419
|
+
* DROPDOWN: Dropdown,
|
|
420
|
+
* TAGS: Tags,
|
|
421
|
+
* TIME_INPUT: TimeInput,
|
|
422
|
+
* TEXT: RichText,
|
|
423
|
+
* SUBMIT_BUTTON: SubmitButton,
|
|
424
|
+
* PRODUCT_LIST: ProductList,
|
|
425
|
+
* FIXED_PAYMENT: FixedPayment,
|
|
426
|
+
* PAYMENT_INPUT: PaymentInput,
|
|
427
|
+
* DONATION: Donation,
|
|
428
|
+
* APPOINTMENT: Appointment,
|
|
429
|
+
* IMAGE_CHOICE: ImageChoice,
|
|
430
|
+
* };
|
|
431
|
+
*
|
|
432
|
+
* function ContactForm({ formServiceConfig }) {
|
|
433
|
+
* return (
|
|
434
|
+
* <Form.Root formServiceConfig={formServiceConfig}>
|
|
435
|
+
* <Form.Loading className="flex justify-center p-4" />
|
|
436
|
+
* <Form.LoadingError className="text-destructive px-4 py-3 rounded mb-4" />
|
|
437
|
+
* <Form.Fields
|
|
438
|
+
* fieldMap={FIELD_MAP}
|
|
439
|
+
* rowGapClassname="gap-y-4"
|
|
440
|
+
* columnGapClassname="gap-x-2"
|
|
441
|
+
* />
|
|
442
|
+
* <Form.Error className="text-destructive p-4 rounded-lg mb-4" />
|
|
443
|
+
* <Form.Submitted className="text-green-500 p-4 rounded-lg mb-4" />
|
|
444
|
+
* </Form.Root>
|
|
445
|
+
* );
|
|
446
|
+
* }
|
|
447
|
+
* ```
|
|
448
|
+
*
|
|
449
|
+
* @example
|
|
450
|
+
* ```tsx
|
|
451
|
+
* // Creating custom field components - ALL field components MUST use Form.Field
|
|
452
|
+
* // This example shows the REQUIRED structure for a TEXT_INPUT component
|
|
453
|
+
* import { Form, type TextInputProps } from '@wix/headless-forms/react';
|
|
454
|
+
*
|
|
455
|
+
* const TextInput = (props: TextInputProps) => {
|
|
456
|
+
* const { id, value, onChange, label, error, required, ...inputProps } = props;
|
|
457
|
+
*
|
|
458
|
+
* // Form.Field provides automatic grid layout positioning
|
|
459
|
+
* return (
|
|
460
|
+
* <Form.Field id={id}>
|
|
461
|
+
* <Form.Field.Label>
|
|
462
|
+
* <label className="text-foreground font-paragraph">
|
|
463
|
+
* {label}
|
|
464
|
+
* {required && <span className="text-destructive ml-1">*</span>}
|
|
465
|
+
* </label>
|
|
466
|
+
* </Form.Field.Label>
|
|
467
|
+
* <Form.Field.Input
|
|
468
|
+
* description={error && <span className="text-destructive text-sm">{error}</span>}
|
|
469
|
+
* >
|
|
470
|
+
* <input
|
|
471
|
+
* type="text"
|
|
472
|
+
* value={value || ''}
|
|
473
|
+
* onChange={(e) => onChange(e.target.value)}
|
|
474
|
+
* className="bg-background border-foreground text-foreground"
|
|
475
|
+
* aria-invalid={!!error}
|
|
476
|
+
* {...inputProps}
|
|
477
|
+
* />
|
|
478
|
+
* </Form.Field.Input>
|
|
479
|
+
* </Form.Field>
|
|
480
|
+
* );
|
|
481
|
+
* };
|
|
482
|
+
*
|
|
483
|
+
* const FIELD_MAP = {
|
|
484
|
+
* TEXT_INPUT: TextInput,
|
|
485
|
+
* // ... all other field components must also use Form.Field
|
|
486
|
+
* };
|
|
487
|
+
* ```
|
|
488
|
+
*/
|
|
489
|
+
export const Fields = React.forwardRef((props, ref) => {
|
|
490
|
+
const [formValues, setFormValues] = useState({});
|
|
491
|
+
const [formErrors, setFormErrors] = useState([]);
|
|
492
|
+
const handleFormChange = useCallback((values) => {
|
|
493
|
+
setFormValues(values);
|
|
494
|
+
}, []);
|
|
495
|
+
const handleFormValidate = useCallback((errors) => {
|
|
496
|
+
setFormErrors(errors);
|
|
497
|
+
}, []);
|
|
498
|
+
return (_jsx(CoreFields, { children: ({ form, submitForm }) => {
|
|
499
|
+
if (!form)
|
|
500
|
+
return null;
|
|
501
|
+
return (_jsx("div", { ref: ref, children: _jsx(FormProvider, { currency: 'USD', locale: 'en', children: _jsx(FieldsWithForm, { form: form, values: formValues, onChange: handleFormChange, errors: formErrors, onValidate: handleFormValidate, fields: props.fieldMap, submitForm: () => submitForm(formValues), rowGapClassname: props.rowGapClassname, columnGapClassname: props.columnGapClassname }) }) }));
|
|
502
|
+
} }));
|
|
503
|
+
});
|
|
504
|
+
const FieldsWithForm = ({ form, submitForm, values, onChange, errors, onValidate, fields: fieldMap, rowGapClassname, columnGapClassname, }) => {
|
|
505
|
+
const formData = useForm({
|
|
506
|
+
form,
|
|
507
|
+
values,
|
|
508
|
+
errors,
|
|
509
|
+
onChange,
|
|
510
|
+
onValidate,
|
|
511
|
+
submitForm,
|
|
512
|
+
fieldMap,
|
|
513
|
+
});
|
|
514
|
+
if (!formData)
|
|
515
|
+
return null;
|
|
516
|
+
const { columnCount, fieldElements, fieldsLayout } = formData;
|
|
517
|
+
return (
|
|
518
|
+
// TODO: use readOnly, isDisabled
|
|
519
|
+
// TODO: step title a11y support
|
|
520
|
+
// TODO: mobile support?
|
|
521
|
+
_jsx(FieldLayoutProvider, { value: fieldsLayout, children: _jsx("form", { onSubmit: (e) => e.preventDefault(), children: _jsx("fieldset", { style: { display: 'flex', flexDirection: 'column' }, className: rowGapClassname, children: fieldElements.map((rowElements, index) => {
|
|
522
|
+
return (_jsx("div", { style: {
|
|
523
|
+
display: 'grid',
|
|
524
|
+
width: '100%',
|
|
525
|
+
gridTemplateColumns: `repeat(${columnCount}, 1fr)`,
|
|
526
|
+
gridAutoRows: 'minmax(min-content, max-content)',
|
|
527
|
+
}, className: columnGapClassname, children: rowElements }, index));
|
|
528
|
+
}) }) }) }));
|
|
529
|
+
};
|
|
530
|
+
/**
|
|
531
|
+
* Container component for a form field with grid layout support.
|
|
532
|
+
* Provides context to Field.Label, Field.InputWrapper, Field.Input, and Field.Error child components.
|
|
533
|
+
* Based on the default-field-layout functionality.
|
|
534
|
+
*
|
|
535
|
+
* @component
|
|
536
|
+
* @example
|
|
537
|
+
* ```tsx
|
|
538
|
+
* import { Form } from '@wix/headless-forms/react';
|
|
539
|
+
*
|
|
540
|
+
* function FormFields() {
|
|
541
|
+
* return (
|
|
542
|
+
* <Form.Field id="username">
|
|
543
|
+
* <Form.Field.Label>
|
|
544
|
+
* <label className="text-foreground font-paragraph">Username</label>
|
|
545
|
+
* </Form.Field.Label>
|
|
546
|
+
* <Form.Field.InputWrapper>
|
|
547
|
+
* <Form.Field.Input description={<span className="text-secondary-foreground">Required</span>}>
|
|
548
|
+
* <input className="bg-background border-foreground text-foreground" />
|
|
549
|
+
* </Form.Field.Input>
|
|
550
|
+
* <Form.Field.Error>
|
|
551
|
+
* <span className="text-destructive text-sm font-paragraph">Username is required</span>
|
|
552
|
+
* </Form.Field.Error>
|
|
553
|
+
* </Form.Field.InputWrapper>
|
|
554
|
+
* </Form.Field>
|
|
555
|
+
* );
|
|
556
|
+
* }
|
|
557
|
+
* ```
|
|
558
|
+
*/
|
|
559
|
+
const FieldRoot = React.forwardRef((props, ref) => {
|
|
560
|
+
const { id, children, asChild, className, ...otherProps } = props;
|
|
561
|
+
const layout = useFieldLayout(id);
|
|
562
|
+
if (!layout) {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
return (_jsx(CoreField, { id: id, layout: layout, children: (fieldData) => {
|
|
566
|
+
const contextValue = {
|
|
567
|
+
id,
|
|
568
|
+
layout: fieldData.layout,
|
|
569
|
+
gridStyles: fieldData.gridStyles,
|
|
570
|
+
};
|
|
571
|
+
return (_jsx(FieldContext.Provider, { value: contextValue, children: _jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.fieldRoot, customElement: children, customElementProps: {}, ...otherProps, children: children }) }));
|
|
572
|
+
} }));
|
|
573
|
+
});
|
|
574
|
+
FieldRoot.displayName = 'Form.Field';
|
|
575
|
+
/**
|
|
576
|
+
* Label component for a form field with automatic grid positioning.
|
|
577
|
+
* Must be used within a Form.Field component.
|
|
578
|
+
* Renders in the label row of the field's grid layout.
|
|
579
|
+
*
|
|
580
|
+
* @component
|
|
581
|
+
* @example
|
|
582
|
+
* ```tsx
|
|
583
|
+
* import { Form } from '@wix/headless-forms/react';
|
|
584
|
+
*
|
|
585
|
+
* <Form.Field id="email">
|
|
586
|
+
* <Form.Field.Label>
|
|
587
|
+
* <label className="text-foreground font-paragraph">
|
|
588
|
+
* Email Address
|
|
589
|
+
* <Form.Field.Label.Required required={true} />
|
|
590
|
+
* </label>
|
|
591
|
+
* </Form.Field.Label>
|
|
592
|
+
* <Form.Field.InputWrapper>
|
|
593
|
+
* <Form.Field.Input>
|
|
594
|
+
* <input type="email" className="bg-background border-foreground text-foreground" />
|
|
595
|
+
* </Form.Field.Input>
|
|
596
|
+
* </Form.Field.InputWrapper>
|
|
597
|
+
* </Form.Field>
|
|
598
|
+
* ```
|
|
599
|
+
*/
|
|
600
|
+
const FieldLabelRoot = React.forwardRef((props, ref) => {
|
|
601
|
+
const { children, asChild, className, ...otherProps } = props;
|
|
602
|
+
const { gridStyles } = useFieldContext();
|
|
603
|
+
return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, style: gridStyles.label, "data-testid": TestIds.fieldLabel, customElement: children, customElementProps: {}, ...otherProps, children: _jsx("div", { children: children }) }));
|
|
604
|
+
});
|
|
605
|
+
FieldLabelRoot.displayName = 'Form.Field.Label';
|
|
606
|
+
/**
|
|
607
|
+
* Required indicator component for form field labels.
|
|
608
|
+
* Must be used within a Form.Field.Label component.
|
|
609
|
+
*
|
|
610
|
+
* @component
|
|
611
|
+
* @example
|
|
612
|
+
* ```tsx
|
|
613
|
+
* import { Form } from '@wix/headless-forms/react';
|
|
614
|
+
*
|
|
615
|
+
* // Basic usage with required prop
|
|
616
|
+
* <Form.Field.Label>
|
|
617
|
+
* <label className="text-foreground font-paragraph">
|
|
618
|
+
* Email Address
|
|
619
|
+
* <Form.Field.Label.Required />
|
|
620
|
+
* </label>
|
|
621
|
+
* </Form.Field.Label>
|
|
622
|
+
*
|
|
623
|
+
* // Custom styling
|
|
624
|
+
* <Form.Field.Label>
|
|
625
|
+
* <label className="text-foreground font-paragraph">
|
|
626
|
+
* Username
|
|
627
|
+
* <Form.Field.Label.Required required={true} className="text-destructive ml-2" />
|
|
628
|
+
* </label>
|
|
629
|
+
* </Form.Field.Label>
|
|
630
|
+
*/
|
|
631
|
+
export const FieldLabelRequired = React.forwardRef((props, ref) => {
|
|
632
|
+
const { required = false, children, asChild, className, ...otherProps } = props;
|
|
633
|
+
const requiredIndicator = 'asterisk';
|
|
634
|
+
// @ts-expect-error
|
|
635
|
+
if (!required || requiredIndicator === 'none')
|
|
636
|
+
return null;
|
|
637
|
+
return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, customElement: children, ...otherProps, children: _jsx("span", { children: requiredIndicator === 'asterisk'
|
|
638
|
+
? '*'
|
|
639
|
+
: requiredIndicator === 'text'
|
|
640
|
+
? '(Required)'
|
|
641
|
+
: null }) }));
|
|
642
|
+
});
|
|
643
|
+
FieldLabelRequired.displayName = 'Form.Field.Label.Required';
|
|
644
|
+
export const FieldLabel = FieldLabelRoot;
|
|
645
|
+
FieldLabel.Required = FieldLabelRequired;
|
|
646
|
+
/**
|
|
647
|
+
* InputWrapper component that wraps input and error elements with grid positioning.
|
|
648
|
+
* Must be used within a Form.Field component.
|
|
649
|
+
* This wrapper applies the grid positioning styles to contain both the input and error.
|
|
650
|
+
*
|
|
651
|
+
* @component
|
|
652
|
+
* @example
|
|
653
|
+
* ```tsx
|
|
654
|
+
* import { Form } from '@wix/headless-forms/react';
|
|
655
|
+
*
|
|
656
|
+
* <Form.Field id="email">
|
|
657
|
+
* <Form.Field.Label>
|
|
658
|
+
* <label className="text-foreground font-paragraph">Email Address</label>
|
|
659
|
+
* </Form.Field.Label>
|
|
660
|
+
* <Form.Field.InputWrapper>
|
|
661
|
+
* <Form.Field.Input>
|
|
662
|
+
* <input type="email" className="bg-background border-foreground text-foreground" />
|
|
663
|
+
* </Form.Field.Input>
|
|
664
|
+
* <Form.Field.Error>
|
|
665
|
+
* <span className="text-destructive text-sm font-paragraph">Please enter a valid email</span>
|
|
666
|
+
* </Form.Field.Error>
|
|
667
|
+
* </Form.Field.InputWrapper>
|
|
668
|
+
* </Form.Field>
|
|
669
|
+
* ```
|
|
670
|
+
*/
|
|
671
|
+
export const FieldInputWrapper = React.forwardRef((props, ref) => {
|
|
672
|
+
const { children, asChild, className, ...otherProps } = props;
|
|
673
|
+
const { gridStyles } = useFieldContext();
|
|
674
|
+
return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, style: gridStyles.input, "data-testid": TestIds.fieldInputWrapper, customElement: children, customElementProps: {}, ...otherProps, children: _jsx("div", { children: children }) }));
|
|
675
|
+
});
|
|
676
|
+
FieldInputWrapper.displayName = 'Form.Field.InputWrapper';
|
|
677
|
+
/**
|
|
678
|
+
* Input component for a form field.
|
|
679
|
+
* Must be used within a Form.Field.InputWrapper component.
|
|
680
|
+
* Renders the actual input element without grid positioning.
|
|
681
|
+
*
|
|
682
|
+
* @component
|
|
683
|
+
* @example
|
|
684
|
+
* ```tsx
|
|
685
|
+
* import { Form } from '@wix/headless-forms/react';
|
|
686
|
+
*
|
|
687
|
+
* <Form.Field id="password">
|
|
688
|
+
* <Form.Field.Label>
|
|
689
|
+
* <label className="text-foreground font-paragraph">Password</label>
|
|
690
|
+
* </Form.Field.Label>
|
|
691
|
+
* <Form.Field.InputWrapper>
|
|
692
|
+
* <Form.Field.Input description={<span className="text-secondary-foreground">Min 8 characters</span>}>
|
|
693
|
+
* <input type="password" className="bg-background border-foreground text-foreground" />
|
|
694
|
+
* </Form.Field.Input>
|
|
695
|
+
* </Form.Field.InputWrapper>
|
|
696
|
+
* </Form.Field>
|
|
697
|
+
* ```
|
|
698
|
+
*/
|
|
699
|
+
export const FieldInput = React.forwardRef((props, ref) => {
|
|
700
|
+
const { children, description, asChild, className, ...otherProps } = props;
|
|
701
|
+
return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.fieldInput, customElement: children, customElementProps: {}, ...otherProps, children: _jsx("div", { children: children }) }));
|
|
702
|
+
});
|
|
703
|
+
FieldInput.displayName = 'Form.Field.Input';
|
|
704
|
+
/**
|
|
705
|
+
* Error component for displaying field-level validation errors.
|
|
706
|
+
* Must be used within a Form.Field.InputWrapper component.
|
|
707
|
+
* Only renders when there is an error for the current field.
|
|
708
|
+
*
|
|
709
|
+
* @component
|
|
710
|
+
* @example
|
|
711
|
+
* ```tsx
|
|
712
|
+
* import { Form } from '@wix/headless-forms/react';
|
|
713
|
+
*
|
|
714
|
+
* <Form.Field id="email">
|
|
715
|
+
* <Form.Field.Label>
|
|
716
|
+
* <label className="text-foreground font-paragraph">Email Address</label>
|
|
717
|
+
* </Form.Field.Label>
|
|
718
|
+
* <Form.Field.InputWrapper>
|
|
719
|
+
* <Form.Field.Input>
|
|
720
|
+
* <input type="email" className="bg-background border-foreground text-foreground" />
|
|
721
|
+
* </Form.Field.Input>
|
|
722
|
+
* <Form.Field.Error path="email">
|
|
723
|
+
* <span className="text-destructive text-sm font-paragraph">Please enter a valid email address</span>
|
|
724
|
+
* </Form.Field.Error>
|
|
725
|
+
* </Form.Field.InputWrapper>
|
|
726
|
+
* </Form.Field>
|
|
727
|
+
* ```
|
|
728
|
+
*/
|
|
729
|
+
export const FieldError = React.forwardRef((props, ref) => {
|
|
730
|
+
const { errorMessage, asChild, className, children, ...otherProps } = props;
|
|
731
|
+
if (!errorMessage && !children)
|
|
732
|
+
return null;
|
|
733
|
+
return (_jsx(AsChildSlot, { "data-testid": TestIds.fieldError, ref: ref, asChild: asChild, className: className, ...otherProps, children: children || errorMessage }));
|
|
734
|
+
});
|
|
735
|
+
FieldError.displayName = 'Form.Field.Error';
|
|
736
|
+
export const Field = FieldRoot;
|
|
737
|
+
Field.Label = FieldLabel;
|
|
738
|
+
Field.InputWrapper = FieldInputWrapper;
|
|
739
|
+
Field.Input = FieldInput;
|
|
740
|
+
Field.Error = FieldError;
|