remix-validated-form 4.5.0-beta.0 → 4.5.1

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 (35) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/browser/ValidatedForm.js +10 -32
  3. package/browser/internal/hooks.d.ts +3 -1
  4. package/browser/internal/hooks.js +2 -0
  5. package/browser/internal/logic/nestedObjectToPathObject.d.ts +1 -0
  6. package/browser/internal/logic/nestedObjectToPathObject.js +47 -0
  7. package/browser/internal/state/arrayUtil.d.ts +12 -0
  8. package/browser/internal/state/arrayUtil.js +337 -0
  9. package/browser/internal/state/createFormStore.d.ts +4 -2
  10. package/browser/internal/state/createFormStore.js +21 -4
  11. package/browser/internal/state/fieldArray.d.ts +28 -0
  12. package/browser/internal/state/fieldArray.js +73 -0
  13. package/browser/internal/state/types.d.ts +0 -0
  14. package/browser/internal/state/types.js +0 -0
  15. package/browser/unreleased/formStateHooks.d.ts +12 -2
  16. package/browser/unreleased/formStateHooks.js +15 -2
  17. package/browser/userFacingFormContext.d.ts +12 -2
  18. package/browser/userFacingFormContext.js +5 -1
  19. package/dist/remix-validated-form.cjs.js +3 -3
  20. package/dist/remix-validated-form.cjs.js.map +1 -1
  21. package/dist/remix-validated-form.es.js +55 -37
  22. package/dist/remix-validated-form.es.js.map +1 -1
  23. package/dist/remix-validated-form.umd.js +3 -3
  24. package/dist/remix-validated-form.umd.js.map +1 -1
  25. package/dist/types/internal/hooks.d.ts +3 -1
  26. package/dist/types/internal/state/createFormStore.d.ts +4 -2
  27. package/dist/types/unreleased/formStateHooks.d.ts +12 -2
  28. package/dist/types/userFacingFormContext.d.ts +12 -2
  29. package/package.json +1 -1
  30. package/src/ValidatedForm.tsx +24 -42
  31. package/src/internal/hooks.ts +6 -0
  32. package/src/internal/state/createFormStore.ts +40 -6
  33. package/src/unreleased/formStateHooks.ts +32 -3
  34. package/src/userFacingFormContext.ts +22 -2
  35. package/src/validation/validation.test.ts +7 -7
@@ -18,7 +18,7 @@ export declare const useInternalIsSubmitting: (formId: InternalFormId) => boolea
18
18
  export declare const useInternalIsValid: (formId: InternalFormId) => boolean;
19
19
  export declare const useInternalHasBeenSubmitted: (formId: InternalFormId) => boolean;
20
20
  export declare const useValidateField: (formId: InternalFormId) => (fieldName: string) => Promise<string | null>;
21
- export declare const useValidate: (formId: InternalFormId) => () => Promise<void>;
21
+ export declare const useValidate: (formId: InternalFormId) => () => Promise<import("..").ValidationResult<unknown>>;
22
22
  export declare const useRegisterReceiveFocus: (formId: InternalFormId) => (fieldName: string, handler: () => void) => () => void;
23
23
  export declare const useSyncedDefaultValues: (formId: InternalFormId) => {
24
24
  [fieldName: string]: any;
@@ -28,5 +28,7 @@ export declare const useTouchedFields: (formId: InternalFormId) => import("..").
28
28
  export declare const useFieldErrors: (formId: InternalFormId) => FieldErrors;
29
29
  export declare const useSetFieldErrors: (formId: InternalFormId) => (errors: FieldErrors) => void;
30
30
  export declare const useResetFormElement: (formId: InternalFormId) => () => void;
31
+ export declare const useSubmitForm: (formId: InternalFormId) => () => void;
31
32
  export declare const useFormActionProp: (formId: InternalFormId) => string | undefined;
32
33
  export declare const useFormSubactionProp: (formId: InternalFormId) => string | undefined;
34
+ export declare const useFormValues: (formId: InternalFormId) => () => FormData;
@@ -1,5 +1,5 @@
1
1
  import { WritableDraft } from "immer/dist/internal";
2
- import { FieldErrors, TouchedFields, Validator } from "../../validation/types";
2
+ import { FieldErrors, TouchedFields, ValidationResult, Validator } from "../../validation/types";
3
3
  import { InternalFormId } from "./types";
4
4
  export declare type SyncedFormProps = {
5
5
  formId?: string;
@@ -39,8 +39,10 @@ export declare type FormState = {
39
39
  setHydrated: () => void;
40
40
  setFormElement: (formElement: HTMLFormElement | null) => void;
41
41
  validateField: (fieldName: string) => Promise<string | null>;
42
- validate: () => Promise<void>;
42
+ validate: () => Promise<ValidationResult<unknown>>;
43
43
  resetFormElement: () => void;
44
+ submit: () => void;
45
+ getValues: () => FormData;
44
46
  };
45
47
  export declare const useRootFormStore: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<FormStoreState>, "setState"> & {
46
48
  setState(nextStateOrUpdater: FormStoreState | Partial<FormStoreState> | ((state: WritableDraft<FormStoreState>) => void), shouldReplace?: boolean | undefined): void;
@@ -1,4 +1,4 @@
1
- import { FieldErrors, TouchedFields } from "../validation/types";
1
+ import { FieldErrors, TouchedFields, ValidationResult } from "../validation/types";
2
2
  export declare type FormState = {
3
3
  fieldErrors: FieldErrors;
4
4
  isSubmitting: boolean;
@@ -33,7 +33,7 @@ export declare type FormHelpers = {
33
33
  /**
34
34
  * Validate the whole form and populate any errors.
35
35
  */
36
- validate: () => Promise<void>;
36
+ validate: () => Promise<ValidationResult<unknown>>;
37
37
  /**
38
38
  * Clears all errors on the form.
39
39
  */
@@ -45,6 +45,16 @@ export declare type FormHelpers = {
45
45
  * or clicking a button element with `type="reset"`.
46
46
  */
47
47
  reset: () => void;
48
+ /**
49
+ * Submits the form, running all validations first.
50
+ *
51
+ * _Note_: This is equivalent to clicking a button element with `type="submit"` or calling formElement.submit().
52
+ */
53
+ submit: () => void;
54
+ /**
55
+ * Returns the current form values as FormData
56
+ */
57
+ getValues: () => FormData;
48
58
  };
49
59
  /**
50
60
  * Returns helpers that can be used to update the form state.
@@ -1,4 +1,4 @@
1
- import { FieldErrors, TouchedFields } from "./validation/types";
1
+ import { FieldErrors, TouchedFields, ValidationResult } from "./validation/types";
2
2
  export declare type FormContextValue = {
3
3
  /**
4
4
  * All the errors in all the fields in the form.
@@ -56,7 +56,7 @@ export declare type FormContextValue = {
56
56
  /**
57
57
  * Validate the whole form and populate any errors.
58
58
  */
59
- validate: () => Promise<void>;
59
+ validate: () => Promise<ValidationResult<unknown>>;
60
60
  /**
61
61
  * Clears all errors on the form.
62
62
  */
@@ -68,6 +68,16 @@ export declare type FormContextValue = {
68
68
  * or clicking a button element with `type="reset"`.
69
69
  */
70
70
  reset: () => void;
71
+ /**
72
+ * Submits the form, running all validations first.
73
+ *
74
+ * _Note_: This is equivalent to clicking a button element with `type="submit"` or calling formElement.submit().
75
+ */
76
+ submit: () => void;
77
+ /**
78
+ * Returns the current form values as FormData
79
+ */
80
+ getValues: () => FormData;
71
81
  };
72
82
  /**
73
83
  * Provides access to some of the internal state of the form.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remix-validated-form",
3
- "version": "4.5.0-beta.0",
3
+ "version": "4.5.1",
4
4
  "description": "Form component and utils for easy form validation in remix",
5
5
  "browser": "./dist/remix-validated-form.cjs.js",
6
6
  "main": "./dist/remix-validated-form.umd.js",
@@ -98,7 +98,8 @@ const focusFirstInvalidInput = (
98
98
  const namesInOrder = [...formElement.elements]
99
99
  .map((el) => {
100
100
  const input = el instanceof RadioNodeList ? el[0] : el;
101
- if (input instanceof HTMLInputElement) return input.name;
101
+ if (input instanceof HTMLElement && "name" in input)
102
+ return (input as any).name;
102
103
  return null;
103
104
  })
104
105
  .filter(nonNull)
@@ -129,8 +130,8 @@ const focusFirstInvalidInput = (
129
130
  }
130
131
  }
131
132
 
132
- if (elem instanceof HTMLInputElement) {
133
- if (elem.type === "hidden") {
133
+ if (elem instanceof HTMLElement) {
134
+ if (elem instanceof HTMLInputElement && elem.type === "hidden") {
134
135
  continue;
135
136
  }
136
137
 
@@ -296,33 +297,11 @@ export function ValidatedForm<DataType>({
296
297
  endSubmit();
297
298
  });
298
299
 
299
- let clickedButtonRef = React.useRef<any>();
300
- useEffect(() => {
301
- let form = formRef.current;
302
- if (!form) return;
303
-
304
- function handleClick(event: MouseEvent) {
305
- if (!(event.target instanceof HTMLElement)) return;
306
- let submitButton = event.target.closest<
307
- HTMLButtonElement | HTMLInputElement
308
- >("button,input[type=submit]");
309
-
310
- if (
311
- submitButton &&
312
- submitButton.form === form &&
313
- submitButton.type === "submit"
314
- ) {
315
- clickedButtonRef.current = submitButton;
316
- }
317
- }
318
-
319
- window.addEventListener("click", handleClick, { capture: true });
320
- return () => {
321
- window.removeEventListener("click", handleClick, { capture: true });
322
- };
323
- }, []);
324
-
325
- const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
300
+ const handleSubmit = async (
301
+ e: FormEvent<HTMLFormElement>,
302
+ target: typeof e.currentTarget,
303
+ nativeEvent: HTMLSubmitEvent["nativeEvent"]
304
+ ) => {
326
305
  startSubmit();
327
306
  const result = await validator.validate(getDataFromForm(e.currentTarget));
328
307
  if (result.error) {
@@ -343,8 +322,7 @@ export function ValidatedForm<DataType>({
343
322
  return;
344
323
  }
345
324
 
346
- const submitter = (e as unknown as HTMLSubmitEvent).nativeEvent
347
- .submitter as HTMLFormSubmitter | null;
325
+ const submitter = nativeEvent.submitter as HTMLFormSubmitter | null;
348
326
 
349
327
  // We deviate from the remix code here a bit because of our async submit.
350
328
  // In remix's `FormImpl`, they use `event.currentTarget` to get the form,
@@ -352,9 +330,7 @@ export function ValidatedForm<DataType>({
352
330
  // If we use `event.currentTarget` here, it will break because `currentTarget`
353
331
  // will have changed since the start of the submission.
354
332
  if (fetcher) fetcher.submit(submitter || e.currentTarget);
355
- else submit(submitter || e.currentTarget, { method, replace });
356
-
357
- clickedButtonRef.current = null;
333
+ else submit(submitter || target, { replace });
358
334
  }
359
335
  };
360
336
 
@@ -368,7 +344,11 @@ export function ValidatedForm<DataType>({
368
344
  replace={replace}
369
345
  onSubmit={(e) => {
370
346
  e.preventDefault();
371
- handleSubmit(e);
347
+ handleSubmit(
348
+ e,
349
+ e.currentTarget,
350
+ (e as unknown as HTMLSubmitEvent).nativeEvent
351
+ );
372
352
  }}
373
353
  onReset={(event) => {
374
354
  onReset?.(event);
@@ -378,12 +358,14 @@ export function ValidatedForm<DataType>({
378
358
  }}
379
359
  >
380
360
  <InternalFormContext.Provider value={contextValue}>
381
- <FormResetter formRef={formRef} resetAfterSubmit={resetAfterSubmit} />
382
- {subaction && (
383
- <input type="hidden" value={subaction} name="subaction" />
384
- )}
385
- {id && <input type="hidden" value={id} name={FORM_ID_FIELD} />}
386
- {children}
361
+ <>
362
+ <FormResetter formRef={formRef} resetAfterSubmit={resetAfterSubmit} />
363
+ {subaction && (
364
+ <input type="hidden" value={subaction} name="subaction" />
365
+ )}
366
+ {id && <input type="hidden" value={id} name={FORM_ID_FIELD} />}
367
+ {children}
368
+ </>
387
369
  </InternalFormContext.Provider>
388
370
  </Form>
389
371
  );
@@ -196,8 +196,14 @@ export const useSetFieldErrors = (formId: InternalFormId) =>
196
196
  export const useResetFormElement = (formId: InternalFormId) =>
197
197
  useFormStore(formId, (state) => state.resetFormElement);
198
198
 
199
+ export const useSubmitForm = (formId: InternalFormId) =>
200
+ useFormStore(formId, (state) => state.submit);
201
+
199
202
  export const useFormActionProp = (formId: InternalFormId) =>
200
203
  useFormStore(formId, (state) => state.formProps?.action);
201
204
 
202
205
  export const useFormSubactionProp = (formId: InternalFormId) =>
203
206
  useFormStore(formId, (state) => state.formProps?.subaction);
207
+
208
+ export const useFormValues = (formId: InternalFormId) =>
209
+ useFormStore(formId, (state) => state.getValues);
@@ -2,7 +2,12 @@ import { WritableDraft } from "immer/dist/internal";
2
2
  import invariant from "tiny-invariant";
3
3
  import create, { GetState } from "zustand";
4
4
  import { immer } from "zustand/middleware/immer";
5
- import { FieldErrors, TouchedFields, Validator } from "../../validation/types";
5
+ import {
6
+ FieldErrors,
7
+ TouchedFields,
8
+ ValidationResult,
9
+ Validator,
10
+ } from "../../validation/types";
6
11
  import { useControlledFieldStore } from "./controlledFieldStore";
7
12
  import { InternalFormId } from "./types";
8
13
 
@@ -43,8 +48,10 @@ export type FormState = {
43
48
  setHydrated: () => void;
44
49
  setFormElement: (formElement: HTMLFormElement | null) => void;
45
50
  validateField: (fieldName: string) => Promise<string | null>;
46
- validate: () => Promise<void>;
51
+ validate: () => Promise<ValidationResult<unknown>>;
47
52
  resetFormElement: () => void;
53
+ submit: () => void;
54
+ getValues: () => FormData;
48
55
  };
49
56
 
50
57
  const noOp = () => {};
@@ -69,9 +76,16 @@ const defaultFormState: FormState = {
69
76
  setFormElement: noOp,
70
77
  validateField: async () => null,
71
78
 
72
- validate: async () => {},
79
+ validate: async () => {
80
+ throw new Error("Validate called before form was initialized.");
81
+ },
82
+
83
+ submit: async () => {
84
+ throw new Error("Submit called before form was initialized.");
85
+ },
73
86
 
74
87
  resetFormElement: noOp,
88
+ getValues: () => new FormData(),
75
89
  };
76
90
 
77
91
  const createFormState = (
@@ -113,7 +127,6 @@ const createFormState = (
113
127
  set((state) => {
114
128
  delete state.fieldErrors[fieldName];
115
129
  }),
116
-
117
130
  reset: () =>
118
131
  set((state) => {
119
132
  state.fieldErrors = {};
@@ -181,8 +194,29 @@ const createFormState = (
181
194
  "Cannot validator. This is probably a bug in remix-validated-form."
182
195
  );
183
196
 
184
- const { error } = await validator.validate(new FormData(formElement));
185
- if (error) get().setFieldErrors(error.fieldErrors);
197
+ const result = await validator.validate(new FormData(formElement));
198
+ if (result.error) get().setFieldErrors(result.error.fieldErrors);
199
+ return result;
200
+ },
201
+
202
+ submit: () => {
203
+ const formElement = get().formElement;
204
+ invariant(
205
+ formElement,
206
+ "Cannot find reference to form. This is probably a bug in remix-validated-form."
207
+ );
208
+
209
+ formElement.submit();
210
+ },
211
+
212
+ getValues: () => {
213
+ const formElement = get().formElement;
214
+ invariant(
215
+ formElement,
216
+ "Cannot find reference to form. This is probably a bug in remix-validated-form."
217
+ );
218
+
219
+ return new FormData(formElement);
186
220
  },
187
221
 
188
222
  resetFormElement: () => get().formElement?.reset(),
@@ -18,8 +18,14 @@ import {
18
18
  useSyncedDefaultValues,
19
19
  useFormActionProp,
20
20
  useFormSubactionProp,
21
+ useSubmitForm,
22
+ useFormValues,
21
23
  } from "../internal/hooks";
22
- import { FieldErrors, TouchedFields } from "../validation/types";
24
+ import {
25
+ FieldErrors,
26
+ TouchedFields,
27
+ ValidationResult,
28
+ } from "../validation/types";
23
29
 
24
30
  export type FormState = {
25
31
  fieldErrors: FieldErrors;
@@ -95,7 +101,7 @@ export type FormHelpers = {
95
101
  /**
96
102
  * Validate the whole form and populate any errors.
97
103
  */
98
- validate: () => Promise<void>;
104
+ validate: () => Promise<ValidationResult<unknown>>;
99
105
  /**
100
106
  * Clears all errors on the form.
101
107
  */
@@ -107,6 +113,16 @@ export type FormHelpers = {
107
113
  * or clicking a button element with `type="reset"`.
108
114
  */
109
115
  reset: () => void;
116
+ /**
117
+ * Submits the form, running all validations first.
118
+ *
119
+ * _Note_: This is equivalent to clicking a button element with `type="submit"` or calling formElement.submit().
120
+ */
121
+ submit: () => void;
122
+ /**
123
+ * Returns the current form values as FormData
124
+ */
125
+ getValues: () => FormData;
110
126
  };
111
127
 
112
128
  /**
@@ -122,6 +138,8 @@ export const useFormHelpers = (formId?: string): FormHelpers => {
122
138
  const clearError = useClearError(formContext);
123
139
  const setFieldErrors = useSetFieldErrors(formContext.formId);
124
140
  const reset = useResetFormElement(formContext.formId);
141
+ const submit = useSubmitForm(formContext.formId);
142
+ const getValues = useFormValues(formContext.formId);
125
143
  return useMemo(
126
144
  () => ({
127
145
  setTouched,
@@ -130,7 +148,18 @@ export const useFormHelpers = (formId?: string): FormHelpers => {
130
148
  validate,
131
149
  clearAllErrors: () => setFieldErrors({}),
132
150
  reset,
151
+ submit,
152
+ getValues,
133
153
  }),
134
- [clearError, reset, setFieldErrors, setTouched, validate, validateField]
154
+ [
155
+ clearError,
156
+ reset,
157
+ setFieldErrors,
158
+ setTouched,
159
+ submit,
160
+ validate,
161
+ validateField,
162
+ getValues,
163
+ ]
135
164
  );
136
165
  };
@@ -4,7 +4,11 @@ import {
4
4
  useRegisterReceiveFocus,
5
5
  } from "./internal/hooks";
6
6
  import { useFormHelpers, useFormState } from "./unreleased/formStateHooks";
7
- import { FieldErrors, TouchedFields } from "./validation/types";
7
+ import {
8
+ FieldErrors,
9
+ TouchedFields,
10
+ ValidationResult,
11
+ } from "./validation/types";
8
12
 
9
13
  export type FormContextValue = {
10
14
  /**
@@ -61,7 +65,7 @@ export type FormContextValue = {
61
65
  /**
62
66
  * Validate the whole form and populate any errors.
63
67
  */
64
- validate: () => Promise<void>;
68
+ validate: () => Promise<ValidationResult<unknown>>;
65
69
  /**
66
70
  * Clears all errors on the form.
67
71
  */
@@ -73,6 +77,16 @@ export type FormContextValue = {
73
77
  * or clicking a button element with `type="reset"`.
74
78
  */
75
79
  reset: () => void;
80
+ /**
81
+ * Submits the form, running all validations first.
82
+ *
83
+ * _Note_: This is equivalent to clicking a button element with `type="submit"` or calling formElement.submit().
84
+ */
85
+ submit: () => void;
86
+ /**
87
+ * Returns the current form values as FormData
88
+ */
89
+ getValues: () => FormData;
76
90
  };
77
91
 
78
92
  /**
@@ -89,6 +103,8 @@ export const useFormContext = (formId?: string): FormContextValue => {
89
103
  clearAllErrors,
90
104
  validate,
91
105
  reset,
106
+ submit,
107
+ getValues,
92
108
  } = useFormHelpers(formId);
93
109
 
94
110
  const registerReceiveFocus = useRegisterReceiveFocus(context.formId);
@@ -112,6 +128,8 @@ export const useFormContext = (formId?: string): FormContextValue => {
112
128
  clearAllErrors,
113
129
  validate,
114
130
  reset,
131
+ submit,
132
+ getValues,
115
133
  }),
116
134
  [
117
135
  clearAllErrors,
@@ -120,8 +138,10 @@ export const useFormContext = (formId?: string): FormContextValue => {
120
138
  reset,
121
139
  setTouched,
122
140
  state,
141
+ submit,
123
142
  validate,
124
143
  validateField,
144
+ getValues,
125
145
  ]
126
146
  );
127
147
  };
@@ -56,21 +56,21 @@ const validationTestCases: ValidationTestCase[] = [
56
56
  name: "zod",
57
57
  validator: withZod(
58
58
  z.object({
59
- firstName: z.string().nonempty(),
60
- lastName: z.string().nonempty(),
59
+ firstName: z.string().min(1),
60
+ lastName: z.string().min(1),
61
61
  age: z.optional(z.number()),
62
62
  address: z.preprocess(
63
63
  (value) => (value == null ? {} : value),
64
64
  z.object({
65
- streetAddress: z.string().nonempty(),
66
- city: z.string().nonempty(),
67
- country: z.string().nonempty(),
65
+ streetAddress: z.string().min(1),
66
+ city: z.string().min(1),
67
+ country: z.string().min(1),
68
68
  })
69
69
  ),
70
70
  pets: z
71
71
  .object({
72
- animal: z.string().nonempty(),
73
- name: z.string().nonempty(),
72
+ animal: z.string().min(1),
73
+ name: z.string().min(1),
74
74
  })
75
75
  .array()
76
76
  .optional(),