@railway-ts/use-form 0.1.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,989 @@
1
+ import { useCallback, useReducer, useMemo, useRef, useEffect } from 'react';
2
+ import { isErr, match } from '@railway-ts/pipelines/result';
3
+ import { validate, formatErrors } from '@railway-ts/pipelines/schema';
4
+
5
+ // src/useForm.ts
6
+
7
+ // src/utils.ts
8
+ var getValueByPath = (obj, path) => {
9
+ if (!path) return obj;
10
+ const normalizedPath = path.replace(/\[(\d+)\]/g, ".$1");
11
+ const parts = normalizedPath.split(".").filter(Boolean);
12
+ let current = obj;
13
+ for (const part of parts) {
14
+ if (current == null) return void 0;
15
+ current = current[part];
16
+ }
17
+ return current;
18
+ };
19
+ var setValueByPath = (obj, path, value) => {
20
+ if (!path) return value;
21
+ const normalizedPath = path.replace(/\[(\d+)\]/g, ".$1");
22
+ const parts = normalizedPath.split(".").filter(Boolean);
23
+ const result = Array.isArray(obj) ? [...obj] : { ...obj };
24
+ let currNew = Array.isArray(result) ? result : result;
25
+ let currOld = obj;
26
+ for (let i = 0; i < parts.length; i++) {
27
+ const keyStr = parts[i];
28
+ const isIndex = /^\d+$/.test(keyStr);
29
+ const key = isIndex ? Number(keyStr) : keyStr;
30
+ if (i === parts.length - 1) {
31
+ if (Array.isArray(currNew) && typeof key === "number") {
32
+ currNew[key] = value;
33
+ } else if (!Array.isArray(currNew) && typeof key === "string") {
34
+ currNew[key] = value;
35
+ }
36
+ break;
37
+ }
38
+ let nextOld = void 0;
39
+ if (currOld != null && typeof currOld === "object") {
40
+ if (Array.isArray(currOld) && typeof key === "number") {
41
+ nextOld = currOld[key];
42
+ } else if (!Array.isArray(currOld) && typeof key === "string") {
43
+ nextOld = currOld[key];
44
+ }
45
+ }
46
+ let nextNew;
47
+ if (nextOld == null) {
48
+ const nextIsIndex = /^\d+$/.test(parts[i + 1] ?? "");
49
+ nextNew = nextIsIndex ? [] : {};
50
+ } else {
51
+ nextNew = Array.isArray(nextOld) ? [...nextOld] : { ...nextOld };
52
+ }
53
+ if (Array.isArray(currNew) && typeof key === "number") {
54
+ currNew[key] = nextNew;
55
+ } else if (!Array.isArray(currNew) && typeof key === "string") {
56
+ currNew[key] = nextNew;
57
+ }
58
+ currNew = nextNew;
59
+ currOld = nextOld ?? (Array.isArray(nextNew) ? [] : {});
60
+ }
61
+ return result;
62
+ };
63
+ var isPathAffected = (path, changePath) => {
64
+ if (path === changePath) return true;
65
+ const normalizedPath = path.replace(/\[(\d+)\]/g, ".$1");
66
+ const normalizedChangePath = changePath.replace(/\[(\d+)\]/g, ".$1");
67
+ return normalizedPath === normalizedChangePath || normalizedPath.startsWith(`${normalizedChangePath}.`);
68
+ };
69
+ var collectFieldPaths = (obj, prefix = "") => {
70
+ const paths = [];
71
+ const isLeaf = (v) => v == null || typeof v !== "object" || v instanceof Date || v instanceof RegExp;
72
+ const visit = (value, path) => {
73
+ if (path) paths.push(path);
74
+ if (isLeaf(value)) return;
75
+ if (Array.isArray(value)) {
76
+ value.forEach((item, i) => visit(item, `${path}[${i}]`));
77
+ return;
78
+ }
79
+ const entries = Object.entries(value);
80
+ for (const [k, v] of entries) {
81
+ const childPath = path ? `${path}.${k}` : k;
82
+ visit(v, childPath);
83
+ }
84
+ };
85
+ visit(obj, prefix);
86
+ return paths;
87
+ };
88
+
89
+ // src/formReducer.ts
90
+ var formReducer = (state, action, initialValues) => {
91
+ switch (action.type) {
92
+ case "SET_FIELD_VALUE": {
93
+ const newValues = setValueByPath(
94
+ state.values,
95
+ action.field,
96
+ action.value
97
+ );
98
+ const newServerErrors = { ...state.serverErrors };
99
+ Object.keys(newServerErrors).forEach((errorPath) => {
100
+ if (isPathAffected(errorPath, action.field)) {
101
+ delete newServerErrors[errorPath];
102
+ }
103
+ });
104
+ return {
105
+ ...state,
106
+ values: newValues,
107
+ serverErrors: newServerErrors,
108
+ isDirty: true
109
+ };
110
+ }
111
+ case "SET_VALUES": {
112
+ const newServerErrors = { ...state.serverErrors };
113
+ Object.keys(action.values).forEach((field) => {
114
+ Object.keys(newServerErrors).forEach((errorPath) => {
115
+ if (isPathAffected(errorPath, field)) {
116
+ delete newServerErrors[errorPath];
117
+ }
118
+ });
119
+ });
120
+ return {
121
+ ...state,
122
+ values: { ...state.values, ...action.values },
123
+ serverErrors: newServerErrors,
124
+ isDirty: true
125
+ };
126
+ }
127
+ case "SET_FIELD_TOUCHED":
128
+ return {
129
+ ...state,
130
+ touched: {
131
+ ...state.touched,
132
+ [action.field]: action.isTouched
133
+ }
134
+ };
135
+ case "SET_CLIENT_ERRORS":
136
+ return {
137
+ ...state,
138
+ clientErrors: action.errors
139
+ };
140
+ case "SET_SERVER_ERRORS":
141
+ return {
142
+ ...state,
143
+ serverErrors: action.errors
144
+ };
145
+ case "CLEAR_SERVER_ERRORS":
146
+ return {
147
+ ...state,
148
+ serverErrors: {}
149
+ };
150
+ case "SET_SUBMITTING":
151
+ return {
152
+ ...state,
153
+ isSubmitting: action.isSubmitting
154
+ };
155
+ case "RESET_FORM":
156
+ return {
157
+ values: initialValues,
158
+ touched: {},
159
+ clientErrors: {},
160
+ serverErrors: {},
161
+ isSubmitting: false,
162
+ isDirty: false
163
+ };
164
+ case "MARK_ALL_TOUCHED": {
165
+ const allTouched = {};
166
+ action.fields.forEach((path) => {
167
+ allTouched[path] = true;
168
+ });
169
+ return {
170
+ ...state,
171
+ touched: {
172
+ ...state.touched,
173
+ ...allTouched
174
+ }
175
+ };
176
+ }
177
+ default:
178
+ return state;
179
+ }
180
+ };
181
+
182
+ // src/arrayHelpersFactory.ts
183
+ var createArrayHelpers = (field, arrayValue, setFieldValue, getFieldProps, getSelectFieldProps, getSliderProps, getCheckboxProps, getSwitchProps, getFileFieldProps, getRadioGroupOptionProps) => {
184
+ return {
185
+ /**
186
+ * The current array of values for this field.
187
+ * Use this to iterate over items when rendering the form.
188
+ *
189
+ * @example
190
+ * // Render array items
191
+ * helpers.values.map((contact, index) => (
192
+ * <div key={index}>
193
+ * <input {...helpers.getFieldProps(index, "name")} />
194
+ * </div>
195
+ * ))
196
+ */
197
+ values: arrayValue,
198
+ /**
199
+ * Adds a new item to the end of the array.
200
+ * The form state is updated immutably and all subscribed components re-render.
201
+ *
202
+ * @param value - The item to add to the array
203
+ *
204
+ * @example
205
+ * // Add a new contact
206
+ * const helpers = form.arrayHelpers("contacts");
207
+ * helpers.push({ name: "", email: "", phone: "" });
208
+ */
209
+ push: (value) => {
210
+ const newArray = [...arrayValue, value];
211
+ setFieldValue(field, newArray);
212
+ },
213
+ /**
214
+ * Removes an item at the specified index from the array.
215
+ * If the index is out of bounds, no action is taken.
216
+ * The form state is updated immutably.
217
+ *
218
+ * @param index - The zero-based index of the item to remove
219
+ *
220
+ * @example
221
+ * // Remove the second contact
222
+ * const helpers = form.arrayHelpers("contacts");
223
+ * helpers.remove(1);
224
+ */
225
+ remove: (index) => {
226
+ if (index < 0 || index >= arrayValue.length) return;
227
+ const newArray = [...arrayValue];
228
+ newArray.splice(index, 1);
229
+ setFieldValue(field, newArray);
230
+ },
231
+ /**
232
+ * Inserts an item at the specified index in the array.
233
+ * Items at and after the index are shifted to the right.
234
+ * If the index is out of bounds, it's clamped to valid range [0, length].
235
+ *
236
+ * @param index - The zero-based insertion position
237
+ * @param value - The item to insert
238
+ *
239
+ * @example
240
+ * // Insert a contact at the beginning
241
+ * const helpers = form.arrayHelpers("contacts");
242
+ * helpers.insert(0, { name: "New Contact", email: "", phone: "" });
243
+ */
244
+ insert: (index, value) => {
245
+ const clamped = Math.max(0, Math.min(index, arrayValue.length));
246
+ const newArray = [...arrayValue];
247
+ newArray.splice(clamped, 0, value);
248
+ setFieldValue(field, newArray);
249
+ },
250
+ /**
251
+ * Swaps the positions of two items in the array.
252
+ * If either index is out of bounds or they are equal, no action is taken.
253
+ * Useful for drag-and-drop reordering interfaces.
254
+ *
255
+ * @param indexA - The zero-based index of the first item
256
+ * @param indexB - The zero-based index of the second item
257
+ *
258
+ * @example
259
+ * // Swap first and second contacts
260
+ * const helpers = form.arrayHelpers("contacts");
261
+ * helpers.swap(0, 1);
262
+ *
263
+ * @example
264
+ * // Move item up in list
265
+ * const moveUp = (index: number) => {
266
+ * if (index > 0) helpers.swap(index, index - 1);
267
+ * };
268
+ */
269
+ swap: (indexA, indexB) => {
270
+ if (indexA === indexB || indexA < 0 || indexB < 0 || indexA >= arrayValue.length || indexB >= arrayValue.length) {
271
+ return;
272
+ }
273
+ const newArray = [...arrayValue];
274
+ const a = newArray[indexA];
275
+ const b = newArray[indexB];
276
+ if (a === void 0 || b === void 0) {
277
+ return;
278
+ }
279
+ newArray[indexA] = b;
280
+ newArray[indexB] = a;
281
+ setFieldValue(field, newArray);
282
+ },
283
+ /**
284
+ * Replaces an entire item at the specified index with a new value.
285
+ * If the index is out of bounds, no action is taken.
286
+ *
287
+ * @param index - The zero-based index of the item to replace
288
+ * @param value - The new item value
289
+ *
290
+ * @example
291
+ * // Replace a contact entirely
292
+ * const helpers = form.arrayHelpers("contacts");
293
+ * helpers.replace(0, { name: "John Doe", email: "john@example.com", phone: "555-0100" });
294
+ */
295
+ replace: (index, value) => {
296
+ if (index < 0 || index >= arrayValue.length) return;
297
+ const newArray = [...arrayValue];
298
+ newArray[index] = value;
299
+ setFieldValue(field, newArray);
300
+ },
301
+ /**
302
+ * Gets props for a native text input bound to a field within an array item.
303
+ * Provides type-safe field path autocomplete for nested fields.
304
+ * Works with: <input type="text">, <input type="email">, <textarea>, etc.
305
+ *
306
+ * @param index - The zero-based index of the array item
307
+ * @param subField - The field path within the array item (type-safe and autocompleted)
308
+ * @returns Props object to spread onto a native input element
309
+ *
310
+ * @example
311
+ * // Text input for contact name
312
+ * helpers.values.map((_, index) => (
313
+ * <input type="text" {...helpers.getFieldProps(index, "name")} />
314
+ * ))
315
+ */
316
+ getFieldProps: (index, subField) => {
317
+ const path = `${field}[${index}].${subField}`;
318
+ return getFieldProps(path);
319
+ },
320
+ /**
321
+ * Gets props for a native select element bound to a field within an array item.
322
+ * Provides type-safe field path autocomplete for nested fields.
323
+ * Works with: <select>
324
+ *
325
+ * @param index - The zero-based index of the array item
326
+ * @param subField - The field path within the array item (type-safe and autocompleted)
327
+ * @returns Props object to spread onto a native select element
328
+ *
329
+ * @example
330
+ * // Select for contact type
331
+ * <select {...helpers.getSelectFieldProps(index, "type")}>
332
+ * <option value="work">Work</option>
333
+ * <option value="personal">Personal</option>
334
+ * </select>
335
+ */
336
+ getSelectFieldProps: (index, subField) => {
337
+ const path = `${field}[${index}].${subField}`;
338
+ return getSelectFieldProps(path);
339
+ },
340
+ /**
341
+ * Gets props for a native range input bound to a field within an array item.
342
+ * Provides type-safe field path autocomplete for nested fields.
343
+ * Works with: <input type="range">
344
+ *
345
+ * @param index - The zero-based index of the array item
346
+ * @param subField - The field path within the array item (type-safe and autocompleted)
347
+ * @returns Props object to spread onto a native range input element
348
+ *
349
+ * @example
350
+ * // Range slider for priority
351
+ * <input type="range" min={0} max={10} {...helpers.getSliderProps(index, "priority")} />
352
+ */
353
+ getSliderProps: (index, subField) => {
354
+ const path = `${field}[${index}].${subField}`;
355
+ return getSliderProps(path);
356
+ },
357
+ /**
358
+ * Gets props for a native checkbox bound to a field within an array item.
359
+ * Provides type-safe field path autocomplete for nested fields.
360
+ * Works with: <input type="checkbox">
361
+ *
362
+ * @param index - The zero-based index of the array item
363
+ * @param subField - The field path within the array item (type-safe and autocompleted)
364
+ * @returns Props object to spread onto a native checkbox input
365
+ *
366
+ * @example
367
+ * // Checkbox for marking contact as favorite
368
+ * <input type="checkbox" {...helpers.getCheckboxProps(index, "isFavorite")} />
369
+ */
370
+ getCheckboxProps: (index, subField) => {
371
+ const path = `${field}[${index}].${subField}`;
372
+ return getCheckboxProps(path);
373
+ },
374
+ /**
375
+ * Gets props for a native switch (checkbox styled as switch) bound to a field within an array item.
376
+ * Provides type-safe field path autocomplete for nested fields.
377
+ * Works with: <input type="checkbox"> (styled as switch via CSS)
378
+ *
379
+ * @param index - The zero-based index of the array item
380
+ * @param subField - The field path within the array item (type-safe and autocompleted)
381
+ * @returns Props object to spread onto a native checkbox input
382
+ *
383
+ * @example
384
+ * // Switch for enabling/disabling a feature
385
+ * <label className="switch">
386
+ * <input type="checkbox" {...helpers.getSwitchProps(index, "enabled")} />
387
+ * <span className="slider"></span>
388
+ * </label>
389
+ */
390
+ getSwitchProps: (index, subField) => {
391
+ const path = `${field}[${index}].${subField}`;
392
+ return getSwitchProps(path);
393
+ },
394
+ /**
395
+ * Gets props for a native file input bound to a field within an array item.
396
+ * Provides type-safe field path autocomplete for nested fields.
397
+ * Works with: <input type="file">
398
+ *
399
+ * @param index - The zero-based index of the array item
400
+ * @param subField - The field path within the array item (type-safe and autocompleted)
401
+ * @returns Props object to spread onto a native file input
402
+ *
403
+ * @example
404
+ * // File input for contact avatar
405
+ * <input type="file" accept="image/*" {...helpers.getFileFieldProps(index, "avatar")} />
406
+ */
407
+ getFileFieldProps: (index, subField) => {
408
+ const path = `${field}[${index}].${subField}`;
409
+ return getFileFieldProps(path);
410
+ },
411
+ /**
412
+ * Gets props for a radio group option bound to a field within an array item.
413
+ * Provides type-safe field path autocomplete for nested fields.
414
+ * Works with: multiple <input type="radio"> elements sharing the same name
415
+ *
416
+ * @param index - The zero-based index of the array item
417
+ * @param subField - The field path within the array item (type-safe and autocompleted)
418
+ * @param optionValue - The value this radio option represents
419
+ * @returns Props object to spread onto a native radio input
420
+ *
421
+ * @example
422
+ * // Radio group for contact method preference
423
+ * <label>
424
+ * <input type="radio" {...helpers.getRadioGroupOptionProps(index, "preferredMethod", "email")} />
425
+ * Email
426
+ * </label>
427
+ * <label>
428
+ * <input type="radio" {...helpers.getRadioGroupOptionProps(index, "preferredMethod", "phone")} />
429
+ * Phone
430
+ * </label>
431
+ */
432
+ getRadioGroupOptionProps: (index, subField, optionValue) => {
433
+ const path = `${field}[${index}].${subField}`;
434
+ return getRadioGroupOptionProps(path, optionValue);
435
+ }
436
+ };
437
+ };
438
+
439
+ // src/fieldPropsFactory.ts
440
+ var createNativeFieldProps = (field, formValues, handleChange, handleBlur) => {
441
+ const raw = getValueByPath(formValues, field);
442
+ const value = raw == null ? "" : typeof raw === "string" || typeof raw === "number" ? raw : raw instanceof Date ? isNaN(raw.getTime()) ? "" : raw.toISOString().split("T")[0] : JSON.stringify(raw);
443
+ return {
444
+ id: `field-${field.replace(/[[\].]/g, "-")}`,
445
+ name: field,
446
+ value,
447
+ onChange: (e) => {
448
+ handleChange(field, e.target.value);
449
+ },
450
+ onBlur: () => handleBlur(field, true)
451
+ };
452
+ };
453
+ var createNativeSelectFieldProps = (field, formValues, handleChange, handleBlur) => {
454
+ const raw = getValueByPath(formValues, field);
455
+ const value = raw == null ? "" : typeof raw === "string" || typeof raw === "number" ? raw : JSON.stringify(raw);
456
+ return {
457
+ id: `field-${field.replace(/[[\].]/g, "-")}`,
458
+ name: field,
459
+ value,
460
+ onChange: (e) => handleChange(field, e.target.value),
461
+ onBlur: () => handleBlur(field, true)
462
+ };
463
+ };
464
+ var createNativeCheckboxProps = (field, formValues, handleChange, handleBlur) => {
465
+ const value = getValueByPath(formValues, field);
466
+ return {
467
+ id: `field-${field.replace(/[[\].]/g, "-")}`,
468
+ name: field,
469
+ checked: !!value,
470
+ onChange: (e) => handleChange(field, e.target.checked),
471
+ onBlur: () => handleBlur(field, true)
472
+ };
473
+ };
474
+ var createNativeSwitchProps = (field, formValues, handleChange, handleBlur) => {
475
+ const value = getValueByPath(formValues, field);
476
+ return {
477
+ id: `field-${field.replace(/[[\].]/g, "-")}`,
478
+ name: field,
479
+ checked: !!value,
480
+ onChange: (e) => handleChange(field, e.target.checked),
481
+ onBlur: () => handleBlur(field, true)
482
+ };
483
+ };
484
+ var createNativeSliderProps = (field, formValues, handleChange, handleBlur) => {
485
+ const raw = getValueByPath(formValues, field);
486
+ const toNumber = (v) => typeof v === "string" ? Number(v || 0) : typeof v === "number" ? v : 0;
487
+ const value = Array.isArray(raw) ? toNumber(raw[0]) : toNumber(raw);
488
+ return {
489
+ id: `field-${field.replace(/[[\].]/g, "-")}`,
490
+ name: field,
491
+ type: "range",
492
+ value,
493
+ onChange: (e) => handleChange(field, Number(e.target.value)),
494
+ onBlur: () => handleBlur(field, true)
495
+ };
496
+ };
497
+ var createCheckboxGroupOptionProps = (field, optionValue, formValues, handleChange, handleBlur) => {
498
+ const raw = getValueByPath(formValues, field);
499
+ const current = Array.isArray(raw) ? raw : [];
500
+ const isChecked = current.some((v) => String(v) === String(optionValue));
501
+ return {
502
+ id: `field-${field.replace(/[[\].]/g, "-")}-${String(optionValue)}`,
503
+ name: field,
504
+ value: optionValue,
505
+ checked: isChecked,
506
+ onChange: (e) => {
507
+ if (e.target.checked) {
508
+ if (!current.some((v) => String(v) === String(optionValue))) {
509
+ handleChange(field, [...current, optionValue]);
510
+ }
511
+ } else {
512
+ const next = current.filter((v) => String(v) !== String(optionValue));
513
+ handleChange(field, next);
514
+ }
515
+ },
516
+ onBlur: () => handleBlur(field, true)
517
+ };
518
+ };
519
+ var createNativeFileFieldProps = (field, _formValues, handleChange, handleBlur) => {
520
+ return {
521
+ id: `field-${field.replace(/[[\].]/g, "-")}`,
522
+ name: field,
523
+ onChange: (e) => {
524
+ const input = e.target;
525
+ const files = input.files;
526
+ if (!files || files.length === 0) {
527
+ handleChange(field, input.multiple ? [] : null);
528
+ return;
529
+ }
530
+ handleChange(field, input.multiple ? Array.from(files) : files[0]);
531
+ },
532
+ onBlur: () => handleBlur(field, true)
533
+ };
534
+ };
535
+ var createRadioGroupOptionProps = (field, optionValue, formValues, handleChange, handleBlur) => {
536
+ const current = getValueByPath(formValues, field);
537
+ const checked = String(current) === String(optionValue);
538
+ return {
539
+ id: `field-${field.replace(/[[\].]/g, "-")}-${String(optionValue)}`,
540
+ name: field,
541
+ value: optionValue,
542
+ checked,
543
+ onChange: (e) => {
544
+ if (e.target.checked) handleChange(field, optionValue);
545
+ },
546
+ onBlur: () => handleBlur(field, true)
547
+ };
548
+ };
549
+
550
+ // src/useForm.ts
551
+ var useForm = (validator, options = {}) => {
552
+ const {
553
+ initialValues = {},
554
+ onSubmit,
555
+ validationMode
556
+ } = options;
557
+ const mode = validationMode ?? "live";
558
+ const validateOnChange = mode === "live";
559
+ const validateOnBlur = mode === "live" || mode === "blur";
560
+ const validateOnMount = mode === "mount";
561
+ const touchOnChange = mode === "live";
562
+ const reducerFn = useCallback(
563
+ (state, action) => formReducer(state, action, initialValues),
564
+ [initialValues]
565
+ );
566
+ const initialState = {
567
+ values: initialValues,
568
+ touched: {},
569
+ clientErrors: {},
570
+ serverErrors: {},
571
+ isSubmitting: false,
572
+ isDirty: false
573
+ };
574
+ const [formState, dispatch] = useReducer(reducerFn, initialState);
575
+ const errors = useMemo(() => {
576
+ const combined = {};
577
+ Object.entries(formState.serverErrors).forEach(([path, message]) => {
578
+ combined[path] = message;
579
+ });
580
+ Object.entries(formState.clientErrors).forEach(([path, message]) => {
581
+ if (!combined[path]) {
582
+ combined[path] = message;
583
+ }
584
+ });
585
+ return combined;
586
+ }, [formState.clientErrors, formState.serverErrors]);
587
+ const isValid = useMemo(() => Object.keys(errors).length === 0, [errors]);
588
+ const validateForm = useCallback(
589
+ (values) => {
590
+ const validationResult = validate(values, validator);
591
+ if (isErr(validationResult)) {
592
+ dispatch({
593
+ type: "SET_CLIENT_ERRORS",
594
+ errors: formatErrors(validationResult.error)
595
+ });
596
+ } else {
597
+ dispatch({
598
+ type: "SET_CLIENT_ERRORS",
599
+ errors: {}
600
+ });
601
+ }
602
+ return validationResult;
603
+ },
604
+ [validator]
605
+ );
606
+ const didMountRef = useRef(false);
607
+ useEffect(() => {
608
+ if (validateOnMount && !didMountRef.current) {
609
+ didMountRef.current = true;
610
+ validateForm(initialValues);
611
+ const allFields = collectFieldPaths(
612
+ initialValues
613
+ );
614
+ dispatch({
615
+ type: "MARK_ALL_TOUCHED",
616
+ fields: allFields
617
+ });
618
+ }
619
+ }, [initialValues, validateForm, validateOnMount]);
620
+ const setFieldValue = useCallback(
621
+ (field, value, shouldValidate = validateOnChange) => {
622
+ const updatedValues = setValueByPath(
623
+ formState.values,
624
+ field,
625
+ value
626
+ );
627
+ dispatch({
628
+ type: "SET_FIELD_VALUE",
629
+ field,
630
+ value
631
+ });
632
+ if (touchOnChange && !formState.touched[field]) {
633
+ dispatch({ type: "SET_FIELD_TOUCHED", field, isTouched: true });
634
+ }
635
+ if (shouldValidate) {
636
+ validateForm(updatedValues);
637
+ }
638
+ },
639
+ [
640
+ formState.values,
641
+ formState.touched,
642
+ validateOnChange,
643
+ validateForm,
644
+ touchOnChange
645
+ ]
646
+ );
647
+ const setValues = useCallback(
648
+ (newValues, shouldValidate = validateOnChange) => {
649
+ dispatch({
650
+ type: "SET_VALUES",
651
+ values: newValues
652
+ });
653
+ if (shouldValidate) {
654
+ const updatedValues = { ...formState.values, ...newValues };
655
+ validateForm(updatedValues);
656
+ }
657
+ },
658
+ [formState.values, validateOnChange, validateForm]
659
+ );
660
+ const setFieldTouched = useCallback(
661
+ (field, isTouched = true, shouldValidate = validateOnBlur) => {
662
+ dispatch({
663
+ type: "SET_FIELD_TOUCHED",
664
+ field,
665
+ isTouched
666
+ });
667
+ if (shouldValidate) {
668
+ validateForm(formState.values);
669
+ }
670
+ },
671
+ [formState.values, validateOnBlur, validateForm]
672
+ );
673
+ const setServerErrors = useCallback(
674
+ (serverErrors) => {
675
+ dispatch({
676
+ type: "SET_SERVER_ERRORS",
677
+ errors: serverErrors
678
+ });
679
+ },
680
+ []
681
+ );
682
+ const clearServerErrors = useCallback(() => {
683
+ dispatch({
684
+ type: "CLEAR_SERVER_ERRORS"
685
+ });
686
+ }, []);
687
+ const handleSubmit = useCallback(
688
+ async (e) => {
689
+ if (e) {
690
+ e.preventDefault();
691
+ }
692
+ const valuePaths = collectFieldPaths(
693
+ formState.values
694
+ );
695
+ const allPaths = Array.from(
696
+ /* @__PURE__ */ new Set([
697
+ ...valuePaths,
698
+ ...Object.keys(formState.clientErrors),
699
+ ...Object.keys(formState.serverErrors)
700
+ ])
701
+ );
702
+ dispatch({
703
+ type: "MARK_ALL_TOUCHED",
704
+ fields: allPaths
705
+ });
706
+ dispatch({
707
+ type: "SET_SUBMITTING",
708
+ isSubmitting: true
709
+ });
710
+ dispatch({
711
+ type: "CLEAR_SERVER_ERRORS"
712
+ });
713
+ const validationResult = validate(formState.values, validator);
714
+ return match(validationResult, {
715
+ ok: async (validData) => {
716
+ if (onSubmit) {
717
+ try {
718
+ await onSubmit(validData);
719
+ } catch (error) {
720
+ console.error("Form submission error:", error);
721
+ }
722
+ }
723
+ dispatch({
724
+ type: "SET_SUBMITTING",
725
+ isSubmitting: false
726
+ });
727
+ return validData;
728
+ },
729
+ err: async (errors2) => {
730
+ dispatch({
731
+ type: "SET_CLIENT_ERRORS",
732
+ errors: formatErrors(errors2)
733
+ });
734
+ dispatch({
735
+ type: "SET_SUBMITTING",
736
+ isSubmitting: false
737
+ });
738
+ return Promise.resolve(errors2);
739
+ }
740
+ });
741
+ },
742
+ [
743
+ formState.values,
744
+ formState.clientErrors,
745
+ formState.serverErrors,
746
+ validator,
747
+ onSubmit
748
+ ]
749
+ );
750
+ const resetForm = useCallback(() => {
751
+ dispatch({
752
+ type: "RESET_FORM"
753
+ });
754
+ if (validateOnMount) {
755
+ validateForm(initialValues);
756
+ }
757
+ }, [initialValues, validateForm, validateOnMount]);
758
+ const getFieldProps = useCallback(
759
+ (field) => {
760
+ return createNativeFieldProps(
761
+ field,
762
+ formState.values,
763
+ setFieldValue,
764
+ setFieldTouched
765
+ );
766
+ },
767
+ [formState.values, setFieldValue, setFieldTouched]
768
+ );
769
+ const getSelectFieldProps = useCallback(
770
+ (field) => {
771
+ return createNativeSelectFieldProps(
772
+ field,
773
+ formState.values,
774
+ setFieldValue,
775
+ setFieldTouched
776
+ );
777
+ },
778
+ [formState.values, setFieldValue, setFieldTouched]
779
+ );
780
+ const getCheckboxProps = useCallback(
781
+ (field) => {
782
+ return createNativeCheckboxProps(
783
+ field,
784
+ formState.values,
785
+ setFieldValue,
786
+ setFieldTouched
787
+ );
788
+ },
789
+ [formState.values, setFieldValue, setFieldTouched]
790
+ );
791
+ const getSwitchProps = useCallback(
792
+ (field) => {
793
+ return createNativeSwitchProps(
794
+ field,
795
+ formState.values,
796
+ setFieldValue,
797
+ setFieldTouched
798
+ );
799
+ },
800
+ [formState.values, setFieldValue, setFieldTouched]
801
+ );
802
+ const getSliderProps = useCallback(
803
+ (field) => {
804
+ return createNativeSliderProps(
805
+ field,
806
+ formState.values,
807
+ setFieldValue,
808
+ setFieldTouched
809
+ );
810
+ },
811
+ [formState.values, setFieldValue, setFieldTouched]
812
+ );
813
+ const getCheckboxGroupOptionProps = useCallback(
814
+ (field, optionValue) => {
815
+ return createCheckboxGroupOptionProps(
816
+ field,
817
+ optionValue,
818
+ formState.values,
819
+ setFieldValue,
820
+ setFieldTouched
821
+ );
822
+ },
823
+ [formState.values, setFieldValue, setFieldTouched]
824
+ );
825
+ const getFileFieldProps = useCallback(
826
+ (field) => {
827
+ return createNativeFileFieldProps(
828
+ field,
829
+ formState.values,
830
+ setFieldValue,
831
+ setFieldTouched
832
+ );
833
+ },
834
+ [formState.values, setFieldValue, setFieldTouched]
835
+ );
836
+ const getRadioGroupOptionProps = useCallback(
837
+ (field, optionValue) => {
838
+ return createRadioGroupOptionProps(
839
+ field,
840
+ optionValue,
841
+ formState.values,
842
+ setFieldValue,
843
+ setFieldTouched
844
+ );
845
+ },
846
+ [formState.values, setFieldValue, setFieldTouched]
847
+ );
848
+ const arrayHelpersImpl = useCallback(
849
+ (field) => {
850
+ const getFieldPropsAtPath = (path) => createNativeFieldProps(
851
+ path,
852
+ formState.values,
853
+ setFieldValue,
854
+ setFieldTouched
855
+ );
856
+ const getSelectFieldPropsAtPath = (path) => createNativeSelectFieldProps(
857
+ path,
858
+ formState.values,
859
+ setFieldValue,
860
+ setFieldTouched
861
+ );
862
+ const getSliderPropsAtPath = (path) => createNativeSliderProps(
863
+ path,
864
+ formState.values,
865
+ setFieldValue,
866
+ setFieldTouched
867
+ );
868
+ const getCheckboxPropsAtPath = (path) => createNativeCheckboxProps(
869
+ path,
870
+ formState.values,
871
+ setFieldValue,
872
+ setFieldTouched
873
+ );
874
+ const getSwitchPropsAtPath = (path) => createNativeSwitchProps(
875
+ path,
876
+ formState.values,
877
+ setFieldValue,
878
+ setFieldTouched
879
+ );
880
+ const getFileFieldPropsAtPath = (path) => createNativeFileFieldProps(
881
+ path,
882
+ formState.values,
883
+ setFieldValue,
884
+ setFieldTouched
885
+ );
886
+ const getRadioGroupOptionPropsAtPath = (path, opt) => createRadioGroupOptionProps(
887
+ path,
888
+ opt,
889
+ formState.values,
890
+ setFieldValue,
891
+ setFieldTouched
892
+ );
893
+ const arrayValue = getValueByPath(formState.values, field) || [];
894
+ return createArrayHelpers(
895
+ field,
896
+ arrayValue,
897
+ setFieldValue,
898
+ getFieldPropsAtPath,
899
+ getSelectFieldPropsAtPath,
900
+ getSliderPropsAtPath,
901
+ getCheckboxPropsAtPath,
902
+ getSwitchPropsAtPath,
903
+ getFileFieldPropsAtPath,
904
+ getRadioGroupOptionPropsAtPath
905
+ );
906
+ },
907
+ [formState.values, setFieldValue, setFieldTouched]
908
+ );
909
+ const arrayHelpers = arrayHelpersImpl;
910
+ return {
911
+ // Form state
912
+ values: formState.values,
913
+ touched: formState.touched,
914
+ errors,
915
+ clientErrors: formState.clientErrors,
916
+ serverErrors: formState.serverErrors,
917
+ isSubmitting: formState.isSubmitting,
918
+ isValid,
919
+ isDirty: formState.isDirty,
920
+ // Field management
921
+ setFieldValue,
922
+ setFieldTouched,
923
+ setValues,
924
+ // Server error management
925
+ setServerErrors,
926
+ clearServerErrors,
927
+ // Form actions
928
+ handleSubmit,
929
+ resetForm,
930
+ validateForm,
931
+ // Native HTML field integration
932
+ getFieldProps,
933
+ getSelectFieldProps,
934
+ getCheckboxProps,
935
+ getSwitchProps,
936
+ getSliderProps,
937
+ getCheckboxGroupOptionProps,
938
+ getFileFieldProps,
939
+ getRadioGroupOptionProps,
940
+ // Array field helpers
941
+ arrayHelpers
942
+ };
943
+ };
944
+ function useDebounce(callback, delay) {
945
+ const timeoutRef = useRef(null);
946
+ const callbackRef = useRef(callback);
947
+ useEffect(() => {
948
+ callbackRef.current = callback;
949
+ }, [callback]);
950
+ useEffect(() => {
951
+ return () => {
952
+ if (timeoutRef.current) {
953
+ clearTimeout(timeoutRef.current);
954
+ }
955
+ };
956
+ }, []);
957
+ return useCallback(
958
+ (...args) => {
959
+ if (timeoutRef.current) {
960
+ clearTimeout(timeoutRef.current);
961
+ }
962
+ timeoutRef.current = setTimeout(() => {
963
+ callbackRef.current(...args);
964
+ }, delay);
965
+ },
966
+ [delay]
967
+ );
968
+ }
969
+
970
+ // src/useAutoSubmitForm.ts
971
+ var useFormAutoSubmission = (form, delay = 200) => {
972
+ const lastValidatedRef = useRef("");
973
+ const debouncedSubmit = useDebounce(() => {
974
+ if (form.isValid) form.handleSubmit();
975
+ }, delay);
976
+ useEffect(() => {
977
+ if (!form.isDirty) return;
978
+ const valuesString = JSON.stringify(form.values);
979
+ if (valuesString === lastValidatedRef.current) return;
980
+ lastValidatedRef.current = valuesString;
981
+ form.validateForm(form.values);
982
+ if (form.isValid) debouncedSubmit();
983
+ }, [form.values, form.isDirty, form.isValid, debouncedSubmit, form]);
984
+ return null;
985
+ };
986
+
987
+ export { useDebounce, useForm, useFormAutoSubmission };
988
+ //# sourceMappingURL=index.js.map
989
+ //# sourceMappingURL=index.js.map