@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/README.md +129 -0
- package/dist/index.d.ts +828 -0
- package/dist/index.js +989 -0
- package/dist/index.js.map +1 -0
- package/package.json +92 -0
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
|