@wix/headless-bookings 0.0.56 → 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/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/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
|
@@ -1,58 +1,42 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Core BookingForm Component
|
|
4
|
+
*
|
|
5
|
+
* Provides low-level access to booking form functionality via render props.
|
|
6
|
+
* Wraps CoreForm.Root and provides merged field map with booking defaults.
|
|
7
|
+
*
|
|
8
|
+
* Supports two modes:
|
|
9
|
+
* 1. Client-side: Provide formId to load the form dynamically
|
|
10
|
+
* 2. SSR/SSG: Provide a pre-loaded form object for server-side rendering
|
|
11
|
+
*/
|
|
12
|
+
import * as React from 'react';
|
|
13
|
+
import { useMemo, useRef, useCallback } from 'react';
|
|
3
14
|
import { WixServices, useService } from '@wix/services-manager-react';
|
|
4
15
|
import { createServicesMap } from '@wix/services-manager';
|
|
5
|
-
import {
|
|
16
|
+
import { UniqueFieldSuffixContextProvider, } from '@wix/form-public';
|
|
17
|
+
import { BookingFormServiceDefinition, BookingFormService, BookingFormConfigurationError, BOOKING_FORM_NAMESPACE, extractFormIdFromConfig, } from '../../../services/booking-form/booking-form.js';
|
|
18
|
+
import { BookingServiceDefinition } from '../../../services/booking/booking.js';
|
|
6
19
|
import { FormServiceDefinition, FormService, } from '@wix/headless-forms/services';
|
|
7
|
-
// TODO: remove this when form will be updated to support optional field map
|
|
8
|
-
const DEFAULT_BOOKING_FIELD_MAP = {
|
|
9
|
-
TEXT_INPUT: () => null,
|
|
10
|
-
TEXT_AREA: () => null,
|
|
11
|
-
PHONE_INPUT: () => null,
|
|
12
|
-
EMAIL_INPUT: () => null,
|
|
13
|
-
NUMBER_INPUT: () => null,
|
|
14
|
-
CHECKBOX: () => null,
|
|
15
|
-
DROPDOWN: () => null,
|
|
16
|
-
DATE_PICKER: () => null,
|
|
17
|
-
SUBMIT_BUTTON: () => null,
|
|
18
|
-
RADIO_GROUP: () => null,
|
|
19
|
-
CHECKBOX_GROUP: () => null,
|
|
20
|
-
DATE_INPUT: () => null,
|
|
21
|
-
DATE_TIME_INPUT: () => null,
|
|
22
|
-
TIME_INPUT: () => null,
|
|
23
|
-
FILE_UPLOAD: () => null,
|
|
24
|
-
SIGNATURE: () => null,
|
|
25
|
-
TEXT: () => null,
|
|
26
|
-
MULTILINE_ADDRESS: () => null,
|
|
27
|
-
RATING_INPUT: () => null,
|
|
28
|
-
TAGS: () => null,
|
|
29
|
-
PRODUCT_LIST: () => null,
|
|
30
|
-
FIXED_PAYMENT: () => null,
|
|
31
|
-
PAYMENT_INPUT: () => null,
|
|
32
|
-
DONATION: () => null,
|
|
33
|
-
APPOINTMENT: () => null,
|
|
34
|
-
IMAGE_CHOICE: () => null,
|
|
35
|
-
};
|
|
36
20
|
// ============================================================================
|
|
37
21
|
// Component
|
|
38
22
|
// ============================================================================
|
|
39
23
|
/**
|
|
40
24
|
* Extracts the form ID from props (either directly or from form object)
|
|
25
|
+
* Returns undefined if formId should be extracted from services by the service layer
|
|
41
26
|
*/
|
|
42
27
|
function extractFormIdFromProps(props) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return formId;
|
|
28
|
+
// Convert props to config-like object for the utility
|
|
29
|
+
const config = props.form
|
|
30
|
+
? { form: props.form }
|
|
31
|
+
: props.formId
|
|
32
|
+
? { formId: props.formId }
|
|
33
|
+
: undefined;
|
|
34
|
+
const formId = extractFormIdFromConfig(config);
|
|
35
|
+
// If form was provided but we couldn't extract formId, that's an error
|
|
36
|
+
if (props.form && !formId) {
|
|
37
|
+
throw new Error('BookingForm: Could not extract form ID from provided form object.');
|
|
54
38
|
}
|
|
55
|
-
|
|
39
|
+
return formId;
|
|
56
40
|
}
|
|
57
41
|
/**
|
|
58
42
|
* Core component that provides booking form data via render props.
|
|
@@ -123,41 +107,75 @@ function extractFormIdFromProps(props) {
|
|
|
123
107
|
* ```
|
|
124
108
|
*/
|
|
125
109
|
export function Root(props) {
|
|
126
|
-
const { additionalMetadata, fieldMap
|
|
110
|
+
const { serviceIds, additionalMetadata, fieldMap, children } = props;
|
|
127
111
|
// Determine if we have a pre-loaded form
|
|
128
112
|
const hasPreloadedForm = 'form' in props && props.form;
|
|
129
113
|
const preloadedForm = hasPreloadedForm ? props.form : null;
|
|
130
114
|
// Extract formId from props
|
|
131
|
-
const
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
115
|
+
const propsFormId = extractFormIdFromProps(props);
|
|
116
|
+
// Try to get BookingService if available (to extract formId and serviceIds from services)
|
|
117
|
+
let bookingService = null;
|
|
118
|
+
try {
|
|
119
|
+
bookingService = useService(BookingServiceDefinition);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// BookingService not available - that's ok if formId and serviceIds were provided explicitly
|
|
123
|
+
bookingService = null;
|
|
124
|
+
}
|
|
125
|
+
// Determine final formId: use props formId if provided, otherwise extract from services
|
|
126
|
+
let formId = propsFormId;
|
|
127
|
+
if (!formId && bookingService) {
|
|
128
|
+
const serviceSelections = bookingService.serviceSelections.get();
|
|
129
|
+
const services = serviceSelections.length
|
|
130
|
+
? serviceSelections.map((s) => s.service)
|
|
131
|
+
: [];
|
|
132
|
+
formId = services.find((s) => s.form?._id)?.form?._id;
|
|
133
|
+
}
|
|
134
|
+
// Determine final serviceIds: use props serviceIds if provided, otherwise extract from services
|
|
135
|
+
let resolvedServiceIds = serviceIds;
|
|
136
|
+
if (!resolvedServiceIds && bookingService) {
|
|
137
|
+
const serviceSelections = bookingService.serviceSelections.get();
|
|
138
|
+
resolvedServiceIds = serviceSelections
|
|
139
|
+
.map((s) => s.service._id)
|
|
140
|
+
.filter((id) => Boolean(id));
|
|
141
|
+
}
|
|
142
|
+
// Merge serviceIds into additionalMetadata
|
|
143
|
+
const mergedMetadata = {
|
|
144
|
+
...additionalMetadata,
|
|
145
|
+
...(resolvedServiceIds &&
|
|
146
|
+
resolvedServiceIds.length > 0 && { serviceIds: resolvedServiceIds }),
|
|
147
|
+
};
|
|
137
148
|
// Build formServiceConfig for FormService
|
|
138
|
-
// If we have a pre-loaded form, pass it directly
|
|
149
|
+
// If we have a pre-loaded form, pass it directly
|
|
150
|
+
// If we have formId, pass it with namespace and metadata
|
|
139
151
|
const formServiceConfig = useMemo(() => {
|
|
140
152
|
if (preloadedForm) {
|
|
141
153
|
return { form: preloadedForm };
|
|
142
154
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
155
|
+
// Only provide config if formId is available
|
|
156
|
+
if (formId) {
|
|
157
|
+
return {
|
|
158
|
+
formId,
|
|
159
|
+
namespace: BOOKING_FORM_NAMESPACE,
|
|
160
|
+
...(Object.keys(mergedMetadata).length > 0 && {
|
|
161
|
+
additionalMetadata: mergedMetadata,
|
|
162
|
+
}),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}, [formId, mergedMetadata, preloadedForm]);
|
|
149
166
|
// Build bookingFormServiceConfig
|
|
150
167
|
const bookingFormServiceConfig = useMemo(() => {
|
|
151
168
|
if (preloadedForm) {
|
|
152
169
|
return { form: preloadedForm };
|
|
153
170
|
}
|
|
154
|
-
|
|
155
|
-
|
|
171
|
+
// formId is optional - service will extract from services if not provided
|
|
172
|
+
return formId ? { formId, serviceIds } : undefined;
|
|
173
|
+
}, [formId, preloadedForm, serviceIds]);
|
|
156
174
|
// Both FormService and BookingFormService are added to the same WixServices context
|
|
157
175
|
// This allows BookingFormService to access FormService via getService internally
|
|
158
176
|
return (_jsx(WixServices, { servicesMap: createServicesMap()
|
|
159
177
|
.addService(FormServiceDefinition, FormService, formServiceConfig)
|
|
160
|
-
.addService(BookingFormServiceDefinition, BookingFormService, bookingFormServiceConfig), children: _jsx(
|
|
178
|
+
.addService(BookingFormServiceDefinition, BookingFormService, bookingFormServiceConfig), children: _jsx(UniqueFieldSuffixContextProvider, { children: _jsx(BookingFormContent, { fields: fieldMap, children: children }) }) }));
|
|
161
179
|
}
|
|
162
180
|
/**
|
|
163
181
|
* Internal component that consumes BookingFormService and provides render props.
|
|
@@ -165,17 +183,139 @@ export function Root(props) {
|
|
|
165
183
|
*
|
|
166
184
|
* Note: BookingFormService internally accesses FormService to get formValues,
|
|
167
185
|
* so we only need to consume BookingFormService here.
|
|
186
|
+
*
|
|
187
|
+
* Also automatically syncs form submission to BookingService when available,
|
|
188
|
+
* so Booking.Actions.Book can check canBook() correctly.
|
|
189
|
+
*
|
|
190
|
+
* Note: BookingService must be available in a parent WixServices context
|
|
191
|
+
* (typically provided by Booking.Root) for the sync to work. If BookingService
|
|
192
|
+
* is not available, useService will throw. This is expected since Booking.Actions.Book
|
|
193
|
+
* requires Booking.Root to be in the component tree.
|
|
168
194
|
*/
|
|
169
|
-
function BookingFormContent({
|
|
195
|
+
function BookingFormContent({ fields, children, }) {
|
|
170
196
|
const bookingFormService = useService(BookingFormServiceDefinition);
|
|
197
|
+
// Try to get BookingService from parent context (provided by Booking.Root)
|
|
198
|
+
// Note: useService must be called unconditionally per React hooks rules.
|
|
199
|
+
// If BookingService is not available, useService will throw an error.
|
|
200
|
+
// This is expected - Booking.Actions.Book requires Booking.Root to be in the tree.
|
|
201
|
+
// We use a wrapper component pattern to handle this gracefully.
|
|
202
|
+
// For now, we'll always attempt to get it and let it throw if not available
|
|
203
|
+
// The user should wrap BookingForm with Booking.Root when using Booking.Actions.Book
|
|
204
|
+
const bookingService = useService(BookingServiceDefinition);
|
|
205
|
+
// Create internal ref for form handle
|
|
206
|
+
const formRef = useRef(null);
|
|
207
|
+
// Create validate action that uses the form ref
|
|
208
|
+
const validate = useCallback(() => {
|
|
209
|
+
return formRef.current?.validate() ?? Promise.resolve(false);
|
|
210
|
+
}, []);
|
|
211
|
+
// Wrap setFormSubmission to also sync to BookingService when available
|
|
212
|
+
const setFormSubmission = useCallback((formValues) => {
|
|
213
|
+
// Set in BookingFormService (always)
|
|
214
|
+
bookingFormService.setFormSubmission(formValues);
|
|
215
|
+
// Also set in BookingService if available (for canBook check)
|
|
216
|
+
// This allows Booking.Actions.Book to work correctly
|
|
217
|
+
if (bookingService) {
|
|
218
|
+
bookingService.actions.setFormSubmission(formValues);
|
|
219
|
+
}
|
|
220
|
+
}, [bookingFormService, bookingService]);
|
|
171
221
|
const formSubmission = bookingFormService.formSubmission.get();
|
|
172
222
|
const formValues = bookingFormService.formValues.get();
|
|
223
|
+
// Get formId from service (it extracts from services if not provided in config)
|
|
224
|
+
const resolvedFormId = bookingFormService.formId.get();
|
|
173
225
|
return children({
|
|
174
|
-
formId,
|
|
226
|
+
formId: resolvedFormId,
|
|
175
227
|
formValues,
|
|
176
228
|
formSubmission,
|
|
177
|
-
setFormSubmission
|
|
178
|
-
validateFormSubmission:
|
|
229
|
+
setFormSubmission,
|
|
230
|
+
validateFormSubmission: validate,
|
|
231
|
+
formRef,
|
|
179
232
|
fields,
|
|
180
233
|
});
|
|
181
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Core component that provides booking form actions via render props.
|
|
237
|
+
* Must be used within BookingForm.Root context.
|
|
238
|
+
*
|
|
239
|
+
* @component
|
|
240
|
+
* @example
|
|
241
|
+
* ```tsx
|
|
242
|
+
* <CoreBookingForm.Root formId="form-123">
|
|
243
|
+
* {({ fields }) => (
|
|
244
|
+
* <>
|
|
245
|
+
* <Form.Fields fieldMap={fields} />
|
|
246
|
+
* <CoreBookingForm.Actions>
|
|
247
|
+
* {({ validateFormSubmission, formId }) => (
|
|
248
|
+
* <button onClick={async () => {
|
|
249
|
+
* const result = await validateFormSubmission();
|
|
250
|
+
* console.log('Validation result:', result);
|
|
251
|
+
* }}>
|
|
252
|
+
* Validate Form
|
|
253
|
+
* </button>
|
|
254
|
+
* )}
|
|
255
|
+
* </CoreBookingForm.Actions>
|
|
256
|
+
* </>
|
|
257
|
+
* )}
|
|
258
|
+
* </CoreBookingForm.Root>
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
export function Actions(props) {
|
|
262
|
+
const bookingFormService = useService(BookingFormServiceDefinition);
|
|
263
|
+
const formSubmission = bookingFormService.formSubmission.get();
|
|
264
|
+
const formValues = bookingFormService.formValues.get();
|
|
265
|
+
const formId = bookingFormService.formId.get();
|
|
266
|
+
return props.children({
|
|
267
|
+
validateFormSubmission: bookingFormService.validateFormSubmission,
|
|
268
|
+
formId,
|
|
269
|
+
formValues,
|
|
270
|
+
formSubmission,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Component to display when BookingForm fails to load due to configuration errors.
|
|
275
|
+
* This component should be used in an error boundary to catch BookingFormConfigurationError.
|
|
276
|
+
*
|
|
277
|
+
* @component
|
|
278
|
+
* @example
|
|
279
|
+
* ```tsx
|
|
280
|
+
* import * as CoreBookingForm from '@wix/headless-bookings/react/core/booking-form';
|
|
281
|
+
*
|
|
282
|
+
* // Default usage with className
|
|
283
|
+
* <CoreBookingForm.LoadError error={error} className="text-destructive p-4" />
|
|
284
|
+
*
|
|
285
|
+
* // Custom content
|
|
286
|
+
* <CoreBookingForm.LoadError error={error}>
|
|
287
|
+
* <div className="error-container">
|
|
288
|
+
* <h3>Error Loading Form</h3>
|
|
289
|
+
* <p>Please try again later.</p>
|
|
290
|
+
* </div>
|
|
291
|
+
* </CoreBookingForm.LoadError>
|
|
292
|
+
*
|
|
293
|
+
* // With asChild for custom components
|
|
294
|
+
* <CoreBookingForm.LoadError error={error} asChild>
|
|
295
|
+
* <CustomErrorComponent />
|
|
296
|
+
* </CoreBookingForm.LoadError>
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
299
|
+
export const LoadError = React.forwardRef((props, ref) => {
|
|
300
|
+
const { error, asChild, children, className, ...otherProps } = props;
|
|
301
|
+
const errorData = { error, message: error.message };
|
|
302
|
+
if (asChild && React.isValidElement(children)) {
|
|
303
|
+
return React.cloneElement(children, {
|
|
304
|
+
ref,
|
|
305
|
+
className,
|
|
306
|
+
'data-testid': 'booking-form-load-error',
|
|
307
|
+
role: 'alert',
|
|
308
|
+
...errorData,
|
|
309
|
+
...otherProps,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return (_jsx("div", { ref: ref, className: className, "data-testid": "booking-form-load-error", role: "alert", ...otherProps, children: children ?? error.message }));
|
|
313
|
+
});
|
|
314
|
+
/**
|
|
315
|
+
* Type guard to check if an error is a BookingFormConfigurationError
|
|
316
|
+
*/
|
|
317
|
+
export function isBookingFormConfigurationError(error) {
|
|
318
|
+
return error instanceof BookingFormConfigurationError;
|
|
319
|
+
}
|
|
320
|
+
// Re-export the error class for convenience
|
|
321
|
+
export { BookingFormConfigurationError };
|
|
@@ -25,24 +25,44 @@ export interface ValidationResult {
|
|
|
25
25
|
/** Array of validation failure messages */
|
|
26
26
|
validationFailures: string[];
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Error thrown when BookingFormService cannot be initialized due to missing configuration.
|
|
30
|
+
* This occurs when:
|
|
31
|
+
* - No config (formId or form) was provided
|
|
32
|
+
* - AND the component is not wrapped with Booking.Root (no BookingService available)
|
|
33
|
+
*/
|
|
34
|
+
export declare class BookingFormConfigurationError extends Error {
|
|
35
|
+
constructor(message?: string);
|
|
36
|
+
}
|
|
28
37
|
/**
|
|
29
38
|
* Configuration for BookingFormService.
|
|
30
39
|
* Supports two modes:
|
|
31
40
|
* 1. Client-side loading: Provide formId to fetch the form dynamically
|
|
32
41
|
* 2. SSR/SSG: Provide a pre-loaded form object (must be from bookings namespace)
|
|
33
42
|
*
|
|
43
|
+
* Note: The config itself is optional. If not provided, the formId will be
|
|
44
|
+
* extracted from the selected service if available.
|
|
45
|
+
*
|
|
34
46
|
* @example
|
|
35
47
|
* ```tsx
|
|
36
48
|
* // Pattern 1: Client-side loading
|
|
37
49
|
* const config = { formId: 'form-123' };
|
|
38
50
|
*
|
|
51
|
+
* // Pattern 1b: Client-side loading with service IDs
|
|
52
|
+
* const config = { formId: 'form-123', serviceIds: ['service-1', 'service-2'] };
|
|
53
|
+
*
|
|
39
54
|
* // Pattern 2: Pre-loaded form (SSR/SSG)
|
|
40
55
|
* const config = { form: preloadedForm };
|
|
56
|
+
*
|
|
57
|
+
* // Pattern 3: No config (formId extracted from service)
|
|
58
|
+
* // config = undefined
|
|
41
59
|
* ```
|
|
42
60
|
*/
|
|
43
61
|
export type BookingFormServiceConfig = {
|
|
44
62
|
/** The form ID to load (client-side loading) */
|
|
45
63
|
formId: string;
|
|
64
|
+
/** Optional list of service IDs associated with this booking form */
|
|
65
|
+
serviceIds?: string[];
|
|
46
66
|
} | {
|
|
47
67
|
/** Pre-loaded form object (SSR/SSG) - must be from bookings namespace */
|
|
48
68
|
form: forms.Form;
|
|
@@ -79,18 +99,7 @@ export interface BookingFormServiceInternalAPI extends BookingFormServiceAPI {
|
|
|
79
99
|
/** Internal: Register validate callback (called by core component) */
|
|
80
100
|
_setValidateCallback: (callback: () => Promise<ValidationResult>) => void;
|
|
81
101
|
}
|
|
82
|
-
|
|
83
|
-
* Type guard to check if config has formId
|
|
84
|
-
*/
|
|
85
|
-
export declare function hasFormId(config: BookingFormServiceConfig): config is {
|
|
86
|
-
formId: string;
|
|
87
|
-
};
|
|
88
|
-
/**
|
|
89
|
-
* Type guard to check if config has pre-loaded form
|
|
90
|
-
*/
|
|
91
|
-
export declare function hasForm(config: BookingFormServiceConfig): config is {
|
|
92
|
-
form: forms.Form;
|
|
93
|
-
};
|
|
102
|
+
export { hasFormId, hasForm, extractFormIdFromForm, extractFormIdFromConfig, type FormIdConfig, } from './utils.js';
|
|
94
103
|
/**
|
|
95
104
|
* Service definition for BookingForm.
|
|
96
105
|
* Defines the contract that the BookingFormService must implement.
|
|
@@ -137,4 +146,4 @@ export declare const BookingFormService: import("@wix/services-definitions").Ser
|
|
|
137
146
|
__api: BookingFormServiceAPI;
|
|
138
147
|
__config: {};
|
|
139
148
|
isServiceDefinition?: boolean;
|
|
140
|
-
} & BookingFormServiceAPI, BookingFormServiceConfig>;
|
|
149
|
+
} & BookingFormServiceAPI, BookingFormServiceConfig | undefined>;
|
|
@@ -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
|
+
}
|