@wix/headless-bookings 0.0.55 → 0.0.57
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/cjs/dist/react/booking-form/BookingForm.d.ts +117 -17
- package/cjs/dist/react/booking-form/BookingForm.js +149 -15
- package/cjs/dist/react/core/booking-form/BookingForm.d.ts +112 -10
- package/cjs/dist/react/core/booking-form/BookingForm.js +204 -64
- package/cjs/dist/react/time-slot-list/TimeSlot.d.ts +25 -0
- package/cjs/dist/react/time-slot-list/TimeSlot.js +48 -13
- package/cjs/dist/services/booking-form/booking-form.d.ts +22 -13
- package/cjs/dist/services/booking-form/booking-form.js +67 -25
- package/cjs/dist/services/booking-form/utils.d.ts +52 -0
- package/cjs/dist/services/booking-form/utils.js +47 -0
- package/dist/react/booking-form/BookingForm.d.ts +117 -17
- package/dist/react/booking-form/BookingForm.js +149 -15
- package/dist/react/core/booking-form/BookingForm.d.ts +112 -10
- package/dist/react/core/booking-form/BookingForm.js +204 -64
- package/dist/react/time-slot-list/TimeSlot.d.ts +25 -0
- package/dist/react/time-slot-list/TimeSlot.js +48 -13
- package/dist/services/booking-form/booking-form.d.ts +22 -13
- package/dist/services/booking-form/booking-form.js +67 -25
- package/dist/services/booking-form/utils.d.ts +52 -0
- package/dist/services/booking-form/utils.js +47 -0
- package/package.json +3 -3
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import { defineService, implementService } from '@wix/services-definitions';
|
|
11
11
|
import { SignalsServiceDefinition, } from '@wix/services-definitions/core-services/signals';
|
|
12
12
|
import { FormServiceDefinition } from '@wix/headless-forms/services';
|
|
13
|
+
import { BookingServiceDefinition, } from '../booking/booking.js';
|
|
14
|
+
import { hasForm, extractFormIdFromConfig } from './utils.js';
|
|
13
15
|
// ============================================================================
|
|
14
16
|
// Constants
|
|
15
17
|
// ============================================================================
|
|
@@ -18,22 +20,25 @@ import { FormServiceDefinition } from '@wix/headless-forms/services';
|
|
|
18
20
|
* All booking forms must belong to this namespace.
|
|
19
21
|
*/
|
|
20
22
|
export const BOOKING_FORM_NAMESPACE = 'wix.bookings.v2.bookings';
|
|
21
|
-
// ============================================================================
|
|
22
|
-
// Helper Types
|
|
23
|
-
// ============================================================================
|
|
24
|
-
/**
|
|
25
|
-
* Type guard to check if config has formId
|
|
26
|
-
*/
|
|
27
|
-
export function hasFormId(config) {
|
|
28
|
-
return 'formId' in config;
|
|
29
|
-
}
|
|
30
23
|
/**
|
|
31
|
-
*
|
|
24
|
+
* Error thrown when BookingFormService cannot be initialized due to missing configuration.
|
|
25
|
+
* This occurs when:
|
|
26
|
+
* - No config (formId or form) was provided
|
|
27
|
+
* - AND the component is not wrapped with Booking.Root (no BookingService available)
|
|
32
28
|
*/
|
|
33
|
-
export
|
|
34
|
-
|
|
29
|
+
export class BookingFormConfigurationError extends Error {
|
|
30
|
+
constructor(message) {
|
|
31
|
+
super(message ||
|
|
32
|
+
'BookingFormService: Cannot initialize - no formId or form provided in config and no BookingService available. ' +
|
|
33
|
+
'Either provide a formId/form in the config, or wrap with Booking.Root.');
|
|
34
|
+
this.name = 'BookingFormConfigurationError';
|
|
35
|
+
}
|
|
35
36
|
}
|
|
36
37
|
// ============================================================================
|
|
38
|
+
// Re-export utilities
|
|
39
|
+
// ============================================================================
|
|
40
|
+
export { hasFormId, hasForm, extractFormIdFromForm, extractFormIdFromConfig, } from './utils.js';
|
|
41
|
+
// ============================================================================
|
|
37
42
|
// Service Definition
|
|
38
43
|
// ============================================================================
|
|
39
44
|
/**
|
|
@@ -64,20 +69,21 @@ function validateBookingFormNamespace(form) {
|
|
|
64
69
|
}
|
|
65
70
|
}
|
|
66
71
|
/**
|
|
67
|
-
* Extracts the form ID from either config type.
|
|
68
|
-
* @param config The service config
|
|
72
|
+
* Extracts the form ID from either config type or from services.
|
|
73
|
+
* @param config The service config (optional)
|
|
74
|
+
* @param services The list of services to extract formId from if config is not provided
|
|
69
75
|
* @returns The form ID
|
|
70
76
|
*/
|
|
71
|
-
function extractFormId(config) {
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
function extractFormId(config, services) {
|
|
78
|
+
// Try to extract from config first
|
|
79
|
+
let formId = extractFormIdFromConfig(config);
|
|
80
|
+
// Fallback to extracting from services
|
|
81
|
+
if (!formId) {
|
|
82
|
+
formId = services.find((s) => s.form?._id)?.form?._id;
|
|
74
83
|
}
|
|
75
|
-
// Extract from form object - forms typically have an 'id' or '_id' property
|
|
76
|
-
const form = config.form;
|
|
77
|
-
const formId = form.id ||
|
|
78
|
-
form._id;
|
|
79
84
|
if (!formId) {
|
|
80
|
-
throw new
|
|
85
|
+
throw new BookingFormConfigurationError('BookingFormService: no formId found in config or services. ' +
|
|
86
|
+
'Either provide a formId in the config, or ensure Booking.Root has selected services with a form.');
|
|
81
87
|
}
|
|
82
88
|
return formId;
|
|
83
89
|
}
|
|
@@ -120,14 +126,38 @@ function extractFormId(config) {
|
|
|
120
126
|
export const BookingFormService = implementService.withConfig()(BookingFormServiceDefinition, ({ getService, config }) => {
|
|
121
127
|
const signalsService = getService(SignalsServiceDefinition);
|
|
122
128
|
const formService = getService(FormServiceDefinition);
|
|
129
|
+
// BookingService is optional - may not be available if not wrapped with Booking.Root
|
|
130
|
+
let bookingService = null;
|
|
131
|
+
try {
|
|
132
|
+
bookingService = getService(BookingServiceDefinition);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// BookingService not available - that's ok if formId was provided explicitly
|
|
136
|
+
bookingService = null;
|
|
137
|
+
}
|
|
138
|
+
const getServices = () => {
|
|
139
|
+
if (!bookingService) {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
const serviceSelections = bookingService.serviceSelections.peek();
|
|
143
|
+
return serviceSelections.length
|
|
144
|
+
? serviceSelections.map((s) => s.service)
|
|
145
|
+
: [];
|
|
146
|
+
};
|
|
147
|
+
// Validate that we have either config or bookingService with serviceSelections
|
|
148
|
+
// If neither is available, we can't extract formId and can't initialize the service
|
|
149
|
+
const hasServiceSelections = getServices().length > 0;
|
|
150
|
+
if (!config && (!bookingService || !hasServiceSelections)) {
|
|
151
|
+
throw new BookingFormConfigurationError();
|
|
152
|
+
}
|
|
123
153
|
// Determine if we have a pre-loaded form
|
|
124
|
-
const hasPreloadedForm =
|
|
154
|
+
const hasPreloadedForm = hasForm(config);
|
|
125
155
|
// If form is provided, validate namespace
|
|
126
156
|
if (hasPreloadedForm) {
|
|
127
157
|
validateBookingFormNamespace(config.form);
|
|
128
158
|
}
|
|
129
159
|
// Extract formId from config (either directly or from form object)
|
|
130
|
-
const formIdValue = extractFormId(config);
|
|
160
|
+
const formIdValue = extractFormId(config, getServices());
|
|
131
161
|
// Initialize formSubmission signal (stores form data to be submitted)
|
|
132
162
|
const formSubmission = signalsService.signal(null);
|
|
133
163
|
// Initialize submissionId signal (for future use)
|
|
@@ -135,7 +165,7 @@ export const BookingFormService = implementService.withConfig()(BookingFormServi
|
|
|
135
165
|
// formId signal - readonly
|
|
136
166
|
const formId = signalsService.signal(formIdValue);
|
|
137
167
|
// form signal - contains pre-loaded form if provided
|
|
138
|
-
const form = signalsService.signal(
|
|
168
|
+
const form = signalsService.signal(hasForm(config) ? config.form : null);
|
|
139
169
|
// Internal: Store validate callback (set by core component, not exposed publicly)
|
|
140
170
|
let _validateCallback = null;
|
|
141
171
|
// Internal: Register validate callback (called by core component via closure)
|
|
@@ -155,6 +185,18 @@ export const BookingFormService = implementService.withConfig()(BookingFormServi
|
|
|
155
185
|
}
|
|
156
186
|
return _validateCallback();
|
|
157
187
|
};
|
|
188
|
+
// Auto-sync formValues to BookingService.formSubmission (client-side only)
|
|
189
|
+
// This keeps Booking.Data.formSubmission updated as the user types
|
|
190
|
+
// and ensures the book action gets the latest form values
|
|
191
|
+
// Only set up sync if BookingService is available
|
|
192
|
+
if (bookingService) {
|
|
193
|
+
signalsService.effect(() => {
|
|
194
|
+
const formValues = formService.formValuesSignal.get();
|
|
195
|
+
if (formValues && Object.keys(formValues).length > 0) {
|
|
196
|
+
bookingService.actions.setFormSubmission(formValues);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
158
200
|
// Public API (what's exposed in BookingFormServiceAPI interface)
|
|
159
201
|
const publicApi = {
|
|
160
202
|
formValues: formService.formValuesSignal,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for BookingForm
|
|
3
|
+
*
|
|
4
|
+
* Note: This file should not import from booking-form.ts to avoid circular dependencies.
|
|
5
|
+
* Type guards use generic types that will be compatible with BookingFormServiceConfig.
|
|
6
|
+
*/
|
|
7
|
+
import { forms } from '@wix/forms';
|
|
8
|
+
/**
|
|
9
|
+
* Type guard to check if config has formId
|
|
10
|
+
*/
|
|
11
|
+
export declare function hasFormId<T extends {
|
|
12
|
+
formId: string;
|
|
13
|
+
serviceIds?: string[];
|
|
14
|
+
} | {
|
|
15
|
+
form: forms.Form;
|
|
16
|
+
}>(config: T | undefined): config is T & {
|
|
17
|
+
formId: string;
|
|
18
|
+
serviceIds?: string[];
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Type guard to check if config has pre-loaded form
|
|
22
|
+
*/
|
|
23
|
+
export declare function hasForm<T extends {
|
|
24
|
+
formId: string;
|
|
25
|
+
serviceIds?: string[];
|
|
26
|
+
} | {
|
|
27
|
+
form: forms.Form;
|
|
28
|
+
}>(config: T | undefined): config is T & {
|
|
29
|
+
form: forms.Form;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Extracts the form ID from a form object.
|
|
33
|
+
* @param form The form object to extract ID from
|
|
34
|
+
* @returns The form ID or undefined if not found
|
|
35
|
+
*/
|
|
36
|
+
export declare function extractFormIdFromForm(form: forms.Form): string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Configuration object that may contain formId or form.
|
|
39
|
+
* Used by extractFormIdFromConfig.
|
|
40
|
+
*/
|
|
41
|
+
export type FormIdConfig = {
|
|
42
|
+
formId: string;
|
|
43
|
+
serviceIds?: string[];
|
|
44
|
+
} | {
|
|
45
|
+
form: forms.Form;
|
|
46
|
+
} | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Extracts the form ID from a config object (either formId directly or from form object).
|
|
49
|
+
* @param config The config object containing either formId or form
|
|
50
|
+
* @returns The form ID or undefined if not found/extractable
|
|
51
|
+
*/
|
|
52
|
+
export declare function extractFormIdFromConfig(config: FormIdConfig): string | undefined;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for BookingForm
|
|
3
|
+
*
|
|
4
|
+
* Note: This file should not import from booking-form.ts to avoid circular dependencies.
|
|
5
|
+
* Type guards use generic types that will be compatible with BookingFormServiceConfig.
|
|
6
|
+
*/
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Type Guards
|
|
9
|
+
// ============================================================================
|
|
10
|
+
/**
|
|
11
|
+
* Type guard to check if config has formId
|
|
12
|
+
*/
|
|
13
|
+
export function hasFormId(config) {
|
|
14
|
+
return config != null && 'formId' in config;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Type guard to check if config has pre-loaded form
|
|
18
|
+
*/
|
|
19
|
+
export function hasForm(config) {
|
|
20
|
+
return config != null && 'form' in config;
|
|
21
|
+
}
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Form ID Extraction
|
|
24
|
+
// ============================================================================
|
|
25
|
+
/**
|
|
26
|
+
* Extracts the form ID from a form object.
|
|
27
|
+
* @param form The form object to extract ID from
|
|
28
|
+
* @returns The form ID or undefined if not found
|
|
29
|
+
*/
|
|
30
|
+
export function extractFormIdFromForm(form) {
|
|
31
|
+
return (form.id ||
|
|
32
|
+
form._id);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Extracts the form ID from a config object (either formId directly or from form object).
|
|
36
|
+
* @param config The config object containing either formId or form
|
|
37
|
+
* @returns The form ID or undefined if not found/extractable
|
|
38
|
+
*/
|
|
39
|
+
export function extractFormIdFromConfig(config) {
|
|
40
|
+
if (hasFormId(config)) {
|
|
41
|
+
return config.formId;
|
|
42
|
+
}
|
|
43
|
+
if (hasForm(config)) {
|
|
44
|
+
return extractFormIdFromForm(config.form);
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
@@ -4,17 +4,20 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import React from 'react';
|
|
6
6
|
import { Form } from '@wix/headless-forms/react';
|
|
7
|
+
import * as CoreBookingForm from '../core/booking-form/BookingForm.js';
|
|
7
8
|
type FieldMap = Parameters<typeof Form.Fields>[0]['fieldMap'];
|
|
8
9
|
/**
|
|
9
10
|
* Props for BookingForm.Root component
|
|
10
11
|
*/
|
|
11
12
|
export interface RootProps {
|
|
12
13
|
/** The form ID to load (required - standalone mode) */
|
|
13
|
-
formId
|
|
14
|
+
formId?: string;
|
|
15
|
+
/** Optional service IDs to pass to FormService (will be merged into additionalMetadata) */
|
|
16
|
+
serviceIds?: string[];
|
|
14
17
|
/** Optional additional metadata to pass to FormService */
|
|
15
18
|
additionalMetadata?: Record<string, string | string[]>;
|
|
16
|
-
/**
|
|
17
|
-
fieldMap
|
|
19
|
+
/** Field map to override default booking field components */
|
|
20
|
+
fieldMap: FieldMap;
|
|
18
21
|
/** Child components - if not provided, Form.Fields is rendered with default field map */
|
|
19
22
|
children?: React.ReactNode;
|
|
20
23
|
/** Whether to render as a child component */
|
|
@@ -25,6 +28,8 @@ export interface RootProps {
|
|
|
25
28
|
rowGapClassname?: string;
|
|
26
29
|
/** Column gap class name for Form.Fields when using default rendering */
|
|
27
30
|
columnGapClassname?: string;
|
|
31
|
+
/** Custom fallback UI for load errors. Can be a ReactNode or a function that receives the error. */
|
|
32
|
+
loadErrorFallback?: React.ReactNode | ((error: Error) => React.ReactNode);
|
|
28
33
|
}
|
|
29
34
|
/**
|
|
30
35
|
* Root component for booking form that wraps CoreBookingForm.Root.
|
|
@@ -40,6 +45,12 @@ export interface RootProps {
|
|
|
40
45
|
* // Minimal usage - uses default field components
|
|
41
46
|
* <BookingForm.Root formId="your-form-id" />
|
|
42
47
|
*
|
|
48
|
+
* // With serviceIds
|
|
49
|
+
* <BookingForm.Root
|
|
50
|
+
* formId="your-form-id"
|
|
51
|
+
* serviceIds={['service-1', 'service-2']}
|
|
52
|
+
* />
|
|
53
|
+
*
|
|
43
54
|
* // With custom styling
|
|
44
55
|
* <BookingForm.Root
|
|
45
56
|
* formId="your-form-id"
|
|
@@ -91,6 +102,74 @@ export interface FieldsProps {
|
|
|
91
102
|
* ```
|
|
92
103
|
*/
|
|
93
104
|
export declare const Fields: React.ForwardRefExoticComponent<FieldsProps & React.RefAttributes<HTMLDivElement>>;
|
|
105
|
+
/**
|
|
106
|
+
* Props for BookingForm.Actions.ValidateFormSubmission component
|
|
107
|
+
*/
|
|
108
|
+
export interface ValidateFormSubmissionProps {
|
|
109
|
+
asChild?: boolean;
|
|
110
|
+
children?: React.ReactNode | ((props: {
|
|
111
|
+
onClick: () => Promise<void>;
|
|
112
|
+
valid: boolean | null;
|
|
113
|
+
validationFailures: string[];
|
|
114
|
+
}) => React.ReactNode);
|
|
115
|
+
className?: string;
|
|
116
|
+
label?: string;
|
|
117
|
+
/** Callback when validation completes */
|
|
118
|
+
onValidationComplete?: (result: {
|
|
119
|
+
valid: boolean;
|
|
120
|
+
validationFailures: string[];
|
|
121
|
+
}) => void;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Button to validate the booking form submission.
|
|
125
|
+
* Must be used within BookingForm.Root or BookingForm.Data context.
|
|
126
|
+
* Default label is "Validate".
|
|
127
|
+
*
|
|
128
|
+
* @component
|
|
129
|
+
* @example
|
|
130
|
+
* ```tsx
|
|
131
|
+
* // Within BookingForm.Root
|
|
132
|
+
* <BookingForm.Root formId="your-form-id" fieldMap={...}>
|
|
133
|
+
* <BookingForm.Fields />
|
|
134
|
+
* <BookingForm.Actions.ValidateFormSubmission />
|
|
135
|
+
* </BookingForm.Root>
|
|
136
|
+
*
|
|
137
|
+
* // With custom label
|
|
138
|
+
* <BookingForm.Actions.ValidateFormSubmission label="Check Form" />
|
|
139
|
+
*
|
|
140
|
+
* // With asChild
|
|
141
|
+
* <BookingForm.Actions.ValidateFormSubmission asChild>
|
|
142
|
+
* <button className="btn-primary">Validate Booking</button>
|
|
143
|
+
* </BookingForm.Actions.ValidateFormSubmission>
|
|
144
|
+
*
|
|
145
|
+
* // Using render prop pattern with validation callback
|
|
146
|
+
* <BookingForm.Actions.ValidateFormSubmission
|
|
147
|
+
* onValidationComplete={(result) => {
|
|
148
|
+
* if (result.valid) {
|
|
149
|
+
* console.log('Form is valid!');
|
|
150
|
+
* } else {
|
|
151
|
+
* console.log('Validation errors:', result.validationFailures);
|
|
152
|
+
* }
|
|
153
|
+
* }}
|
|
154
|
+
* >
|
|
155
|
+
* {({ onClick, valid, validationFailures }) => (
|
|
156
|
+
* <button onClick={onClick}>
|
|
157
|
+
* {valid === null ? 'Validate' : valid ? 'Valid ✓' : 'Invalid ✗'}
|
|
158
|
+
* {validationFailures.length > 0 && (
|
|
159
|
+
* <span> ({validationFailures.length} errors)</span>
|
|
160
|
+
* )}
|
|
161
|
+
* </button>
|
|
162
|
+
* )}
|
|
163
|
+
* </BookingForm.Actions.ValidateFormSubmission>
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
export declare const ValidateFormSubmission: React.ForwardRefExoticComponent<ValidateFormSubmissionProps & React.RefAttributes<HTMLButtonElement>>;
|
|
167
|
+
/**
|
|
168
|
+
* Actions namespace for BookingForm
|
|
169
|
+
*/
|
|
170
|
+
export declare const Actions: {
|
|
171
|
+
ValidateFormSubmission: React.ForwardRefExoticComponent<ValidateFormSubmissionProps & React.RefAttributes<HTMLButtonElement>>;
|
|
172
|
+
};
|
|
94
173
|
/**
|
|
95
174
|
* Render props for BookingForm.Data component
|
|
96
175
|
*/
|
|
@@ -105,24 +184,27 @@ export interface DataRenderProps {
|
|
|
105
184
|
formSubmission: Record<string, unknown> | null;
|
|
106
185
|
/** Action to store form submission data */
|
|
107
186
|
setFormSubmission: (formValues: Record<string, unknown>) => void;
|
|
108
|
-
/**
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}>;
|
|
187
|
+
/** Ref to access form methods imperatively (pass to Form.Fields) */
|
|
188
|
+
formRef: React.RefObject<CoreBookingForm.FormHandle | null>;
|
|
189
|
+
/** Action to validate the form fields. Returns true if valid, false if there are errors. */
|
|
190
|
+
validate: () => Promise<boolean>;
|
|
113
191
|
}
|
|
114
192
|
/**
|
|
115
193
|
* Props for BookingForm.Data component
|
|
116
194
|
*/
|
|
117
195
|
export interface DataProps {
|
|
118
|
-
/** The form ID to load (
|
|
119
|
-
formId
|
|
196
|
+
/** The form ID to load (if not provided, the formId will be extracted from the selected service if available) */
|
|
197
|
+
formId?: string;
|
|
198
|
+
/** Optional service IDs to pass to FormService (will be merged into additionalMetadata) */
|
|
199
|
+
serviceIds?: string[];
|
|
120
200
|
/** Optional additional metadata to pass to FormService */
|
|
121
201
|
additionalMetadata?: Record<string, string | string[]>;
|
|
122
|
-
/**
|
|
123
|
-
fieldMap
|
|
202
|
+
/** Field map to override default booking field components */
|
|
203
|
+
fieldMap: FieldMap;
|
|
124
204
|
/** Render prop function that receives form data and actions */
|
|
125
205
|
children: (data: DataRenderProps) => React.ReactNode;
|
|
206
|
+
/** Custom fallback UI for load errors. Can be a ReactNode or a function that receives the error. */
|
|
207
|
+
loadErrorFallback?: React.ReactNode | ((error: Error) => React.ReactNode);
|
|
126
208
|
}
|
|
127
209
|
/**
|
|
128
210
|
* Data component that provides booking form data via render props.
|
|
@@ -132,12 +214,17 @@ export interface DataProps {
|
|
|
132
214
|
* @example
|
|
133
215
|
* ```tsx
|
|
134
216
|
* <BookingForm.Data formId="your-form-id">
|
|
135
|
-
* {({ formId, fields, formValues,
|
|
217
|
+
* {({ formId, fields, formValues, formRef, validate, setFormSubmission }) => (
|
|
136
218
|
* <div>
|
|
137
|
-
* <Form.Fields
|
|
219
|
+
* <Form.Fields
|
|
220
|
+
* fieldMap={fields}
|
|
221
|
+
* rowGapClassname="gap-y-4"
|
|
222
|
+
* columnGapClassname="gap-x-2"
|
|
223
|
+
* formRef={formRef}
|
|
224
|
+
* />
|
|
138
225
|
* <button onClick={async () => {
|
|
139
|
-
* const
|
|
140
|
-
* if (
|
|
226
|
+
* const isValid = await validate();
|
|
227
|
+
* if (isValid) {
|
|
141
228
|
* // Store current form values before submission
|
|
142
229
|
* setFormSubmission(formValues);
|
|
143
230
|
* }
|
|
@@ -147,10 +234,23 @@ export interface DataProps {
|
|
|
147
234
|
* </div>
|
|
148
235
|
* )}
|
|
149
236
|
* </BookingForm.Data>
|
|
237
|
+
*
|
|
238
|
+
* // With serviceIds
|
|
239
|
+
* <BookingForm.Data
|
|
240
|
+
* formId="your-form-id"
|
|
241
|
+
* serviceIds={['service-1', 'service-2']}
|
|
242
|
+
* >
|
|
243
|
+
* {({ fields, formRef }) => (
|
|
244
|
+
* <Form.Fields fieldMap={fields} formRef={formRef} />
|
|
245
|
+
* )}
|
|
246
|
+
* </BookingForm.Data>
|
|
150
247
|
* ```
|
|
151
248
|
*/
|
|
152
249
|
export declare function Data(props: DataProps): React.ReactNode;
|
|
153
250
|
export declare namespace Data {
|
|
154
251
|
var displayName: string;
|
|
155
252
|
}
|
|
156
|
-
|
|
253
|
+
/**
|
|
254
|
+
* Re-export LoadError component and utilities for error handling
|
|
255
|
+
*/
|
|
256
|
+
export { LoadError, isBookingFormConfigurationError, BookingFormConfigurationError, } from '../core/booking-form/BookingForm.js';
|
|
@@ -7,10 +7,44 @@ import React from 'react';
|
|
|
7
7
|
import { AsChildSlot } from '@wix/headless-utils/react';
|
|
8
8
|
import { Form } from '@wix/headless-forms/react';
|
|
9
9
|
import * as CoreBookingForm from '../core/booking-form/BookingForm.js';
|
|
10
|
+
import { BookingFormConfigurationError } from '../../services/booking-form/booking-form.js';
|
|
11
|
+
/**
|
|
12
|
+
* Internal ErrorBoundary for catching BookingFormConfigurationError
|
|
13
|
+
*/
|
|
14
|
+
class BookingFormErrorBoundary extends React.Component {
|
|
15
|
+
constructor(props) {
|
|
16
|
+
super(props);
|
|
17
|
+
this.state = { error: null };
|
|
18
|
+
}
|
|
19
|
+
static getDerivedStateFromError(error) {
|
|
20
|
+
return { error };
|
|
21
|
+
}
|
|
22
|
+
render() {
|
|
23
|
+
const { error } = this.state;
|
|
24
|
+
const { children, fallback } = this.props;
|
|
25
|
+
if (error) {
|
|
26
|
+
// If it's a configuration error, render the fallback
|
|
27
|
+
if (error instanceof BookingFormConfigurationError) {
|
|
28
|
+
if (typeof fallback === 'function') {
|
|
29
|
+
return fallback(error);
|
|
30
|
+
}
|
|
31
|
+
if (fallback) {
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
// Default fallback UI
|
|
35
|
+
return _jsx("div", { "data-testid": "booking-form-load-error", children: error.message });
|
|
36
|
+
}
|
|
37
|
+
// Re-throw non-configuration errors
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
return children;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
10
43
|
var TestIds;
|
|
11
44
|
(function (TestIds) {
|
|
12
45
|
TestIds["bookingFormRoot"] = "booking-form-root";
|
|
13
46
|
TestIds["bookingFormFields"] = "booking-form-fields";
|
|
47
|
+
TestIds["bookingFormActionValidateFormSubmission"] = "booking-form-action-validate-form-submission";
|
|
14
48
|
})(TestIds || (TestIds = {}));
|
|
15
49
|
/**
|
|
16
50
|
* Root component for booking form that wraps CoreBookingForm.Root.
|
|
@@ -26,6 +60,12 @@ var TestIds;
|
|
|
26
60
|
* // Minimal usage - uses default field components
|
|
27
61
|
* <BookingForm.Root formId="your-form-id" />
|
|
28
62
|
*
|
|
63
|
+
* // With serviceIds
|
|
64
|
+
* <BookingForm.Root
|
|
65
|
+
* formId="your-form-id"
|
|
66
|
+
* serviceIds={['service-1', 'service-2']}
|
|
67
|
+
* />
|
|
68
|
+
*
|
|
29
69
|
* // With custom styling
|
|
30
70
|
* <BookingForm.Root
|
|
31
71
|
* formId="your-form-id"
|
|
@@ -51,8 +91,8 @@ var TestIds;
|
|
|
51
91
|
* ```
|
|
52
92
|
*/
|
|
53
93
|
export const Root = React.forwardRef((props, ref) => {
|
|
54
|
-
const { children, asChild, className, formId, additionalMetadata, fieldMap, rowGapClassname = 'gap-y-4', columnGapClassname = 'gap-x-2', ...otherProps } = props;
|
|
55
|
-
return (_jsx(CoreBookingForm.Root, { formId: formId, additionalMetadata: additionalMetadata, fieldMap: fieldMap, children: ({ fields }) => (_jsx(BookingFormContext.Provider, { value: { fields, rowGapClassname, columnGapClassname }, children: _jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.bookingFormRoot, ...otherProps, children: _jsx("div", { children: children ?? (_jsxs(_Fragment, { children: [_jsx(Form.Loading, { className: "flex justify-center p-4 text-foreground" }), _jsx(Form.LoadingError, { className: "bg-destructive/10 border border-destructive/20 text-destructive px-4 py-3 rounded-lg mb-4" }), _jsx(Form.Fields, { fieldMap: fields, rowGapClassname: rowGapClassname, columnGapClassname: columnGapClassname }), _jsx(Form.Error, { className: "mt-4 bg-destructive/10 border border-destructive/20 text-destructive px-4 py-3 rounded-lg" }), _jsx(Form.Submitted, { className: "mt-4 bg-green-500/10 border border-green-500/20 text-green-500 px-4 py-3 rounded-lg" })] })) }) }) })) }));
|
|
94
|
+
const { children, asChild, className, formId, serviceIds, additionalMetadata, fieldMap, rowGapClassname = 'gap-y-4', columnGapClassname = 'gap-x-2', loadErrorFallback, ...otherProps } = props;
|
|
95
|
+
return (_jsx(BookingFormErrorBoundary, { fallback: loadErrorFallback, children: _jsx(CoreBookingForm.Root, { formId: formId, serviceIds: serviceIds, additionalMetadata: additionalMetadata, fieldMap: fieldMap, children: ({ fields }) => (_jsx(BookingFormContext.Provider, { value: { fields, rowGapClassname, columnGapClassname }, children: _jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.bookingFormRoot, ...otherProps, children: _jsx("div", { children: children ?? (_jsxs(_Fragment, { children: [_jsx(Form.Loading, { className: "flex justify-center p-4 text-foreground" }), _jsx(Form.LoadingError, { className: "bg-destructive/10 border border-destructive/20 text-destructive px-4 py-3 rounded-lg mb-4" }), _jsx(Form.Fields, { fieldMap: fields, rowGapClassname: rowGapClassname, columnGapClassname: columnGapClassname }), _jsx(Form.Error, { className: "mt-4 bg-destructive/10 border border-destructive/20 text-destructive px-4 py-3 rounded-lg" }), _jsx(Form.Submitted, { className: "mt-4 bg-green-500/10 border border-green-500/20 text-green-500 px-4 py-3 rounded-lg" })] })) }) }) })) }) }));
|
|
56
96
|
});
|
|
57
97
|
Root.displayName = 'BookingForm.Root';
|
|
58
98
|
const BookingFormContext = React.createContext(null);
|
|
@@ -87,6 +127,77 @@ export const Fields = React.forwardRef((props, ref) => {
|
|
|
87
127
|
return (_jsx("div", { ref: ref, className: className, "data-testid": TestIds.bookingFormFields, ...otherProps, children: _jsx(Form.Fields, { fieldMap: context.fields, rowGapClassname: rowGapClassname, columnGapClassname: columnGapClassname }) }));
|
|
88
128
|
});
|
|
89
129
|
Fields.displayName = 'BookingForm.Fields';
|
|
130
|
+
/**
|
|
131
|
+
* Button to validate the booking form submission.
|
|
132
|
+
* Must be used within BookingForm.Root or BookingForm.Data context.
|
|
133
|
+
* Default label is "Validate".
|
|
134
|
+
*
|
|
135
|
+
* @component
|
|
136
|
+
* @example
|
|
137
|
+
* ```tsx
|
|
138
|
+
* // Within BookingForm.Root
|
|
139
|
+
* <BookingForm.Root formId="your-form-id" fieldMap={...}>
|
|
140
|
+
* <BookingForm.Fields />
|
|
141
|
+
* <BookingForm.Actions.ValidateFormSubmission />
|
|
142
|
+
* </BookingForm.Root>
|
|
143
|
+
*
|
|
144
|
+
* // With custom label
|
|
145
|
+
* <BookingForm.Actions.ValidateFormSubmission label="Check Form" />
|
|
146
|
+
*
|
|
147
|
+
* // With asChild
|
|
148
|
+
* <BookingForm.Actions.ValidateFormSubmission asChild>
|
|
149
|
+
* <button className="btn-primary">Validate Booking</button>
|
|
150
|
+
* </BookingForm.Actions.ValidateFormSubmission>
|
|
151
|
+
*
|
|
152
|
+
* // Using render prop pattern with validation callback
|
|
153
|
+
* <BookingForm.Actions.ValidateFormSubmission
|
|
154
|
+
* onValidationComplete={(result) => {
|
|
155
|
+
* if (result.valid) {
|
|
156
|
+
* console.log('Form is valid!');
|
|
157
|
+
* } else {
|
|
158
|
+
* console.log('Validation errors:', result.validationFailures);
|
|
159
|
+
* }
|
|
160
|
+
* }}
|
|
161
|
+
* >
|
|
162
|
+
* {({ onClick, valid, validationFailures }) => (
|
|
163
|
+
* <button onClick={onClick}>
|
|
164
|
+
* {valid === null ? 'Validate' : valid ? 'Valid ✓' : 'Invalid ✗'}
|
|
165
|
+
* {validationFailures.length > 0 && (
|
|
166
|
+
* <span> ({validationFailures.length} errors)</span>
|
|
167
|
+
* )}
|
|
168
|
+
* </button>
|
|
169
|
+
* )}
|
|
170
|
+
* </BookingForm.Actions.ValidateFormSubmission>
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
export const ValidateFormSubmission = React.forwardRef((props, ref) => {
|
|
174
|
+
const { asChild, children, className, label = 'Validate', onValidationComplete, } = props;
|
|
175
|
+
const [valid, setValid] = React.useState(null);
|
|
176
|
+
const [validationFailures, setValidationFailures] = React.useState([]);
|
|
177
|
+
return (_jsx(CoreBookingForm.Actions, { children: ({ validateFormSubmission }) => {
|
|
178
|
+
const handleClick = async () => {
|
|
179
|
+
const result = await validateFormSubmission();
|
|
180
|
+
setValid(result.valid);
|
|
181
|
+
setValidationFailures(result.validationFailures);
|
|
182
|
+
if (onValidationComplete) {
|
|
183
|
+
onValidationComplete(result);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
return (_jsx(AsChildSlot, { ref: ref, asChild: asChild, className: className, "data-testid": TestIds.bookingFormActionValidateFormSubmission, "data-valid": valid, customElement: children, customElementProps: {
|
|
187
|
+
onClick: handleClick,
|
|
188
|
+
valid,
|
|
189
|
+
validationFailures,
|
|
190
|
+
}, children: _jsx("button", { onClick: handleClick, children: label }) }));
|
|
191
|
+
} }));
|
|
192
|
+
});
|
|
193
|
+
ValidateFormSubmission.displayName =
|
|
194
|
+
'BookingForm.Actions.ValidateFormSubmission';
|
|
195
|
+
/**
|
|
196
|
+
* Actions namespace for BookingForm
|
|
197
|
+
*/
|
|
198
|
+
export const Actions = {
|
|
199
|
+
ValidateFormSubmission,
|
|
200
|
+
};
|
|
90
201
|
/**
|
|
91
202
|
* Data component that provides booking form data via render props.
|
|
92
203
|
* Use this when you need programmatic access to form state and actions.
|
|
@@ -95,12 +206,17 @@ Fields.displayName = 'BookingForm.Fields';
|
|
|
95
206
|
* @example
|
|
96
207
|
* ```tsx
|
|
97
208
|
* <BookingForm.Data formId="your-form-id">
|
|
98
|
-
* {({ formId, fields, formValues,
|
|
209
|
+
* {({ formId, fields, formValues, formRef, validate, setFormSubmission }) => (
|
|
99
210
|
* <div>
|
|
100
|
-
* <Form.Fields
|
|
211
|
+
* <Form.Fields
|
|
212
|
+
* fieldMap={fields}
|
|
213
|
+
* rowGapClassname="gap-y-4"
|
|
214
|
+
* columnGapClassname="gap-x-2"
|
|
215
|
+
* formRef={formRef}
|
|
216
|
+
* />
|
|
101
217
|
* <button onClick={async () => {
|
|
102
|
-
* const
|
|
103
|
-
* if (
|
|
218
|
+
* const isValid = await validate();
|
|
219
|
+
* if (isValid) {
|
|
104
220
|
* // Store current form values before submission
|
|
105
221
|
* setFormSubmission(formValues);
|
|
106
222
|
* }
|
|
@@ -110,17 +226,35 @@ Fields.displayName = 'BookingForm.Fields';
|
|
|
110
226
|
* </div>
|
|
111
227
|
* )}
|
|
112
228
|
* </BookingForm.Data>
|
|
229
|
+
*
|
|
230
|
+
* // With serviceIds
|
|
231
|
+
* <BookingForm.Data
|
|
232
|
+
* formId="your-form-id"
|
|
233
|
+
* serviceIds={['service-1', 'service-2']}
|
|
234
|
+
* >
|
|
235
|
+
* {({ fields, formRef }) => (
|
|
236
|
+
* <Form.Fields fieldMap={fields} formRef={formRef} />
|
|
237
|
+
* )}
|
|
238
|
+
* </BookingForm.Data>
|
|
113
239
|
* ```
|
|
114
240
|
*/
|
|
115
241
|
export function Data(props) {
|
|
116
|
-
const { formId, additionalMetadata, fieldMap, children } = props;
|
|
117
|
-
return (_jsx(CoreBookingForm.Root, { formId: formId, additionalMetadata: additionalMetadata, fieldMap: fieldMap, children: ({ formId: id, fields, formValues, formSubmission, setFormSubmission, validateFormSubmission, }) => children({
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
242
|
+
const { formId, serviceIds, additionalMetadata, fieldMap, children, loadErrorFallback, } = props;
|
|
243
|
+
return (_jsx(BookingFormErrorBoundary, { fallback: loadErrorFallback, children: _jsx(CoreBookingForm.Root, { formId: formId, serviceIds: serviceIds, additionalMetadata: additionalMetadata, fieldMap: fieldMap, children: ({ formId: id, fields, formValues, formSubmission, setFormSubmission, validateFormSubmission, formRef, }) => children({
|
|
244
|
+
formId: id,
|
|
245
|
+
fields,
|
|
246
|
+
formValues,
|
|
247
|
+
formSubmission,
|
|
248
|
+
setFormSubmission,
|
|
249
|
+
formRef,
|
|
250
|
+
validate: validateFormSubmission,
|
|
251
|
+
}) }) }));
|
|
125
252
|
}
|
|
126
253
|
Data.displayName = 'BookingForm.Data';
|
|
254
|
+
// ============================================================================
|
|
255
|
+
// Error Handling
|
|
256
|
+
// ============================================================================
|
|
257
|
+
/**
|
|
258
|
+
* Re-export LoadError component and utilities for error handling
|
|
259
|
+
*/
|
|
260
|
+
export { LoadError, isBookingFormConfigurationError, BookingFormConfigurationError, } from '../core/booking-form/BookingForm.js';
|