@maestro-js/form 1.0.0-alpha.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/dist/index.js ADDED
@@ -0,0 +1,888 @@
1
+ // src/form-root.tsx
2
+ import React2 from "react";
3
+
4
+ // src/compose-refs.ts
5
+ import * as React from "react";
6
+ function setRef(ref, value) {
7
+ if (typeof ref === "function") {
8
+ ref(value);
9
+ } else if (ref !== null && ref !== void 0) {
10
+ ;
11
+ ref.current = value;
12
+ }
13
+ }
14
+ function composeRefs(...refs) {
15
+ return (node) => refs.forEach((ref) => setRef(ref, node));
16
+ }
17
+
18
+ // src/form-root.tsx
19
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
20
+ function _HeadlessFormRoot({ id: inputId, children, as, method, ...props }, forwardedRef) {
21
+ const FormComponent = as ?? "form";
22
+ const generatedId = React2.useId();
23
+ const id = inputId ?? generatedId;
24
+ const formRef = React2.useRef(null);
25
+ const serverErrors = useServerErrors(id);
26
+ const { csrfToken } = React2.useContext(GlobalContext);
27
+ const lastServerErrors = React2.useRef(serverErrors);
28
+ React2.useEffect(() => {
29
+ if (formRef?.current && lastServerErrors.current !== serverErrors) {
30
+ lastServerErrors.current = serverErrors;
31
+ formRef.current?.checkValidity();
32
+ }
33
+ });
34
+ const isGET = method === "get" || method === "GET" || !method;
35
+ return /* @__PURE__ */ jsx(formContext.Provider, { value: { id }, children: /* @__PURE__ */ jsxs(
36
+ FormComponent,
37
+ {
38
+ ...props,
39
+ id,
40
+ method,
41
+ ref: composeRefs(formRef, forwardedRef),
42
+ onSubmit: (e) => {
43
+ if (isGET) {
44
+ if (!formRef.current?.checkValidity()) {
45
+ e.preventDefault();
46
+ e.stopPropagation();
47
+ }
48
+ }
49
+ },
50
+ children: [
51
+ isGET ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
52
+ /* @__PURE__ */ jsx("input", { type: "hidden", name: "__formId", value: id }),
53
+ /* @__PURE__ */ jsx("input", { type: "hidden", name: "__csrfToken", value: csrfToken })
54
+ ] }),
55
+ children
56
+ ]
57
+ }
58
+ ) });
59
+ }
60
+ var HeadlessFormRoot = React2.forwardRef(_HeadlessFormRoot);
61
+ var formContext = React2.createContext({ id: "" });
62
+ function GlobalProvider({ useServerErrors: useServerErrors2, children, csrfToken }) {
63
+ return /* @__PURE__ */ jsx(GlobalContext.Provider, { value: { useServerErrors: useServerErrors2, csrfToken }, children });
64
+ }
65
+ var GlobalContext = React2.createContext({
66
+ useServerErrors() {
67
+ throw new Error("'GlobalContext not found. Please wrap your site with GlobalProvider");
68
+ }
69
+ });
70
+ function useServerErrors(formId) {
71
+ const { useServerErrors: useServerErrors2 } = React2.useContext(GlobalContext);
72
+ const context = React2.useContext(formContext);
73
+ const serverErrors = useServerErrors2(formId ?? context.id);
74
+ return serverErrors ?? EMPTY_ARR;
75
+ }
76
+ var EMPTY_ARR = [];
77
+
78
+ // src/hidden-file-input.tsx
79
+ import React3 from "react";
80
+ import { jsx as jsx2 } from "react/jsx-runtime";
81
+ var HiddenFileInput = React3.forwardRef(({ value, name, form, isRequired }, ref) => {
82
+ const fileInputRef = React3.useRef(null);
83
+ React3.useEffect(() => {
84
+ if (fileInputRef.current) {
85
+ const dt = new DataTransfer();
86
+ if (Array.isArray(value)) {
87
+ value.forEach((v) => dt.items.add(v));
88
+ } else if (value) {
89
+ dt.items.add(value);
90
+ }
91
+ fileInputRef.current.files = dt.files;
92
+ }
93
+ }, [value]);
94
+ return /* @__PURE__ */ jsx2(
95
+ "input",
96
+ {
97
+ style: { display: "none" },
98
+ type: "file",
99
+ tabIndex: -1,
100
+ "aria-hidden": "true",
101
+ onChange: () => {
102
+ },
103
+ ref: composeRefs(fileInputRef, ref),
104
+ name,
105
+ form,
106
+ "aria-required": isRequired,
107
+ required: isRequired
108
+ }
109
+ );
110
+ });
111
+ HiddenFileInput.displayName = "HiddenFileInput";
112
+
113
+ // src/hidden-input.tsx
114
+ import React4 from "react";
115
+ import { jsx as jsx3 } from "react/jsx-runtime";
116
+ var HiddenInput = React4.forwardRef(({ value, name, form, isRequired, type, min, max }, ref) => {
117
+ return /* @__PURE__ */ jsx3(
118
+ "input",
119
+ {
120
+ style: {
121
+ border: 0,
122
+ clip: "rect(0 0 0 0)",
123
+ clipPath: "inset(50%)",
124
+ height: "1px",
125
+ margin: "-1px",
126
+ overflow: "hidden",
127
+ padding: 0,
128
+ position: "absolute",
129
+ width: "1px",
130
+ whiteSpace: "nowrap"
131
+ },
132
+ "aria-required": isRequired,
133
+ required: isRequired,
134
+ tabIndex: -1,
135
+ "aria-hidden": "true",
136
+ value,
137
+ onChange: () => {
138
+ },
139
+ ref,
140
+ name,
141
+ form,
142
+ type: type ?? "text",
143
+ min,
144
+ max
145
+ }
146
+ );
147
+ });
148
+ HiddenInput.displayName = "HiddenInput";
149
+
150
+ // src/hidden-select.tsx
151
+ import React5 from "react";
152
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
153
+ var HiddenSelect = React5.forwardRef(({ value, name, form, isRequired, children, autoComplete, onChange, label }, ref) => {
154
+ return /* @__PURE__ */ jsx4(
155
+ "div",
156
+ {
157
+ style: {
158
+ border: 0,
159
+ clip: "rect(0 0 0 0)",
160
+ clipPath: "inset(50%)",
161
+ height: "1px",
162
+ margin: "-1px",
163
+ overflow: "hidden",
164
+ padding: 0,
165
+ position: "absolute",
166
+ width: "1px",
167
+ whiteSpace: "nowrap"
168
+ },
169
+ children: /* @__PURE__ */ jsxs2("label", { children: [
170
+ label,
171
+ /* @__PURE__ */ jsxs2(
172
+ "select",
173
+ {
174
+ "aria-required": isRequired,
175
+ required: isRequired,
176
+ tabIndex: -1,
177
+ "aria-hidden": "true",
178
+ value,
179
+ ref,
180
+ name,
181
+ form,
182
+ autoComplete,
183
+ onChange,
184
+ children: [
185
+ /* @__PURE__ */ jsx4("option", { value: "" }),
186
+ children
187
+ ]
188
+ }
189
+ )
190
+ ] })
191
+ }
192
+ );
193
+ });
194
+ HiddenSelect.displayName = "HiddenSelect";
195
+
196
+ // src/use-controlled-state.ts
197
+ import React6 from "react";
198
+ function useControlledState(value, defaultValue, onChange) {
199
+ let [stateValue, setStateValue] = React6.useState(value || defaultValue);
200
+ let isControlledRef = React6.useRef(value !== void 0);
201
+ let isControlled = value !== void 0;
202
+ React6.useEffect(() => {
203
+ let wasControlled = isControlledRef.current;
204
+ if (wasControlled !== isControlled) {
205
+ console.warn(
206
+ `WARN: A component changed from ${wasControlled ? "controlled" : "uncontrolled"} to ${isControlled ? "controlled" : "uncontrolled"}.`
207
+ );
208
+ }
209
+ isControlledRef.current = isControlled;
210
+ }, [isControlled]);
211
+ let currentValue = isControlled ? value : stateValue;
212
+ let setValue = React6.useCallback(
213
+ (value2, ...args) => {
214
+ let onChangeCaller = (value3, ...onChangeArgs) => {
215
+ if (onChange) {
216
+ if (!Object.is(currentValue, value3)) {
217
+ onChange(value3, ...onChangeArgs);
218
+ }
219
+ }
220
+ if (!isControlled) {
221
+ currentValue = value3;
222
+ }
223
+ };
224
+ if (typeof value2 === "function") {
225
+ console.warn(
226
+ "We can not support a function callback. See Github Issues for details https://github.com/adobe/react-spectrum/issues/2320"
227
+ );
228
+ let updateFunction = (oldValue, ...functionArgs) => {
229
+ let interceptedValue = value2(isControlled ? currentValue : oldValue, ...functionArgs);
230
+ onChangeCaller(interceptedValue, ...args);
231
+ if (!isControlled) {
232
+ return interceptedValue;
233
+ }
234
+ return oldValue;
235
+ };
236
+ setStateValue(updateFunction);
237
+ } else {
238
+ if (!isControlled) {
239
+ setStateValue(value2);
240
+ }
241
+ onChangeCaller(value2, ...args);
242
+ }
243
+ },
244
+ [isControlled, currentValue, onChange]
245
+ );
246
+ return [currentValue, setValue];
247
+ }
248
+
249
+ // src/use-form.ts
250
+ import React8 from "react";
251
+
252
+ // src/use-stable-accessor.ts
253
+ import React7 from "react";
254
+ function useStableAccessor(value) {
255
+ const initialPersisterKey = React7.useRef({ value });
256
+ const getValue = React7.useCallback(() => {
257
+ return initialPersisterKey.current.value;
258
+ }, [initialPersisterKey]);
259
+ initialPersisterKey.current.value = value;
260
+ return getValue;
261
+ }
262
+
263
+ // src/use-form.ts
264
+ function useTopLevelFormValidation(formElement) {
265
+ const getForm = useStableAccessor(formElement);
266
+ const revalidate = React8.useCallback(
267
+ (field) => {
268
+ const formRef = getForm();
269
+ const formElement2 = typeof formRef === "string" ? document.getElementById(formRef) : formRef;
270
+ const form = formElement2 && formElement2 instanceof HTMLFormElement ? formElement2 : null;
271
+ const elements = form ? [...form.elements] : [];
272
+ const relevantElements = elements.filter((e) => !field || e.attributes.getNamedItem("name")?.value.startsWith(field));
273
+ relevantElements.forEach((e) => e.dispatchEvent(new Event("change")));
274
+ },
275
+ [getForm]
276
+ );
277
+ return { revalidate };
278
+ }
279
+
280
+ // src/use-reset.ts
281
+ import React10 from "react";
282
+
283
+ // src/use-effect-event.ts
284
+ import React9 from "react";
285
+ function useEffectEvent(fn) {
286
+ const ref = React9.useRef(null);
287
+ useIsomorphicLayoutEffect(() => {
288
+ ref.current = fn;
289
+ }, [fn]);
290
+ return React9.useCallback((...args) => {
291
+ const f = ref.current;
292
+ return f(...args);
293
+ }, []);
294
+ }
295
+ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? React9.useLayoutEffect : React9.useEffect;
296
+
297
+ // src/use-reset.ts
298
+ function useReset(ref, initialValue, onReset) {
299
+ let resetValue = React10.useRef(initialValue);
300
+ let handleReset = useEffectEvent(() => {
301
+ if (onReset) {
302
+ onReset(resetValue.current);
303
+ }
304
+ });
305
+ React10.useEffect(() => {
306
+ let form = ref?.current?.form;
307
+ form?.addEventListener("reset", handleReset);
308
+ return () => {
309
+ form?.removeEventListener("reset", handleReset);
310
+ };
311
+ }, [ref, handleReset]);
312
+ }
313
+
314
+ // src/use-validation.ts
315
+ import React12 from "react";
316
+
317
+ // src/use-form-validation-state.ts
318
+ import React11 from "react";
319
+ var VALID_VALIDITY_STATE = {
320
+ badInput: false,
321
+ customError: false,
322
+ patternMismatch: false,
323
+ rangeOverflow: false,
324
+ rangeUnderflow: false,
325
+ stepMismatch: false,
326
+ tooLong: false,
327
+ tooShort: false,
328
+ typeMismatch: false,
329
+ valueMissing: false,
330
+ valid: true
331
+ };
332
+ var CUSTOM_VALIDITY_STATE = {
333
+ ...VALID_VALIDITY_STATE,
334
+ customError: true,
335
+ valid: false
336
+ };
337
+ var DEFAULT_VALIDATION_RESULT = {
338
+ isInvalid: false,
339
+ validationDetails: VALID_VALIDITY_STATE,
340
+ validationErrors: []
341
+ };
342
+ function useFormValidationState(props) {
343
+ let { isInvalid, name, value, builtinValidation, validate } = props;
344
+ const controlledError = isInvalid ? {
345
+ isInvalid: true,
346
+ validationErrors: [],
347
+ validationDetails: CUSTOM_VALIDITY_STATE
348
+ } : null;
349
+ let clientError = React11.useMemo(() => {
350
+ if (validate) {
351
+ return getValidationResult(runValidate(validate, value));
352
+ } else if (props.isRequired && (value === null || value === void 0 || value === "")) {
353
+ return {
354
+ isInvalid: true,
355
+ validationErrors: ["This field is required"],
356
+ validationDetails: {
357
+ ...VALID_VALIDITY_STATE,
358
+ valueMissing: true,
359
+ valid: false
360
+ }
361
+ };
362
+ } else {
363
+ return null;
364
+ }
365
+ }, [validate, value]);
366
+ if (builtinValidation?.validationDetails.valid) {
367
+ builtinValidation = void 0;
368
+ }
369
+ const serverErrors = useServerErrors(props.form);
370
+ const serverErrorMessages = React11.useMemo(() => {
371
+ if (name) {
372
+ return Array.isArray(name) ? name.flatMap((name2) => serverErrors.filter((s) => s.path === name2).map((s) => s.message)) : serverErrors.filter((s) => s.path === name).map((s) => s.message);
373
+ }
374
+ return [];
375
+ }, [serverErrors, name]);
376
+ let [lastServerErrors, setLastServerErrors] = React11.useState(serverErrors);
377
+ let [isServerErrorCleared, setServerErrorCleared] = React11.useState(false);
378
+ if (serverErrors !== lastServerErrors) {
379
+ setLastServerErrors(serverErrors);
380
+ setServerErrorCleared(false);
381
+ }
382
+ let serverError = React11.useMemo(
383
+ () => getValidationResult(isServerErrorCleared ? [] : serverErrorMessages),
384
+ [isServerErrorCleared, serverErrorMessages]
385
+ );
386
+ let nextValidation = React11.useRef(DEFAULT_VALIDATION_RESULT);
387
+ let [currentValidity, setCurrentValidity] = React11.useState(DEFAULT_VALIDATION_RESULT);
388
+ let lastError = React11.useRef(DEFAULT_VALIDATION_RESULT);
389
+ let commitValidation = () => {
390
+ if (!commitQueued) {
391
+ return;
392
+ }
393
+ setCommitQueued(false);
394
+ let error = clientError || builtinValidation || nextValidation.current;
395
+ if (!isEqualValidation(error, lastError.current)) {
396
+ lastError.current = error;
397
+ setCurrentValidity(error);
398
+ }
399
+ };
400
+ let [commitQueued, setCommitQueued] = React11.useState(false);
401
+ React11.useEffect(commitValidation);
402
+ let realtimeValidation = controlledError || serverError || clientError || builtinValidation || DEFAULT_VALIDATION_RESULT;
403
+ let displayValidation = controlledError || serverError || currentValidity;
404
+ return {
405
+ realtimeValidation,
406
+ displayValidation,
407
+ updateValidation(value2) {
408
+ nextValidation.current = value2;
409
+ },
410
+ resetValidation() {
411
+ let error = DEFAULT_VALIDATION_RESULT;
412
+ if (!isEqualValidation(error, lastError.current)) {
413
+ lastError.current = error;
414
+ setCurrentValidity(error);
415
+ }
416
+ setCommitQueued(false);
417
+ setServerErrorCleared(true);
418
+ },
419
+ commitValidation() {
420
+ setCommitQueued(true);
421
+ setServerErrorCleared(true);
422
+ }
423
+ };
424
+ }
425
+ function asArray(v) {
426
+ if (!v) {
427
+ return [];
428
+ }
429
+ return Array.isArray(v) ? v : [v];
430
+ }
431
+ function runValidate(validate, value) {
432
+ if (typeof validate === "function") {
433
+ let e = validate(value);
434
+ if (e && typeof e !== "boolean") {
435
+ return asArray(e);
436
+ }
437
+ }
438
+ return [];
439
+ }
440
+ function getValidationResult(errors) {
441
+ return errors.length ? {
442
+ isInvalid: true,
443
+ validationErrors: errors,
444
+ validationDetails: CUSTOM_VALIDITY_STATE
445
+ } : null;
446
+ }
447
+ function isEqualValidation(a, b) {
448
+ if (a === b) {
449
+ return true;
450
+ }
451
+ return !!a && !!b && a.isInvalid === b.isInvalid && a.validationErrors.length === b.validationErrors.length && a.validationErrors.every((a2, i) => a2 === b.validationErrors[i]) && Object.entries(a.validationDetails).every(([k, v]) => b.validationDetails[k] === v);
452
+ }
453
+
454
+ // src/use-validation.ts
455
+ function useValidation(props, ref) {
456
+ const state = useFormValidationState(props);
457
+ let { focus } = props;
458
+ useIsomorphicLayoutEffect2(() => {
459
+ if (ref?.current) {
460
+ let errorMessage = state.realtimeValidation.isInvalid ? state.realtimeValidation.validationErrors.join(" ") || "Invalid value." : "";
461
+ ref.current.setCustomValidity(errorMessage);
462
+ if (!ref.current.hasAttribute("title")) {
463
+ ref.current.title = "";
464
+ }
465
+ if (!state.realtimeValidation.isInvalid) {
466
+ state.updateValidation(getNativeValidity(ref.current));
467
+ }
468
+ }
469
+ });
470
+ let onReset = useEffectEvent(() => {
471
+ state.resetValidation();
472
+ });
473
+ let onInvalid = useEffectEvent((e) => {
474
+ if (!state.displayValidation.isInvalid) {
475
+ state.commitValidation();
476
+ }
477
+ let form = ref?.current?.form;
478
+ if (!e.defaultPrevented && ref && form && getFirstInvalidInput(form) === ref.current) {
479
+ if (focus) {
480
+ focus();
481
+ } else {
482
+ ref.current?.focus();
483
+ }
484
+ }
485
+ e.preventDefault();
486
+ });
487
+ let onChange = useEffectEvent(() => {
488
+ state.commitValidation();
489
+ });
490
+ React12.useEffect(() => {
491
+ let input = ref?.current;
492
+ if (!input) {
493
+ return;
494
+ }
495
+ let form = input.form;
496
+ input.addEventListener("invalid", onInvalid);
497
+ input.addEventListener("change", onChange);
498
+ form?.addEventListener("reset", onReset);
499
+ return () => {
500
+ input.removeEventListener("invalid", onInvalid);
501
+ input.removeEventListener("change", onChange);
502
+ form?.removeEventListener("reset", onReset);
503
+ };
504
+ }, [ref, onInvalid, onChange, onReset]);
505
+ return {
506
+ commitValidation: state.commitValidation,
507
+ ...state.displayValidation,
508
+ validationMessage: state.displayValidation.validationErrors[0] ?? (state.displayValidation.isInvalid ? "invalid" : "")
509
+ };
510
+ }
511
+ var useIsomorphicLayoutEffect2 = typeof window !== "undefined" ? React12.useLayoutEffect : React12.useEffect;
512
+ function getValidity(input) {
513
+ let validity = input.validity;
514
+ return {
515
+ badInput: validity.badInput,
516
+ customError: validity.customError,
517
+ patternMismatch: validity.patternMismatch,
518
+ rangeOverflow: validity.rangeOverflow,
519
+ rangeUnderflow: validity.rangeUnderflow,
520
+ stepMismatch: validity.stepMismatch,
521
+ tooLong: validity.tooLong,
522
+ tooShort: validity.tooShort,
523
+ typeMismatch: validity.typeMismatch,
524
+ valueMissing: validity.valueMissing,
525
+ valid: validity.valid
526
+ };
527
+ }
528
+ function getNativeValidity(input) {
529
+ return {
530
+ isInvalid: !input.validity.valid,
531
+ validationDetails: getValidity(input),
532
+ validationErrors: input.validationMessage ? [input.validationMessage] : []
533
+ };
534
+ }
535
+ function getFirstInvalidInput(form) {
536
+ for (let i = 0; i < form.elements.length; i++) {
537
+ let element = form.elements[i];
538
+ if (!element.validity.valid) {
539
+ return element;
540
+ }
541
+ }
542
+ return null;
543
+ }
544
+
545
+ // src/use-warning-state.ts
546
+ import React13 from "react";
547
+ var VALID_VALIDITY_STATE2 = {
548
+ badInput: false,
549
+ customError: false,
550
+ patternMismatch: false,
551
+ rangeOverflow: false,
552
+ rangeUnderflow: false,
553
+ stepMismatch: false,
554
+ tooLong: false,
555
+ tooShort: false,
556
+ typeMismatch: false,
557
+ valueMissing: false,
558
+ valid: true
559
+ };
560
+ var CUSTOM_VALIDITY_STATE2 = {
561
+ ...VALID_VALIDITY_STATE2,
562
+ customError: true,
563
+ valid: false
564
+ };
565
+ var DEFAULT_VALIDATION_RESULT2 = {
566
+ isInvalid: false,
567
+ validationDetails: VALID_VALIDITY_STATE2,
568
+ validationErrors: []
569
+ };
570
+ function useWarningState(props) {
571
+ let { isInvalid, value, validate } = props;
572
+ const controlledError = isInvalid ? {
573
+ isInvalid: true,
574
+ validationErrors: [],
575
+ validationDetails: CUSTOM_VALIDITY_STATE2
576
+ } : null;
577
+ let clientError = React13.useMemo(() => {
578
+ if (validate) {
579
+ return getValidationResult2(runValidate2(validate, value));
580
+ } else {
581
+ return null;
582
+ }
583
+ }, [validate, value]);
584
+ let nextValidation = React13.useRef(DEFAULT_VALIDATION_RESULT2);
585
+ let [currentValidity, setCurrentValidity] = React13.useState(DEFAULT_VALIDATION_RESULT2);
586
+ let lastError = React13.useRef(DEFAULT_VALIDATION_RESULT2);
587
+ let commitValidation = () => {
588
+ if (!commitQueued) {
589
+ return;
590
+ }
591
+ setCommitQueued(false);
592
+ let error = clientError || nextValidation.current;
593
+ if (!isEqualValidation2(error, lastError.current)) {
594
+ lastError.current = error;
595
+ setCurrentValidity(error);
596
+ }
597
+ };
598
+ let [commitQueued, setCommitQueued] = React13.useState(false);
599
+ React13.useEffect(commitValidation);
600
+ let realtimeValidation = controlledError || clientError || DEFAULT_VALIDATION_RESULT2;
601
+ let displayValidation = controlledError || currentValidity;
602
+ return {
603
+ realtimeValidation,
604
+ displayValidation,
605
+ updateValidation(value2) {
606
+ nextValidation.current = value2;
607
+ },
608
+ resetValidation() {
609
+ let error = DEFAULT_VALIDATION_RESULT2;
610
+ if (!isEqualValidation2(error, lastError.current)) {
611
+ lastError.current = error;
612
+ setCurrentValidity(error);
613
+ }
614
+ setCommitQueued(false);
615
+ },
616
+ commitValidation() {
617
+ setCommitQueued(true);
618
+ },
619
+ warningMessage: displayValidation.validationErrors[0] ?? (displayValidation.isInvalid ? "invalid" : "")
620
+ };
621
+ }
622
+ function asArray2(v) {
623
+ if (!v) {
624
+ return [];
625
+ }
626
+ return Array.isArray(v) ? v : [v];
627
+ }
628
+ function runValidate2(validate, value) {
629
+ if (typeof validate === "function") {
630
+ let e = validate(value);
631
+ if (e && typeof e !== "boolean") {
632
+ return asArray2(e);
633
+ }
634
+ }
635
+ return [];
636
+ }
637
+ function getValidationResult2(errors) {
638
+ return errors.length ? {
639
+ isInvalid: true,
640
+ validationErrors: errors,
641
+ validationDetails: CUSTOM_VALIDITY_STATE2
642
+ } : null;
643
+ }
644
+ function isEqualValidation2(a, b) {
645
+ if (a === b) {
646
+ return true;
647
+ }
648
+ return !!a && !!b && a.isInvalid === b.isInvalid && a.validationErrors.length === b.validationErrors.length && a.validationErrors.every((a2, i) => a2 === b.validationErrors[i]) && Object.entries(a.validationDetails).every(([k, v]) => b.validationDetails[k] === v);
649
+ }
650
+
651
+ // src/standard-schema-helpers.ts
652
+ import set2 from "lodash.set";
653
+
654
+ // src/json-schema-helpers.ts
655
+ import set from "lodash.set";
656
+ import get from "lodash.get";
657
+ function parseJsonSchemaFormData(schema, formData) {
658
+ const flat = flattenJsonSchema(schema);
659
+ const booleans = flat.filter((f) => resolvedType(f.schema) === "boolean");
660
+ const arrays = flat.filter((f) => resolvedType(f.schema) === "array" && !f.optional && !f.path.includes(".*."));
661
+ const keys = [...arrays.map((a) => a.path), ...new Set(formData.keys())];
662
+ const rawEntries = keys.filter((key) => key !== "__formId").map((key) => {
663
+ const match = flat.find((f) => formPathsMatch(key, f.path));
664
+ return [key, coerceFormValue(match?.schema, formData.getAll(key))];
665
+ });
666
+ const obj = {};
667
+ rawEntries.forEach(([key, value]) => set(obj, key, value));
668
+ booleans.forEach((p) => {
669
+ const paths = resolveWildcardPath(p.path, obj);
670
+ paths.forEach((path) => {
671
+ if (get(obj, path) === void 0) {
672
+ set(obj, path, false);
673
+ }
674
+ });
675
+ });
676
+ return obj;
677
+ }
678
+ function flattenJsonSchema(schema) {
679
+ return flattenJsonSchemaHelper(schema, "", false);
680
+ }
681
+ function flattenJsonSchemaHelper(schema, prefix, optional) {
682
+ const type = resolvedType(schema);
683
+ const results = [];
684
+ const properties = schema.properties;
685
+ const items = schema.items;
686
+ const required = schema.required ?? [];
687
+ if (type === "object" && properties) {
688
+ for (const [key, child] of Object.entries(properties)) {
689
+ const path = prefix ? `${prefix}.${key}` : key;
690
+ const isOptional = !required.includes(key);
691
+ results.push({ path, schema: child, optional: isOptional });
692
+ results.push(...flattenJsonSchemaHelper(child, path, isOptional));
693
+ }
694
+ } else if (type === "array" && items) {
695
+ const path = prefix ? `${prefix}.*` : "*";
696
+ results.push({ path, schema: items, optional: false });
697
+ results.push(...flattenJsonSchemaHelper(items, path, false));
698
+ }
699
+ return results;
700
+ }
701
+ function coerceFormValue(schema, formValues) {
702
+ if (formValues.length && formValues.every((f) => f instanceof File)) {
703
+ if (formValues.every((f) => f instanceof File && f.name === "" && f.size === 0)) {
704
+ return schema && resolvedType(schema) === "array" ? [] : null;
705
+ }
706
+ return schema && resolvedType(schema) === "array" ? formValues : formValues[0];
707
+ }
708
+ if (!schema) {
709
+ return formValues.length === 1 ? formValues[0] : formValues;
710
+ }
711
+ const type = resolvedType(schema);
712
+ const items = schema.items;
713
+ if (type === "array") {
714
+ const rawValue = formValues;
715
+ if (rawValue.length === 1) {
716
+ if (rawValue[0] === "") return [];
717
+ try {
718
+ const parsed = JSON.parse(rawValue[0]);
719
+ if (Array.isArray(parsed)) return parsed;
720
+ } catch {
721
+ }
722
+ }
723
+ if (rawValue.length === 0) return isNullable(schema) ? null : [];
724
+ return items ? rawValue.map((v) => coerceScalar(v, items)) : rawValue;
725
+ }
726
+ return coerceScalar(formValues[0], schema);
727
+ }
728
+ function coerceScalar(value, schema) {
729
+ if (value === "" && isNullable(schema)) return null;
730
+ const type = resolvedType(schema);
731
+ if (type === "number" || type === "integer") {
732
+ return value === "" ? null : Number(value);
733
+ } else if (type === "boolean") {
734
+ return value === "true";
735
+ } else if (type === "string") {
736
+ return value;
737
+ } else {
738
+ try {
739
+ return JSON.parse(value);
740
+ } catch {
741
+ return value || null;
742
+ }
743
+ }
744
+ }
745
+ function resolvedType(schema) {
746
+ const type = schema.type;
747
+ if (Array.isArray(type)) {
748
+ return type.find((t) => t !== "null") ?? "string";
749
+ }
750
+ return type;
751
+ }
752
+ function isNullable(schema) {
753
+ if (schema.nullable === true) return true;
754
+ const type = schema.type;
755
+ if (Array.isArray(type)) {
756
+ return type.includes("null");
757
+ }
758
+ return type === "null";
759
+ }
760
+ function resolveWildcardPath(path, obj) {
761
+ if (path.includes(".*.")) {
762
+ const length = get(obj, path.slice(0, path.indexOf(".*.")), []).length;
763
+ const indices = Array.from({ length }, (_, i) => i);
764
+ const newPaths = indices.map((i) => path.replace(".*.", `.${i}.`));
765
+ return newPaths.flatMap((p) => resolveWildcardPath(p, obj));
766
+ }
767
+ return [path];
768
+ }
769
+ function formPathsMatch(path1, path2) {
770
+ const segments1 = path1.split(".");
771
+ const segments2 = path2.split(".");
772
+ return segments1.length === segments2.length && segments1.every((s1, i) => s1 === "*" || segments2[i] === "*" || s1 === segments2[i]);
773
+ }
774
+
775
+ // src/standard-schema-helpers.ts
776
+ function parseFormDataClientSide(schema, formData, options) {
777
+ const formId = formData.get("__formId");
778
+ const jsonSchemaParsed = parseJsonSchemaFormData(schema["~standard"].jsonSchema.input({ target: "openapi-3.0" }), formData);
779
+ const files = findFiles(jsonSchemaParsed);
780
+ files.forEach((f) => set2(jsonSchemaParsed, f.path.join("."), "https://fakeurlforformuploading.com"));
781
+ const safeParsed = schema["~standard"].validate(jsonSchemaParsed, options);
782
+ if (isPromise(safeParsed)) {
783
+ throw new Error("Async validation not supported");
784
+ }
785
+ if (safeParsed.issues?.length) {
786
+ console.log([...formData.entries()].map((e) => [e[0], e[1]]));
787
+ console.log(jsonSchemaParsed);
788
+ return {
789
+ issues: [...safeParsed.issues.map((i) => ({ ...i, path: i.path ? [...i.path] : i.path }))],
790
+ data: jsonSchemaParsed,
791
+ formData,
792
+ formId,
793
+ success: false,
794
+ fileEntries: files
795
+ };
796
+ } else {
797
+ return { data: jsonSchemaParsed, issues: void 0, formData, formId, success: true, fileEntries: files };
798
+ }
799
+ }
800
+ async function parseFormData(schema, formData, options) {
801
+ formData = await formData;
802
+ const formId = formData.get("__formId");
803
+ const parsed1 = parseFormDataClientSide(schema, formData, options);
804
+ if (!parsed1.success) {
805
+ return { ...parsed1, formData };
806
+ } else if (options?.handleFile || !parsed1.fileEntries.length) {
807
+ const handleFile = options?.handleFile;
808
+ const uploadedFiles = await Promise.all(
809
+ parsed1.fileEntries.map(async (file) => ({
810
+ key: file.path.join("."),
811
+ value: await handleFile(file.value, file.path.join("."), parsed1.data)
812
+ }))
813
+ );
814
+ const obj2 = { ...parsed1.data };
815
+ uploadedFiles.forEach(({ key, value }) => set2(obj2, key, value));
816
+ const safeParsed2 = schema["~standard"].validate(obj2, options);
817
+ if (isPromise(safeParsed2)) {
818
+ throw new Error("Async validation not supported");
819
+ }
820
+ if (!safeParsed2.issues?.length) {
821
+ return { data: obj2, issues: void 0, formData, formId, success: true };
822
+ } else {
823
+ console.log(obj2);
824
+ console.log("fileEntries", parsed1.fileEntries);
825
+ return {
826
+ issues: [...safeParsed2.issues.map((i) => ({ ...i, path: i.path ? [...i.path] : i.path }))],
827
+ data: obj2,
828
+ formData,
829
+ formId,
830
+ success: false
831
+ };
832
+ }
833
+ } else {
834
+ throw new Error("A file was submitted but no file handler was provider");
835
+ }
836
+ }
837
+ function isPromise(p) {
838
+ return p && typeof p === "object" && !!p["then"];
839
+ }
840
+ function findFiles(input) {
841
+ const results = [];
842
+ const visited = /* @__PURE__ */ new WeakSet();
843
+ function walk(value, path) {
844
+ if (value === null || typeof value !== "object") return;
845
+ if (visited.has(value)) return;
846
+ visited.add(value);
847
+ if (value instanceof File) {
848
+ results.push({ path, value });
849
+ return;
850
+ }
851
+ if (Array.isArray(value)) {
852
+ value.forEach((v, i) => walk(v, [...path, i]));
853
+ return;
854
+ }
855
+ if (value instanceof Date || value instanceof RegExp || value instanceof Map || value instanceof Set) return;
856
+ for (const [key, v] of Object.entries(value)) {
857
+ walk(v, [...path, key]);
858
+ }
859
+ }
860
+ walk(input, []);
861
+ return results;
862
+ }
863
+
864
+ // src/index.ts
865
+ function composeValidators(...fns) {
866
+ return (value) => {
867
+ const errors = fns.flatMap((f) => f ? f(value) : []).filter(Boolean);
868
+ return errors;
869
+ };
870
+ }
871
+ var HeadlessForm = Object.assign(HeadlessFormRoot, {
872
+ GlobalProvider,
873
+ useValidation,
874
+ useWarningState,
875
+ useReset,
876
+ useControlledState,
877
+ HiddenInput,
878
+ HiddenFileInput,
879
+ HiddenSelect,
880
+ useServerErrors,
881
+ parseFormDataClientSide,
882
+ parseFormData,
883
+ useTopLevelFormValidation,
884
+ composeValidators
885
+ });
886
+ export {
887
+ HeadlessForm
888
+ };