@wix/headless-forms 0.0.9 → 0.0.11

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.
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React, { useState, useCallback } from 'react';
3
3
  import { AsChildSlot } from '@wix/headless-utils/react';
4
- import { Form as FormViewer, } from '@wix/form-public';
5
- import { Root as CoreRoot, Loading as CoreLoading, LoadingError as CoreLoadingError, Error as CoreError, Submitted as CoreSubmitted, Fields as CoreFields, } from './core/Form';
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
6
  var TestIds;
7
7
  (function (TestIds) {
8
8
  TestIds["formRoot"] = "form-root";
@@ -11,6 +11,9 @@ var TestIds;
11
11
  TestIds["formLoadingError"] = "form-loading-error";
12
12
  TestIds["formError"] = "form-error";
13
13
  TestIds["formSubmitted"] = "form-submitted";
14
+ TestIds["fieldRoot"] = "field-root";
15
+ TestIds["fieldLabel"] = "field-label";
16
+ TestIds["fieldInput"] = "field-input";
14
17
  })(TestIds || (TestIds = {}));
15
18
  /**
16
19
  * Root component that provides all necessary service contexts for a complete form experience.
@@ -312,6 +315,8 @@ export const Submitted = React.forwardRef((props, ref) => {
312
315
  * @component
313
316
  * @param {FieldsProps} props - Component props
314
317
  * @param {FieldMap} props.fieldMap - A mapping of field types to their corresponding React components
318
+ * @param {string} props.rowGapClassname - CSS class name for gap between rows
319
+ * @param {string} props.columnGapClassname - CSS class name for gap between columns
315
320
  * @example
316
321
  * ```tsx
317
322
  * import { Form } from '@wix/headless-forms/react';
@@ -330,7 +335,11 @@ export const Submitted = React.forwardRef((props, ref) => {
330
335
  * <Form.Root formServiceConfig={formServiceConfig}>
331
336
  * <Form.Loading className="flex justify-center p-4" />
332
337
  * <Form.LoadingError className="text-destructive px-4 py-3 rounded mb-4" />
333
- * <Form.Fields fieldMap={FIELD_MAP} />
338
+ * <Form.Fields
339
+ * fieldMap={FIELD_MAP}
340
+ * rowGapClassname="gap-y-4"
341
+ * columnGapClassname="gap-x-2"
342
+ * />
334
343
  * </Form.Root>
335
344
  * );
336
345
  * }
@@ -345,12 +354,15 @@ export const Submitted = React.forwardRef((props, ref) => {
345
354
  * - Field validation and error display
346
355
  * - Form state management
347
356
  * - Field value updates
357
+ * - Grid layout with configurable row and column gaps
348
358
  *
349
359
  * Must be used within Form.Root to access form context.
350
360
  *
351
361
  * @component
352
362
  * @param {FieldsProps} props - The component props
353
363
  * @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.
364
+ * @param {string} props.rowGapClassname - CSS class name for gap between form rows
365
+ * @param {string} props.columnGapClassname - CSS class name for gap between form columns
354
366
  *
355
367
  * @example
356
368
  * ```tsx
@@ -418,7 +430,11 @@ export const Submitted = React.forwardRef((props, ref) => {
418
430
  * <Form.Root formServiceConfig={formServiceConfig}>
419
431
  * <Form.Loading className="flex justify-center p-4" />
420
432
  * <Form.LoadingError className="text-destructive px-4 py-3 rounded mb-4" />
421
- * <Form.Fields fieldMap={FIELD_MAP} />
433
+ * <Form.Fields
434
+ * fieldMap={FIELD_MAP}
435
+ * rowGapClassname="gap-y-4"
436
+ * columnGapClassname="gap-x-2"
437
+ * />
422
438
  * <Form.Error className="text-destructive p-4 rounded-lg mb-4" />
423
439
  * <Form.Submitted className="text-green-500 p-4 rounded-lg mb-4" />
424
440
  * </Form.Root>
@@ -428,23 +444,41 @@ export const Submitted = React.forwardRef((props, ref) => {
428
444
  *
429
445
  * @example
430
446
  * ```tsx
431
- * // Advanced usage with custom field components
432
- * const CustomTextField = ({ value, onChange, label, error, ...props }) => (
433
- * <div className="form-field">
434
- * <label className="text-foreground font-paragraph">{label}</label>
435
- * <input
436
- * value={value || ''}
437
- * onChange={(e) => onChange(e.target.value)}
438
- * className="bg-background border-foreground text-foreground"
439
- * {...props}
440
- * />
441
- * {error && <span className="text-destructive">{error}</span>}
442
- * </div>
443
- * );
447
+ * // Creating custom field components - ALL field components MUST use Form.Field
448
+ * // This example shows the REQUIRED structure for a TEXT_INPUT component
449
+ * import { Form, type TextInputProps } from '@wix/headless-forms/react';
450
+ *
451
+ * const TextInput = (props: TextInputProps) => {
452
+ * const { id, value, onChange, label, error, required, ...inputProps } = props;
453
+ *
454
+ * // Form.Field provides automatic grid layout positioning
455
+ * return (
456
+ * <Form.Field id={id}>
457
+ * <Form.Field.Label>
458
+ * <label className="text-foreground font-paragraph">
459
+ * {label}
460
+ * {required && <span className="text-destructive ml-1">*</span>}
461
+ * </label>
462
+ * </Form.Field.Label>
463
+ * <Form.Field.Input
464
+ * description={error && <span className="text-destructive text-sm">{error}</span>}
465
+ * >
466
+ * <input
467
+ * type="text"
468
+ * value={value || ''}
469
+ * onChange={(e) => onChange(e.target.value)}
470
+ * className="bg-background border-foreground text-foreground"
471
+ * aria-invalid={!!error}
472
+ * {...inputProps}
473
+ * />
474
+ * </Form.Field.Input>
475
+ * </Form.Field>
476
+ * );
477
+ * };
444
478
  *
445
479
  * const FIELD_MAP = {
446
- * TEXT_INPUT: CustomTextField,
447
- * // ... other field components
480
+ * TEXT_INPUT: TextInput,
481
+ * // ... all other field components must also use Form.Field
448
482
  * };
449
483
  * ```
450
484
  */
@@ -460,6 +494,164 @@ export const Fields = React.forwardRef((props, ref) => {
460
494
  return (_jsx(CoreFields, { children: ({ form, submitForm }) => {
461
495
  if (!form)
462
496
  return null;
463
- return (_jsx("div", { ref: ref, children: _jsx(FormViewer, { form: form, values: formValues, onChange: handleFormChange, errors: formErrors, onValidate: handleFormValidate, fields: props.fieldMap, submitForm: () => submitForm(formValues) }) }));
497
+ return (_jsx("div", { ref: ref, children: _jsx(FormProvider, { 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 }) }) }));
498
+ } }));
499
+ });
500
+ const FieldsWithForm = ({ form, submitForm, values, onChange, errors, onValidate, fields: fieldMap, rowGapClassname, columnGapClassname, }) => {
501
+ const formData = useForm({
502
+ form,
503
+ values,
504
+ onChange,
505
+ errors,
506
+ onValidate,
507
+ submitForm,
508
+ fieldMap,
509
+ });
510
+ if (!formData)
511
+ return null;
512
+ console.log('formData', formData);
513
+ const { columnCount, fieldElements, fieldsLayout } = formData;
514
+ return (
515
+ // TODO: use readOnly, isDisabled
516
+ // TODO: step title a11y support
517
+ // TODO: mobile support?
518
+ _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) => {
519
+ return (_jsx("div", { style: {
520
+ display: 'grid',
521
+ width: '100%',
522
+ gridTemplateColumns: `repeat(${columnCount}, 1fr)`,
523
+ gridAutoRows: 'minmax(min-content, max-content)',
524
+ }, className: columnGapClassname, children: rowElements }, index));
525
+ }) }) }) }));
526
+ };
527
+ /**
528
+ * Context for sharing field layout data across the form
529
+ * @internal
530
+ */
531
+ const FieldLayoutContext = React.createContext(null);
532
+ /**
533
+ * Provider component that makes field layout data available to child components
534
+ * @internal
535
+ */
536
+ const FieldLayoutProvider = ({ value, children, }) => {
537
+ return (_jsx(FieldLayoutContext.Provider, { value: value, children: children }));
538
+ };
539
+ /**
540
+ * Hook to access layout configuration for a specific field
541
+ * @internal
542
+ * @param {string} fieldId - The unique identifier of the field
543
+ * @returns {Layout | null} The layout configuration for the field, or null if not found
544
+ */
545
+ function useFieldLayout(fieldId) {
546
+ const layoutMap = React.useContext(FieldLayoutContext);
547
+ if (!layoutMap) {
548
+ return null;
549
+ }
550
+ return layoutMap[fieldId] || null;
551
+ }
552
+ const FieldContext = React.createContext(null);
553
+ /**
554
+ * Hook to access field context
555
+ */
556
+ function useFieldContext() {
557
+ const context = React.useContext(FieldContext);
558
+ if (!context) {
559
+ throw new globalThis.Error('Field components must be used within a Form.Field component');
560
+ }
561
+ return context;
562
+ }
563
+ /**
564
+ * Container component for a form field with grid layout support.
565
+ * Provides context to Field.Label and Field.Input child components.
566
+ * Based on the default-field-layout functionality.
567
+ *
568
+ * @component
569
+ * @example
570
+ * ```tsx
571
+ * import { Form } from '@wix/headless-forms/react';
572
+ *
573
+ * function FormFields() {
574
+ * return (
575
+ * <Form.Field id="username">
576
+ * <Form.Field.Label>
577
+ * <label className="text-foreground font-paragraph">Username</label>
578
+ * </Form.Field.Label>
579
+ * <Form.Field.Input description={<span className="text-secondary-foreground">Required</span>}>
580
+ * <input className="bg-background border-foreground text-foreground" />
581
+ * </Form.Field.Input>
582
+ * </Form.Field>
583
+ * );
584
+ * }
585
+ * ```
586
+ */
587
+ const FieldRoot = React.forwardRef((props, ref) => {
588
+ const { id, children, asChild, className, ...otherProps } = props;
589
+ const layout = useFieldLayout(id);
590
+ if (!layout) {
591
+ return null;
592
+ }
593
+ return (_jsx(CoreField, { id: id, layout: layout, children: (fieldData) => {
594
+ const contextValue = {
595
+ id,
596
+ layout: fieldData.layout,
597
+ gridStyles: fieldData.gridStyles,
598
+ };
599
+ 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 }) }));
464
600
  } }));
465
601
  });
602
+ FieldRoot.displayName = 'Form.Field';
603
+ /**
604
+ * Label component for a form field with automatic grid positioning.
605
+ * Must be used within a Form.Field component.
606
+ * Renders in the label row of the field's grid layout.
607
+ *
608
+ * @component
609
+ * @example
610
+ * ```tsx
611
+ * import { Form } from '@wix/headless-forms/react';
612
+ *
613
+ * <Form.Field id="email">
614
+ * <Form.Field.Label>
615
+ * <label className="text-foreground font-paragraph">Email Address</label>
616
+ * </Form.Field.Label>
617
+ * <Form.Field.Input>
618
+ * <input type="email" className="bg-background border-foreground" />
619
+ * </Form.Field.Input>
620
+ * </Form.Field>
621
+ * ```
622
+ */
623
+ export const FieldLabel = React.forwardRef((props, ref) => {
624
+ const { children, asChild, className, ...otherProps } = props;
625
+ const { gridStyles } = useFieldContext();
626
+ 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 }) }));
627
+ });
628
+ FieldLabel.displayName = 'Form.Field.Label';
629
+ /**
630
+ * Input component for a form field with automatic grid positioning.
631
+ * Must be used within a Form.Field component.
632
+ * Renders in the input row of the field's grid layout with optional description.
633
+ *
634
+ * @component
635
+ * @example
636
+ * ```tsx
637
+ * import { Form } from '@wix/headless-forms/react';
638
+ *
639
+ * <Form.Field id="password">
640
+ * <Form.Field.Label>
641
+ * <label className="text-foreground font-paragraph">Password</label>
642
+ * </Form.Field.Label>
643
+ * <Form.Field.Input description={<span className="text-secondary-foreground">Min 8 characters</span>}>
644
+ * <input type="password" className="bg-background border-foreground text-foreground" />
645
+ * </Form.Field.Input>
646
+ * </Form.Field>
647
+ * ```
648
+ */
649
+ export const FieldInput = React.forwardRef((props, ref) => {
650
+ const { children, description, asChild, className, ...otherProps } = props;
651
+ const { gridStyles } = useFieldContext();
652
+ return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, style: gridStyles.input, "data-testid": TestIds.fieldInput, customElement: children, customElementProps: {}, ...otherProps, children: _jsx("div", { children: children }) }));
653
+ });
654
+ FieldInput.displayName = 'Form.Field.Input';
655
+ export const Field = FieldRoot;
656
+ Field.Label = FieldLabel;
657
+ Field.Input = FieldInput;
@@ -135,14 +135,14 @@ export interface FormErrorRenderProps {
135
135
  */
136
136
  export declare function LoadingError(props: FormErrorProps): import("react").ReactNode;
137
137
  /**
138
- * Props for Form Error headless component
138
+ * Props for Form Submit Error headless component
139
139
  */
140
140
  export interface FormSubmitErrorProps {
141
141
  /** Render prop function that receives submit error state data */
142
142
  children: (props: FormSubmitErrorRenderProps) => React.ReactNode;
143
143
  }
144
144
  /**
145
- * Render props for Form Error component
145
+ * Render props for Form Submit Error component
146
146
  */
147
147
  export interface FormSubmitErrorRenderProps {
148
148
  /** Submit error message */
@@ -222,14 +222,17 @@ export declare function Submitted(props: FormSubmittedProps): import("react").Re
222
222
  /**
223
223
  * Render props for Fields component
224
224
  */
225
- interface FieldsRenderProps {
225
+ export interface FieldsRenderProps {
226
+ /** The form data, or null if not loaded */
226
227
  form: forms.Form | null;
228
+ /** Function to submit the form with values */
227
229
  submitForm: (formValues: FormValues) => Promise<void>;
228
230
  }
229
231
  /**
230
232
  * Props for Fields headless component
231
233
  */
232
- interface FieldsProps {
234
+ export interface FieldsProps {
235
+ /** Render prop function that receives form data and submit handler */
233
236
  children: (props: FieldsRenderProps) => React.ReactNode;
234
237
  }
235
238
  /**
@@ -264,4 +267,76 @@ interface FieldsProps {
264
267
  * ```
265
268
  */
266
269
  export declare function Fields(props: FieldsProps): import("react").ReactNode;
267
- export {};
270
+ /**
271
+ * Form view interface containing field definitions
272
+ */
273
+ export interface FormView {
274
+ fields: FieldDefinition[];
275
+ }
276
+ /**
277
+ * Field layout configuration
278
+ */
279
+ export interface Layout {
280
+ column: number;
281
+ row: number;
282
+ height: number;
283
+ width: number;
284
+ }
285
+ /**
286
+ * Field definition including layout information
287
+ */
288
+ export interface FieldDefinition {
289
+ id: string;
290
+ layout: Layout;
291
+ }
292
+ /**
293
+ * Render props for Field component
294
+ */
295
+ export interface FieldRenderProps {
296
+ /** The field ID */
297
+ id: string;
298
+ /** The field layout configuration */
299
+ layout: Layout;
300
+ /** Grid styles for container */
301
+ gridStyles: {
302
+ label: React.CSSProperties;
303
+ input: React.CSSProperties;
304
+ };
305
+ }
306
+ /**
307
+ * Props for Field headless component
308
+ */
309
+ export interface FieldProps {
310
+ /** The unique identifier for this field */
311
+ id: string;
312
+ /** The field layout configuration */
313
+ layout: Layout;
314
+ /** Render prop function that receives field layout data */
315
+ children: (props: FieldRenderProps) => React.ReactNode;
316
+ }
317
+ /**
318
+ * Headless Field component that provides field layout data and grid styles.
319
+ * This component accesses field configuration and calculates grid positioning.
320
+ *
321
+ * @component
322
+ * @param {FieldProps} props - Component props
323
+ * @param {FieldProps['children']} props.children - Render prop function that receives field layout data
324
+ * @example
325
+ * ```tsx
326
+ * import { Form } from '@wix/headless-forms/react';
327
+ *
328
+ * function CustomField({ id, layout }) {
329
+ * return (
330
+ * <Form.Field id={id} layout={layout}>
331
+ * {({ id, layout, gridStyles }) => (
332
+ * <div data-field-id={id}>
333
+ * <div style={gridStyles.label}>Label</div>
334
+ * <div style={gridStyles.input}>Input</div>
335
+ * </div>
336
+ * )}
337
+ * </Form.Field>
338
+ * );
339
+ * }
340
+ * ```
341
+ */
342
+ export declare function Field(props: FieldProps): import("react").ReactNode;
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useService, WixServices } from '@wix/services-manager-react';
3
3
  import { createServicesMap } from '@wix/services-manager';
4
4
  import { FormServiceDefinition, FormService, } from '../../services/form-service.js';
5
+ import { calculateGridStyles } from '../utils.js';
5
6
  const DEFAULT_SUCCESS_MESSAGE = 'Your form has been submitted successfully.';
6
7
  /**
7
8
  * Root component that provides the Form service context to its children.
@@ -227,3 +228,42 @@ export function Fields(props) {
227
228
  submitForm,
228
229
  });
229
230
  }
231
+ /**
232
+ * Headless Field component that provides field layout data and grid styles.
233
+ * This component accesses field configuration and calculates grid positioning.
234
+ *
235
+ * @component
236
+ * @param {FieldProps} props - Component props
237
+ * @param {FieldProps['children']} props.children - Render prop function that receives field layout data
238
+ * @example
239
+ * ```tsx
240
+ * import { Form } from '@wix/headless-forms/react';
241
+ *
242
+ * function CustomField({ id, layout }) {
243
+ * return (
244
+ * <Form.Field id={id} layout={layout}>
245
+ * {({ id, layout, gridStyles }) => (
246
+ * <div data-field-id={id}>
247
+ * <div style={gridStyles.label}>Label</div>
248
+ * <div style={gridStyles.input}>Input</div>
249
+ * </div>
250
+ * )}
251
+ * </Form.Field>
252
+ * );
253
+ * }
254
+ * ```
255
+ */
256
+ export function Field(props) {
257
+ const { id, children, layout } = props;
258
+ const { formSignal } = useService(FormServiceDefinition);
259
+ const form = formSignal.get();
260
+ if (!form) {
261
+ return null;
262
+ }
263
+ const gridStyles = calculateGridStyles(layout);
264
+ return children({
265
+ id,
266
+ layout,
267
+ gridStyles,
268
+ });
269
+ }
@@ -0,0 +1,13 @@
1
+ import { Layout } from './core/Form';
2
+ export declare function calculateGridStyles(layout: Layout): {
3
+ label: {
4
+ gridRow: string;
5
+ gridColumn: string;
6
+ display: string;
7
+ alignItems: string;
8
+ };
9
+ input: {
10
+ gridRow: string;
11
+ gridColumn: string;
12
+ };
13
+ };
@@ -0,0 +1,17 @@
1
+ export function calculateGridStyles(layout) {
2
+ const labelRow = 1;
3
+ const inputRow = 2;
4
+ const gridColumn = `${layout.column + 1} / span ${layout.width}`;
5
+ return {
6
+ label: {
7
+ gridRow: `${labelRow} / span 1`,
8
+ gridColumn,
9
+ display: 'flex',
10
+ alignItems: 'flex-end',
11
+ },
12
+ input: {
13
+ gridRow: `${inputRow} / span 1`,
14
+ gridColumn,
15
+ },
16
+ };
17
+ }
@@ -13,6 +13,8 @@ export type SubmitResponse = {
13
13
  message: string;
14
14
  } | {
15
15
  type: 'idle';
16
+ } | {
17
+ type: 'loading';
16
18
  };
17
19
  /**
18
20
  * API interface for the Form service, providing reactive form data management.
@@ -67,7 +67,10 @@ export const FormService = implementService.withConfig()(FormServiceDefinition,
67
67
  }
68
68
  async function defaultSubmitHandler(formId, formValues) {
69
69
  try {
70
- await submissions.createSubmission({ formId, ...formValues });
70
+ await submissions.createSubmission({
71
+ formId,
72
+ submissions: formValues,
73
+ });
71
74
  // TODO: add message
72
75
  return { type: 'success' };
73
76
  }
@@ -88,7 +91,7 @@ export const FormService = implementService.withConfig()(FormServiceDefinition,
88
91
  }
89
92
  // @ts-expect-error
90
93
  const formId = form._id ? form._id : form.id;
91
- submitResponseSignal.set({ type: 'idle' });
94
+ submitResponseSignal.set({ type: 'loading' });
92
95
  try {
93
96
  const handler = config.onSubmit || defaultSubmitHandler;
94
97
  const response = await handler(formId, formValues);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/headless-forms",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "npm run build:esm && npm run build:cjs",
@@ -41,8 +41,8 @@
41
41
  "vitest": "^3.1.4"
42
42
  },
43
43
  "dependencies": {
44
- "@wix/form-public": "^0.26.0",
45
- "@wix/forms": "^1.0.330",
44
+ "@wix/form-public": "^0.40.0",
45
+ "@wix/forms": "^1.0.331",
46
46
  "@wix/headless-utils": "0.0.4",
47
47
  "@wix/services-definitions": "^0.1.4",
48
48
  "@wix/services-manager-react": "^0.1.26"