remix-validated-form 5.0.2 → 5.1.1-beta.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.
Files changed (37) hide show
  1. package/.turbo/turbo-build.log +152 -8
  2. package/dist/index.cjs.js +898 -63
  3. package/dist/index.cjs.js.map +1 -1
  4. package/dist/index.d.ts +7 -2
  5. package/dist/index.esm.js +876 -15
  6. package/dist/index.esm.js.map +1 -1
  7. package/package.json +4 -4
  8. package/src/ValidatedForm.tsx +0 -427
  9. package/src/hooks.ts +0 -160
  10. package/src/index.ts +0 -12
  11. package/src/internal/MultiValueMap.ts +0 -44
  12. package/src/internal/constants.ts +0 -4
  13. package/src/internal/flatten.ts +0 -12
  14. package/src/internal/formContext.ts +0 -13
  15. package/src/internal/getInputProps.test.ts +0 -251
  16. package/src/internal/getInputProps.ts +0 -94
  17. package/src/internal/hooks.ts +0 -217
  18. package/src/internal/hydratable.ts +0 -28
  19. package/src/internal/logic/getCheckboxChecked.ts +0 -10
  20. package/src/internal/logic/getRadioChecked.ts +0 -18
  21. package/src/internal/logic/nestedObjectToPathObject.ts +0 -63
  22. package/src/internal/logic/requestSubmit.test.tsx +0 -24
  23. package/src/internal/logic/requestSubmit.ts +0 -103
  24. package/src/internal/state/arrayUtil.ts +0 -451
  25. package/src/internal/state/controlledFields.ts +0 -86
  26. package/src/internal/state/createFormStore.ts +0 -591
  27. package/src/internal/state/fieldArray.tsx +0 -197
  28. package/src/internal/state/storeHooks.ts +0 -9
  29. package/src/internal/state/types.ts +0 -1
  30. package/src/internal/submissionCallbacks.ts +0 -15
  31. package/src/internal/util.ts +0 -39
  32. package/src/server.ts +0 -53
  33. package/src/unreleased/formStateHooks.ts +0 -170
  34. package/src/userFacingFormContext.ts +0 -147
  35. package/src/validation/createValidator.ts +0 -53
  36. package/src/validation/types.ts +0 -72
  37. package/tsconfig.json +0 -8
@@ -1,591 +0,0 @@
1
- import { WritableDraft } from "immer/dist/internal";
2
- import { getPath, setPath } from "set-get";
3
- import invariant from "tiny-invariant";
4
- import { create, GetState } from "zustand";
5
- import { immer } from "zustand/middleware/immer";
6
- import {
7
- FieldErrors,
8
- TouchedFields,
9
- ValidationResult,
10
- Validator,
11
- } from "../../validation/types";
12
- import { requestSubmit } from "../logic/requestSubmit";
13
- import * as arrayUtil from "./arrayUtil";
14
- import { InternalFormId } from "./types";
15
-
16
- export type SyncedFormProps = {
17
- formId?: string;
18
- action?: string;
19
- subaction?: string;
20
- defaultValues: { [fieldName: string]: any };
21
- registerReceiveFocus: (fieldName: string, handler: () => void) => () => void;
22
- validator: Validator<unknown>;
23
- };
24
-
25
- export type SmartValidateOpts = {
26
- alwaysIncludeErrorsFromFields?: string[];
27
- };
28
-
29
- export type FormStoreState = {
30
- forms: { [formId: InternalFormId]: FormState };
31
- form: (formId: InternalFormId) => FormState;
32
- registerForm: (formId: InternalFormId) => void;
33
- cleanupForm: (formId: InternalFormId) => void;
34
- };
35
-
36
- export type FormState = {
37
- isHydrated: boolean;
38
- isSubmitting: boolean;
39
- hasBeenSubmitted: boolean;
40
- fieldErrors: FieldErrors;
41
- touchedFields: TouchedFields;
42
- formProps?: SyncedFormProps;
43
- formElement: HTMLFormElement | null;
44
- currentDefaultValues: Record<string, any>;
45
-
46
- isValid: () => boolean;
47
- startSubmit: () => void;
48
- endSubmit: () => void;
49
- setTouched: (field: string, touched: boolean) => void;
50
- setFieldError: (field: string, error: string) => void;
51
- setFieldErrors: (errors: FieldErrors) => void;
52
- clearFieldError: (field: string) => void;
53
- reset: () => void;
54
- syncFormProps: (props: SyncedFormProps) => void;
55
- setFormElement: (formElement: HTMLFormElement | null) => void;
56
- validate: () => Promise<ValidationResult<unknown>>;
57
- smartValidate: (
58
- opts?: SmartValidateOpts
59
- ) => Promise<ValidationResult<unknown>>;
60
- resetFormElement: () => void;
61
- submit: () => void;
62
- getValues: () => FormData;
63
-
64
- controlledFields: {
65
- values: { [fieldName: string]: any };
66
- refCounts: { [fieldName: string]: number };
67
- valueUpdatePromises: { [fieldName: string]: Promise<void> };
68
- valueUpdateResolvers: { [fieldName: string]: () => void };
69
-
70
- register: (fieldName: string) => void;
71
- unregister: (fieldName: string) => void;
72
- setValue: (fieldName: string, value: unknown) => void;
73
- kickoffValueUpdate: (fieldName: string) => void;
74
- getValue: (fieldName: string) => unknown;
75
- awaitValueUpdate: (fieldName: string) => Promise<void>;
76
-
77
- array: {
78
- push: (fieldName: string, value: unknown) => void;
79
- swap: (fieldName: string, indexA: number, indexB: number) => void;
80
- move: (fieldName: string, fromIndex: number, toIndex: number) => void;
81
- insert: (fieldName: string, index: number, value: unknown) => void;
82
- unshift: (fieldName: string, value: unknown) => void;
83
- remove: (fieldName: string, index: number) => void;
84
- pop: (fieldName: string) => void;
85
- replace: (fieldName: string, index: number, value: unknown) => void;
86
- };
87
- };
88
- };
89
-
90
- const noOp = () => {};
91
- const defaultFormState: FormState = {
92
- isHydrated: false,
93
- isSubmitting: false,
94
- hasBeenSubmitted: false,
95
- touchedFields: {},
96
- fieldErrors: {},
97
- formElement: null,
98
- isValid: () => true,
99
- startSubmit: noOp,
100
- endSubmit: noOp,
101
- setTouched: noOp,
102
- setFieldError: noOp,
103
- setFieldErrors: noOp,
104
- clearFieldError: noOp,
105
- currentDefaultValues: {},
106
-
107
- reset: () => noOp,
108
- syncFormProps: noOp,
109
- setFormElement: noOp,
110
-
111
- validate: async () => {
112
- throw new Error("Validate called before form was initialized.");
113
- },
114
-
115
- smartValidate: async () => {
116
- throw new Error("Validate called before form was initialized.");
117
- },
118
-
119
- submit: async () => {
120
- throw new Error("Submit called before form was initialized.");
121
- },
122
-
123
- resetFormElement: noOp,
124
- getValues: () => new FormData(),
125
-
126
- controlledFields: {
127
- values: {},
128
- refCounts: {},
129
- valueUpdatePromises: {},
130
- valueUpdateResolvers: {},
131
-
132
- register: noOp,
133
- unregister: noOp,
134
- setValue: noOp,
135
- getValue: noOp,
136
- kickoffValueUpdate: noOp,
137
- awaitValueUpdate: async () => {
138
- throw new Error("AwaitValueUpdate called before form was initialized.");
139
- },
140
-
141
- array: {
142
- push: noOp,
143
- swap: noOp,
144
- move: noOp,
145
- insert: noOp,
146
- unshift: noOp,
147
- remove: noOp,
148
- pop: noOp,
149
- replace: noOp,
150
- },
151
- },
152
- };
153
-
154
- const createFormState = (
155
- set: (setter: (draft: WritableDraft<FormState>) => void) => void,
156
- get: GetState<FormState>
157
- ): FormState => ({
158
- // It's not "hydrated" until the form props are synced
159
- isHydrated: false,
160
- isSubmitting: false,
161
- hasBeenSubmitted: false,
162
- touchedFields: {},
163
- fieldErrors: {},
164
- formElement: null,
165
- currentDefaultValues: {},
166
-
167
- isValid: () => Object.keys(get().fieldErrors).length === 0,
168
- startSubmit: () =>
169
- set((state) => {
170
- state.isSubmitting = true;
171
- state.hasBeenSubmitted = true;
172
- }),
173
- endSubmit: () =>
174
- set((state) => {
175
- state.isSubmitting = false;
176
- }),
177
- setTouched: (fieldName, touched) =>
178
- set((state) => {
179
- state.touchedFields[fieldName] = touched;
180
- }),
181
- setFieldError: (fieldName: string, error: string) =>
182
- set((state) => {
183
- state.fieldErrors[fieldName] = error;
184
- }),
185
- setFieldErrors: (errors: FieldErrors) =>
186
- set((state) => {
187
- state.fieldErrors = errors;
188
- }),
189
- clearFieldError: (fieldName: string) =>
190
- set((state) => {
191
- delete state.fieldErrors[fieldName];
192
- }),
193
- reset: () =>
194
- set((state) => {
195
- state.fieldErrors = {};
196
- state.touchedFields = {};
197
- state.hasBeenSubmitted = false;
198
- const nextDefaults = state.formProps?.defaultValues ?? {};
199
- state.controlledFields.values = nextDefaults;
200
- state.currentDefaultValues = nextDefaults;
201
- }),
202
- syncFormProps: (props: SyncedFormProps) =>
203
- set((state) => {
204
- if (!state.isHydrated) {
205
- state.controlledFields.values = props.defaultValues;
206
- state.currentDefaultValues = props.defaultValues;
207
- }
208
-
209
- state.formProps = props;
210
- state.isHydrated = true;
211
- }),
212
- setFormElement: (formElement: HTMLFormElement | null) => {
213
- // This gets called frequently, so we want to avoid calling set() every time
214
- // Or else we wind up with an infinite loop
215
- if (get().formElement === formElement) return;
216
- set((state) => {
217
- // weird type issue here
218
- // seems to be because formElement is a writable draft
219
- state.formElement = formElement as any;
220
- });
221
- },
222
- validate: async () => {
223
- const formElement = get().formElement;
224
- invariant(
225
- formElement,
226
- "Cannot find reference to form. This is probably a bug in remix-validated-form."
227
- );
228
-
229
- const validator = get().formProps?.validator;
230
- invariant(
231
- validator,
232
- "Cannot find validator. This is probably a bug in remix-validated-form."
233
- );
234
-
235
- const result = await validator.validate(new FormData(formElement));
236
- if (result.error) get().setFieldErrors(result.error.fieldErrors);
237
- return result;
238
- },
239
-
240
- smartValidate: async ({ alwaysIncludeErrorsFromFields = [] } = {}) => {
241
- const formElement = get().formElement;
242
- invariant(
243
- formElement,
244
- "Cannot find reference to form. This is probably a bug in remix-validated-form."
245
- );
246
-
247
- const validator = get().formProps?.validator;
248
- invariant(
249
- validator,
250
- "Cannot find validator. This is probably a bug in remix-validated-form."
251
- );
252
-
253
- await Promise.all(
254
- alwaysIncludeErrorsFromFields.map((field) =>
255
- get().controlledFields.awaitValueUpdate?.(field)
256
- )
257
- );
258
-
259
- const validationResult = await validator.validate(
260
- new FormData(formElement)
261
- );
262
- if (!validationResult.error) {
263
- // Only update the field errors if it hasn't changed
264
- const hadErrors = Object.keys(get().fieldErrors).length > 0;
265
- if (hadErrors) get().setFieldErrors({});
266
- return validationResult;
267
- }
268
-
269
- const {
270
- error: { fieldErrors },
271
- } = validationResult;
272
- const errorFields = new Set<string>();
273
- const incomingErrors = new Set<string>();
274
- const prevErrors = new Set<string>();
275
-
276
- Object.keys(fieldErrors).forEach((field) => {
277
- errorFields.add(field);
278
- incomingErrors.add(field);
279
- });
280
-
281
- Object.keys(get().fieldErrors).forEach((field) => {
282
- errorFields.add(field);
283
- prevErrors.add(field);
284
- });
285
-
286
- const fieldsToUpdate = new Set<string>();
287
- const fieldsToDelete = new Set<string>();
288
-
289
- errorFields.forEach((field) => {
290
- // If an error has been cleared, remove it.
291
- if (!incomingErrors.has(field)) {
292
- fieldsToDelete.add(field);
293
- return;
294
- }
295
-
296
- // If an error has changed, we should update it.
297
- if (prevErrors.has(field) && incomingErrors.has(field)) {
298
- // Only update if the error has changed to avoid unnecessary rerenders
299
- if (fieldErrors[field] !== get().fieldErrors[field])
300
- fieldsToUpdate.add(field);
301
- return;
302
- }
303
-
304
- // If the error is always included, then we should update it.
305
- if (alwaysIncludeErrorsFromFields.includes(field)) {
306
- fieldsToUpdate.add(field);
307
- return;
308
- }
309
-
310
- // If the error is new, then only update if the field has been touched
311
- // or if the form has been submitted
312
- if (!prevErrors.has(field)) {
313
- const fieldTouched = get().touchedFields[field];
314
- const formHasBeenSubmitted = get().hasBeenSubmitted;
315
- if (fieldTouched || formHasBeenSubmitted) fieldsToUpdate.add(field);
316
- return;
317
- }
318
- });
319
-
320
- if (fieldsToDelete.size === 0 && fieldsToUpdate.size === 0) {
321
- return { ...validationResult, error: { fieldErrors: get().fieldErrors } };
322
- }
323
-
324
- set((state) => {
325
- fieldsToDelete.forEach((field) => {
326
- delete state.fieldErrors[field];
327
- });
328
-
329
- fieldsToUpdate.forEach((field) => {
330
- state.fieldErrors[field] = fieldErrors[field];
331
- });
332
- });
333
-
334
- return { ...validationResult, error: { fieldErrors: get().fieldErrors } };
335
- },
336
-
337
- submit: () => {
338
- const formElement = get().formElement;
339
- invariant(
340
- formElement,
341
- "Cannot find reference to form. This is probably a bug in remix-validated-form."
342
- );
343
-
344
- requestSubmit(formElement);
345
- },
346
-
347
- getValues: () => new FormData(get().formElement ?? undefined),
348
-
349
- resetFormElement: () => get().formElement?.reset(),
350
-
351
- controlledFields: {
352
- values: {},
353
- refCounts: {},
354
- valueUpdatePromises: {},
355
- valueUpdateResolvers: {},
356
-
357
- register: (fieldName) => {
358
- set((state) => {
359
- const current = state.controlledFields.refCounts[fieldName] ?? 0;
360
- state.controlledFields.refCounts[fieldName] = current + 1;
361
- });
362
- },
363
- unregister: (fieldName) => {
364
- // For this helper in particular, we may run into a case where state is undefined.
365
- // When the whole form unmounts, the form state may be cleaned up before the fields are.
366
- if (get() === null || get() === undefined) return;
367
- set((state) => {
368
- const current = state.controlledFields.refCounts[fieldName] ?? 0;
369
- if (current > 1) {
370
- state.controlledFields.refCounts[fieldName] = current - 1;
371
- return;
372
- }
373
-
374
- const isNested = Object.keys(state.controlledFields.refCounts).some(
375
- (key) => fieldName.startsWith(key) && key !== fieldName
376
- );
377
-
378
- // When nested within a field array, we should leave resetting up to the field array
379
- if (!isNested) {
380
- setPath(
381
- state.controlledFields.values,
382
- fieldName,
383
- getPath(state.formProps?.defaultValues, fieldName)
384
- );
385
- setPath(
386
- state.currentDefaultValues,
387
- fieldName,
388
- getPath(state.formProps?.defaultValues, fieldName)
389
- );
390
- }
391
-
392
- delete state.controlledFields.refCounts[fieldName];
393
- });
394
- },
395
- getValue: (fieldName) => getPath(get().controlledFields.values, fieldName),
396
- setValue: (fieldName, value) => {
397
- set((state) => {
398
- setPath(state.controlledFields.values, fieldName, value);
399
- });
400
- get().controlledFields.kickoffValueUpdate(fieldName);
401
- },
402
- kickoffValueUpdate: (fieldName) => {
403
- const clear = () =>
404
- set((state) => {
405
- delete state.controlledFields.valueUpdateResolvers[fieldName];
406
- delete state.controlledFields.valueUpdatePromises[fieldName];
407
- });
408
- set((state) => {
409
- const promise = new Promise<void>((resolve) => {
410
- state.controlledFields.valueUpdateResolvers[fieldName] = resolve;
411
- }).then(clear);
412
- state.controlledFields.valueUpdatePromises[fieldName] = promise;
413
- });
414
- },
415
-
416
- awaitValueUpdate: async (fieldName) => {
417
- await get().controlledFields.valueUpdatePromises[fieldName];
418
- },
419
-
420
- array: {
421
- push: (fieldName, item) => {
422
- set((state) => {
423
- arrayUtil
424
- .getArray(state.controlledFields.values, fieldName)
425
- .push(item);
426
- arrayUtil.getArray(state.currentDefaultValues, fieldName).push(item);
427
- // New item added to the end, no need to update touched or error
428
- });
429
- get().controlledFields.kickoffValueUpdate(fieldName);
430
- },
431
-
432
- swap: (fieldName, indexA, indexB) => {
433
- set((state) => {
434
- arrayUtil.swap(
435
- arrayUtil.getArray(state.controlledFields.values, fieldName),
436
- indexA,
437
- indexB
438
- );
439
- arrayUtil.swap(
440
- arrayUtil.getArray(state.currentDefaultValues, fieldName),
441
- indexA,
442
- indexB
443
- );
444
- arrayUtil.mutateAsArray(fieldName, state.touchedFields, (array) =>
445
- arrayUtil.swap(array, indexA, indexB)
446
- );
447
- arrayUtil.mutateAsArray(fieldName, state.fieldErrors, (array) =>
448
- arrayUtil.swap(array, indexA, indexB)
449
- );
450
- });
451
- get().controlledFields.kickoffValueUpdate(fieldName);
452
- },
453
-
454
- move: (fieldName, from, to) => {
455
- set((state) => {
456
- arrayUtil.move(
457
- arrayUtil.getArray(state.controlledFields.values, fieldName),
458
- from,
459
- to
460
- );
461
- arrayUtil.move(
462
- arrayUtil.getArray(state.currentDefaultValues, fieldName),
463
- from,
464
- to
465
- );
466
- arrayUtil.mutateAsArray(fieldName, state.touchedFields, (array) =>
467
- arrayUtil.move(array, from, to)
468
- );
469
- arrayUtil.mutateAsArray(fieldName, state.fieldErrors, (array) =>
470
- arrayUtil.move(array, from, to)
471
- );
472
- });
473
- get().controlledFields.kickoffValueUpdate(fieldName);
474
- },
475
- insert: (fieldName, index, item) => {
476
- set((state) => {
477
- arrayUtil.insert(
478
- arrayUtil.getArray(state.controlledFields.values, fieldName),
479
- index,
480
- item
481
- );
482
- arrayUtil.insert(
483
- arrayUtil.getArray(state.currentDefaultValues, fieldName),
484
- index,
485
- item
486
- );
487
- // Even though this is a new item, we need to push around other items.
488
- arrayUtil.mutateAsArray(fieldName, state.touchedFields, (array) =>
489
- arrayUtil.insertEmpty(array, index)
490
- );
491
- arrayUtil.mutateAsArray(fieldName, state.fieldErrors, (array) =>
492
- arrayUtil.insertEmpty(array, index)
493
- );
494
- });
495
- get().controlledFields.kickoffValueUpdate(fieldName);
496
- },
497
- remove: (fieldName, index) => {
498
- set((state) => {
499
- arrayUtil.remove(
500
- arrayUtil.getArray(state.controlledFields.values, fieldName),
501
- index
502
- );
503
- arrayUtil.remove(
504
- arrayUtil.getArray(state.currentDefaultValues, fieldName),
505
- index
506
- );
507
- arrayUtil.mutateAsArray(fieldName, state.touchedFields, (array) =>
508
- arrayUtil.remove(array, index)
509
- );
510
- arrayUtil.mutateAsArray(fieldName, state.fieldErrors, (array) =>
511
- arrayUtil.remove(array, index)
512
- );
513
- });
514
- get().controlledFields.kickoffValueUpdate(fieldName);
515
- },
516
- pop: (fieldName) => {
517
- set((state) => {
518
- arrayUtil.getArray(state.controlledFields.values, fieldName).pop();
519
- arrayUtil.getArray(state.currentDefaultValues, fieldName).pop();
520
- arrayUtil.mutateAsArray(fieldName, state.touchedFields, (array) =>
521
- array.pop()
522
- );
523
- arrayUtil.mutateAsArray(fieldName, state.fieldErrors, (array) =>
524
- array.pop()
525
- );
526
- });
527
- get().controlledFields.kickoffValueUpdate(fieldName);
528
- },
529
- unshift: (fieldName, value) => {
530
- set((state) => {
531
- arrayUtil
532
- .getArray(state.controlledFields.values, fieldName)
533
- .unshift(value);
534
- arrayUtil
535
- .getArray(state.currentDefaultValues, fieldName)
536
- .unshift(value);
537
- arrayUtil.mutateAsArray(fieldName, state.touchedFields, (array) =>
538
- arrayUtil.insertEmpty(array, 0)
539
- );
540
- arrayUtil.mutateAsArray(fieldName, state.fieldErrors, (array) =>
541
- arrayUtil.insertEmpty(array, 0)
542
- );
543
- });
544
- },
545
- replace: (fieldName, index, item) => {
546
- set((state) => {
547
- arrayUtil.replace(
548
- arrayUtil.getArray(state.controlledFields.values, fieldName),
549
- index,
550
- item
551
- );
552
- arrayUtil.replace(
553
- arrayUtil.getArray(state.currentDefaultValues, fieldName),
554
- index,
555
- item
556
- );
557
- arrayUtil.mutateAsArray(fieldName, state.touchedFields, (array) =>
558
- arrayUtil.replace(array, index, item)
559
- );
560
- arrayUtil.mutateAsArray(fieldName, state.fieldErrors, (array) =>
561
- arrayUtil.replace(array, index, item)
562
- );
563
- });
564
- get().controlledFields.kickoffValueUpdate(fieldName);
565
- },
566
- },
567
- },
568
- });
569
-
570
- export const useRootFormStore = create<FormStoreState>()(
571
- immer((set, get) => ({
572
- forms: {},
573
- form: (formId) => {
574
- return get().forms[formId] ?? defaultFormState;
575
- },
576
- cleanupForm: (formId: InternalFormId) => {
577
- set((state) => {
578
- delete state.forms[formId];
579
- });
580
- },
581
- registerForm: (formId: InternalFormId) => {
582
- if (get().forms[formId]) return;
583
- set((state) => {
584
- state.forms[formId] = createFormState(
585
- (setter) => set((state) => setter(state.forms[formId])),
586
- () => get().forms[formId]
587
- ) as WritableDraft<FormState>;
588
- });
589
- },
590
- }))
591
- );