remix-validated-form 5.0.2 → 5.1.1-beta.0

Sign up to get free protection for your applications and to get access to all the features.
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
- );