remix-validated-form 4.1.0-beta.1 → 4.1.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 (37) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/browser/ValidatedForm.d.ts +2 -2
  3. package/browser/ValidatedForm.js +53 -23
  4. package/browser/hooks.js +9 -3
  5. package/browser/internal/MultiValueMap.js +3 -3
  6. package/browser/internal/hooks.d.ts +0 -1
  7. package/browser/internal/hooks.js +0 -4
  8. package/browser/internal/logic/getCheckboxChecked copy.d.ts +1 -0
  9. package/browser/internal/logic/getCheckboxChecked copy.js +9 -0
  10. package/browser/internal/logic/getCheckboxChecked.d.ts +1 -0
  11. package/browser/internal/logic/getCheckboxChecked.js +9 -0
  12. package/browser/internal/logic/getRadioChecked.d.ts +1 -0
  13. package/browser/internal/logic/getRadioChecked.js +5 -0
  14. package/browser/internal/logic/setFieldValue.d.ts +1 -0
  15. package/browser/internal/logic/setFieldValue.js +40 -0
  16. package/browser/internal/util.d.ts +1 -0
  17. package/browser/internal/util.js +12 -1
  18. package/build/ValidatedForm.d.ts +2 -2
  19. package/build/ValidatedForm.js +52 -22
  20. package/build/hooks.js +8 -2
  21. package/build/internal/MultiValueMap.js +2 -2
  22. package/build/internal/hooks.d.ts +0 -1
  23. package/build/internal/hooks.js +1 -6
  24. package/build/internal/logic/getCheckboxChecked.d.ts +1 -0
  25. package/build/internal/logic/getCheckboxChecked.js +13 -0
  26. package/build/internal/logic/getRadioChecked.d.ts +1 -0
  27. package/build/internal/logic/getRadioChecked.js +9 -0
  28. package/build/internal/logic/setFieldValue.d.ts +1 -0
  29. package/build/internal/logic/setFieldValue.js +47 -0
  30. package/build/internal/util.d.ts +1 -0
  31. package/build/internal/util.js +13 -1
  32. package/package.json +1 -1
  33. package/src/ValidatedForm.tsx +64 -29
  34. package/src/hooks.ts +8 -5
  35. package/src/internal/MultiValueMap.ts +3 -3
  36. package/src/internal/hooks.ts +0 -9
  37. package/src/internal/util.ts +13 -1
@@ -1,9 +1,9 @@
1
1
  $ npm run build:browser && npm run build:main
2
2
 
3
- > remix-validated-form@4.0.2 build:browser
3
+ > remix-validated-form@4.1.0 build:browser
4
4
  > tsc --module ESNext --outDir ./browser
5
5
 
6
6
 
7
- > remix-validated-form@4.0.2 build:main
7
+ > remix-validated-form@4.1.0 build:main
8
8
  > tsc --module CommonJS --outDir ./build
9
9
 
@@ -10,7 +10,7 @@ export declare type FormProps<DataType> = {
10
10
  * A submit callback that gets called when the form is submitted
11
11
  * after all validations have been run.
12
12
  */
13
- onSubmit?: (data: DataType, event: React.FormEvent<HTMLFormElement>) => Promise<void>;
13
+ onSubmit?: (data: DataType, event: React.FormEvent<HTMLFormElement>) => void | Promise<void>;
14
14
  /**
15
15
  * Allows you to provide a `fetcher` from remix's `useFetcher` hook.
16
16
  * The form will use the fetcher for loading states, action data, etc
@@ -47,4 +47,4 @@ export declare type FormProps<DataType> = {
47
47
  /**
48
48
  * The primary form component of `remix-validated-form`.
49
49
  */
50
- export declare function ValidatedForm<DataType>({ validator, onSubmit, children, fetcher, action, defaultValues: providedDefaultValues, formRef: formRefProp, onReset, subaction, resetAfterSubmit, disableFocusOnError, method, replace, id, ...rest }: FormProps<DataType>): JSX.Element;
50
+ export declare function ValidatedForm<DataType>({ validator, onSubmit, children, fetcher, action, defaultValues: unMemoizedDefaults, formRef: formRefProp, onReset, subaction, resetAfterSubmit, disableFocusOnError, method, replace, id, ...rest }: FormProps<DataType>): JSX.Element;
@@ -10,7 +10,7 @@ import { useDefaultValuesFromLoader, useErrorResponseForForm, useFormUpdateAtom,
10
10
  import { useMultiValueMap } from "./internal/MultiValueMap";
11
11
  import { addErrorAtom, clearErrorAtom, endSubmitAtom, formRegistry, resetAtom, setFieldErrorsAtom, startSubmitAtom, syncFormContextAtom, } from "./internal/state";
12
12
  import { useSubmitComplete } from "./internal/submissionCallbacks";
13
- import { mergeRefs, useIsomorphicLayoutEffect as useLayoutEffect, } from "./internal/util";
13
+ import { mergeRefs, useDeepEqualsMemo, useIsomorphicLayoutEffect as useLayoutEffect, } from "./internal/util";
14
14
  const getDataFromForm = (el) => new FormData(el);
15
15
  function nonNull(value) {
16
16
  return value !== null;
@@ -75,13 +75,35 @@ const FormResetter = ({ resetAfterSubmit, formRef, }) => {
75
75
  });
76
76
  return null;
77
77
  };
78
+ function formEventProxy(event) {
79
+ let defaultPrevented = false;
80
+ return new Proxy(event, {
81
+ get: (target, prop) => {
82
+ if (prop === "preventDefault") {
83
+ return () => {
84
+ defaultPrevented = true;
85
+ };
86
+ }
87
+ if (prop === "defaultPrevented") {
88
+ return defaultPrevented;
89
+ }
90
+ return target[prop];
91
+ },
92
+ });
93
+ }
94
+ const useFormAtom = (formId) => {
95
+ const formAtom = formRegistry(formId);
96
+ useEffect(() => () => formRegistry.remove(formId), [formId]);
97
+ return formAtom;
98
+ };
78
99
  /**
79
100
  * The primary form component of `remix-validated-form`.
80
101
  */
81
- export function ValidatedForm({ validator, onSubmit, children, fetcher, action, defaultValues: providedDefaultValues, formRef: formRefProp, onReset, subaction, resetAfterSubmit = false, disableFocusOnError, method, replace, id, ...rest }) {
102
+ export function ValidatedForm({ validator, onSubmit, children, fetcher, action, defaultValues: unMemoizedDefaults, formRef: formRefProp, onReset, subaction, resetAfterSubmit = false, disableFocusOnError, method, replace, id, ...rest }) {
82
103
  var _a;
83
104
  const formId = useFormId(id);
84
- const formAtom = formRegistry(formId);
105
+ const formAtom = useFormAtom(formId);
106
+ const providedDefaultValues = useDeepEqualsMemo(unMemoizedDefaults);
85
107
  const contextValue = useMemo(() => ({
86
108
  formId,
87
109
  action,
@@ -170,28 +192,36 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
170
192
  window.removeEventListener("click", handleClick);
171
193
  };
172
194
  }, []);
173
- return (_jsx(Form, { ref: mergeRefs([formRef, formRefProp]), ...rest, id: id, action: action, method: method, replace: replace, onSubmit: async (e) => {
174
- e.preventDefault();
175
- startSubmit({ formAtom });
176
- const result = await validator.validate(getDataFromForm(e.currentTarget));
177
- if (result.error) {
178
- endSubmit({ formAtom });
179
- setFieldErrors({ fieldErrors: result.error.fieldErrors, formAtom });
180
- if (!disableFocusOnError) {
181
- focusFirstInvalidInput(result.error.fieldErrors, customFocusHandlers(), formRef.current);
182
- }
195
+ const handleSubmit = async (e) => {
196
+ startSubmit({ formAtom });
197
+ const result = await validator.validate(getDataFromForm(e.currentTarget));
198
+ if (result.error) {
199
+ endSubmit({ formAtom });
200
+ setFieldErrors({ fieldErrors: result.error.fieldErrors, formAtom });
201
+ if (!disableFocusOnError) {
202
+ focusFirstInvalidInput(result.error.fieldErrors, customFocusHandlers(), formRef.current);
183
203
  }
184
- else {
185
- onSubmit === null || onSubmit === void 0 ? void 0 : onSubmit(result.data, e);
186
- if (fetcher)
187
- fetcher.submit(clickedButtonRef.current || e.currentTarget);
188
- else
189
- submit(clickedButtonRef.current || e.currentTarget, {
190
- method,
191
- replace,
192
- });
193
- clickedButtonRef.current = null;
204
+ }
205
+ else {
206
+ const eventProxy = formEventProxy(e);
207
+ await (onSubmit === null || onSubmit === void 0 ? void 0 : onSubmit(result.data, eventProxy));
208
+ if (eventProxy.defaultPrevented) {
209
+ endSubmit({ formAtom });
210
+ return;
194
211
  }
212
+ if (fetcher)
213
+ fetcher.submit(clickedButtonRef.current || e.currentTarget);
214
+ else
215
+ submit(clickedButtonRef.current || e.currentTarget, {
216
+ method,
217
+ replace,
218
+ });
219
+ clickedButtonRef.current = null;
220
+ }
221
+ };
222
+ return (_jsx(Form, { ref: mergeRefs([formRef, formRefProp]), ...rest, id: id, action: action, method: method, replace: replace, onSubmit: (e) => {
223
+ e.preventDefault();
224
+ handleSubmit(e);
195
225
  }, onReset: (event) => {
196
226
  onReset === null || onReset === void 0 ? void 0 : onReset(event);
197
227
  if (event.defaultPrevented)
package/browser/hooks.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useMemo } from "react";
2
2
  import { createGetInputProps, } from "./internal/getInputProps";
3
- import { useUnknownFormContextSelectAtom, useInternalFormContext, useFieldTouched, useFieldError, useFieldDefaultValue, useContextSelectAtom, useClearError, useSetTouched, } from "./internal/hooks";
3
+ import { useInternalFormContext, useFieldTouched, useFieldError, useFieldDefaultValue, useContextSelectAtom, useClearError, useSetTouched, } from "./internal/hooks";
4
4
  import { hasBeenSubmittedAtom, isSubmittingAtom, isValidAtom, registerReceiveFocusAtom, validateFieldAtom, } from "./internal/state";
5
5
  /**
6
6
  * Returns whether or not the parent form is currently being submitted.
@@ -9,13 +9,19 @@ import { hasBeenSubmittedAtom, isSubmittingAtom, isValidAtom, registerReceiveFoc
9
9
  *
10
10
  * @param formId
11
11
  */
12
- export const useIsSubmitting = (formId) => useUnknownFormContextSelectAtom(formId, isSubmittingAtom, "useIsSubmitting");
12
+ export const useIsSubmitting = (formId) => {
13
+ const formContext = useInternalFormContext(formId, "useIsSubmitting");
14
+ return useContextSelectAtom(formContext.formId, isSubmittingAtom);
15
+ };
13
16
  /**
14
17
  * Returns whether or not the current form is valid.
15
18
  *
16
19
  * @param formId the id of the form. Only necessary if being used outside a ValidatedForm.
17
20
  */
18
- export const useIsValid = (formId) => useUnknownFormContextSelectAtom(formId, isValidAtom, "useIsValid");
21
+ export const useIsValid = (formId) => {
22
+ const formContext = useInternalFormContext(formId, "useIsValid");
23
+ return useContextSelectAtom(formContext.formId, isValidAtom);
24
+ };
19
25
  /**
20
26
  * Provides the data and helpers necessary to set up a field.
21
27
  */
@@ -1,4 +1,4 @@
1
- import { useRef } from "react";
1
+ import { useCallback, useRef } from "react";
2
2
  export class MultiValueMap {
3
3
  constructor() {
4
4
  this.dict = new Map();
@@ -30,10 +30,10 @@ export class MultiValueMap {
30
30
  }
31
31
  export const useMultiValueMap = () => {
32
32
  const ref = useRef(null);
33
- return () => {
33
+ return useCallback(() => {
34
34
  if (ref.current)
35
35
  return ref.current;
36
36
  ref.current = new MultiValueMap();
37
37
  return ref.current;
38
- };
38
+ }, []);
39
39
  };
@@ -7,7 +7,6 @@ declare type FormSelectorAtomCreator<T> = (formState: FormAtom) => Atom<T>;
7
7
  declare const USE_HYDRATED_STATE: unique symbol;
8
8
  export declare const useInternalFormContext: (formId?: string | symbol | undefined, hookName?: string | undefined) => InternalFormContextValue;
9
9
  export declare const useContextSelectAtom: <T>(formId: string | symbol, selectorAtomCreator: FormSelectorAtomCreator<T>) => T extends Promise<infer V> ? V : T;
10
- export declare const useUnknownFormContextSelectAtom: <T>(formId: string | symbol | undefined, selectorAtomCreator: FormSelectorAtomCreator<T>, hookName: string) => T extends Promise<infer V> ? V : T;
11
10
  export declare const useHydratableSelector: <T, U>({ formId }: InternalFormContextValue, atomCreator: FormSelectorAtomCreator<T>, dataToUse: U | typeof USE_HYDRATED_STATE, selector?: (data: U) => T) => T | (T extends Promise<infer V> ? V : T);
12
11
  export declare function useErrorResponseForForm({ fetcher, subaction, formId, }: InternalFormContextValue): ValidationErrorResponseData | null;
13
12
  export declare const useFieldErrorsForForm: (context: InternalFormContextValue) => import("..").FieldErrors | typeof USE_HYDRATED_STATE | undefined;
@@ -20,10 +20,6 @@ export const useContextSelectAtom = (formId, selectorAtomCreator) => {
20
20
  const selectorAtom = useMemo(() => selectorAtomCreator(formAtom), [formAtom, selectorAtomCreator]);
21
21
  return useAtomValue(selectorAtom, ATOM_SCOPE);
22
22
  };
23
- export const useUnknownFormContextSelectAtom = (formId, selectorAtomCreator, hookName) => {
24
- const formContext = useInternalFormContext(formId, hookName);
25
- return useContextSelectAtom(formContext.formId, selectorAtomCreator);
26
- };
27
23
  export const useHydratableSelector = ({ formId }, atomCreator, dataToUse, selector = identity) => {
28
24
  const dataFromState = useContextSelectAtom(formId, atomCreator);
29
25
  return dataToUse === USE_HYDRATED_STATE ? dataFromState : selector(dataToUse);
@@ -0,0 +1 @@
1
+ export declare const getCheckboxChecked: (checkboxValue: string | undefined, newValue: unknown) => boolean | undefined;
@@ -0,0 +1,9 @@
1
+ export const getCheckboxChecked = (checkboxValue = "on", newValue) => {
2
+ if (Array.isArray(newValue))
3
+ return newValue.includes(checkboxValue);
4
+ if (typeof newValue === "boolean")
5
+ return newValue;
6
+ if (typeof newValue === "string")
7
+ return newValue === checkboxValue;
8
+ return undefined;
9
+ };
@@ -0,0 +1 @@
1
+ export declare const getCheckboxChecked: (checkboxValue: string | undefined, newValue: unknown) => boolean | undefined;
@@ -0,0 +1,9 @@
1
+ export const getCheckboxChecked = (checkboxValue = "on", newValue) => {
2
+ if (Array.isArray(newValue))
3
+ return newValue.includes(checkboxValue);
4
+ if (typeof newValue === "boolean")
5
+ return newValue;
6
+ if (typeof newValue === "string")
7
+ return newValue === checkboxValue;
8
+ return undefined;
9
+ };
@@ -0,0 +1 @@
1
+ export declare const getRadioChecked: (radioValue: string | undefined, newValue: unknown) => boolean | undefined;
@@ -0,0 +1,5 @@
1
+ export const getRadioChecked = (radioValue = "on", newValue) => {
2
+ if (typeof newValue === "string")
3
+ return newValue === radioValue;
4
+ return undefined;
5
+ };
@@ -0,0 +1 @@
1
+ export declare const setFieldValue: (formElement: HTMLFormElement, name: string, value: unknown) => void;
@@ -0,0 +1,40 @@
1
+ import invariant from "tiny-invariant";
2
+ import { getCheckboxChecked } from "./getCheckboxChecked";
3
+ import { getRadioChecked } from "./getRadioChecked";
4
+ const setElementValue = (element, value, name) => {
5
+ if (element instanceof HTMLSelectElement && element.multiple) {
6
+ invariant(Array.isArray(value), "Must specify an array to set the value for a multi-select");
7
+ for (const option of element.options) {
8
+ option.selected = value.includes(option.value);
9
+ }
10
+ return;
11
+ }
12
+ if (element instanceof HTMLInputElement && element.type === "checkbox") {
13
+ const newChecked = getCheckboxChecked(element.value, value);
14
+ invariant(newChecked !== undefined, `Unable to determine if checkbox should be checked. Provided value was ${value} for checkbox ${name}.`);
15
+ element.checked = newChecked;
16
+ return;
17
+ }
18
+ if (element instanceof HTMLInputElement && element.type === "radio") {
19
+ const newChecked = getRadioChecked(element.value, value);
20
+ invariant(newChecked !== undefined, `Unable to determine if radio should be checked. Provided value was ${value} for radio ${name}.`);
21
+ element.checked = newChecked;
22
+ return;
23
+ }
24
+ invariant(typeof value === "string", `Invalid value for field "${name}" which is an ${element.constructor.name}. Expected string but received ${typeof value}`);
25
+ const input = element;
26
+ input.value = value;
27
+ };
28
+ export const setFieldValue = (formElement, name, value) => {
29
+ const controlElement = formElement.elements.namedItem(name);
30
+ if (!controlElement)
31
+ return;
32
+ if (controlElement instanceof RadioNodeList) {
33
+ for (const element of controlElement) {
34
+ setElementValue(element, value, name);
35
+ }
36
+ }
37
+ else {
38
+ setElementValue(controlElement, value, name);
39
+ }
40
+ };
@@ -2,3 +2,4 @@ import type React from "react";
2
2
  export declare const omit: (obj: any, ...keys: string[]) => any;
3
3
  export declare const mergeRefs: <T = any>(refs: (React.MutableRefObject<T> | React.LegacyRef<T> | undefined)[]) => (instance: T | null) => void;
4
4
  export declare const useIsomorphicLayoutEffect: typeof React.useEffect;
5
+ export declare const useDeepEqualsMemo: <T>(item: T) => T;
@@ -1,4 +1,5 @@
1
- import { useEffect, useLayoutEffect } from "react";
1
+ import { isEqual } from "lodash";
2
+ import { useEffect, useLayoutEffect, useRef } from "react";
2
3
  export const omit = (obj, ...keys) => {
3
4
  const result = { ...obj };
4
5
  for (const key of keys) {
@@ -19,3 +20,13 @@ export const mergeRefs = (refs) => {
19
20
  };
20
21
  };
21
22
  export const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
23
+ export const useDeepEqualsMemo = (item) => {
24
+ const ref = useRef(item);
25
+ const areEqual = ref.current === item || isEqual(ref.current, item);
26
+ useEffect(() => {
27
+ if (!areEqual) {
28
+ ref.current = item;
29
+ }
30
+ });
31
+ return areEqual ? ref.current : item;
32
+ };
@@ -10,7 +10,7 @@ export declare type FormProps<DataType> = {
10
10
  * A submit callback that gets called when the form is submitted
11
11
  * after all validations have been run.
12
12
  */
13
- onSubmit?: (data: DataType, event: React.FormEvent<HTMLFormElement>) => Promise<void>;
13
+ onSubmit?: (data: DataType, event: React.FormEvent<HTMLFormElement>) => void | Promise<void>;
14
14
  /**
15
15
  * Allows you to provide a `fetcher` from remix's `useFetcher` hook.
16
16
  * The form will use the fetcher for loading states, action data, etc
@@ -47,4 +47,4 @@ export declare type FormProps<DataType> = {
47
47
  /**
48
48
  * The primary form component of `remix-validated-form`.
49
49
  */
50
- export declare function ValidatedForm<DataType>({ validator, onSubmit, children, fetcher, action, defaultValues: providedDefaultValues, formRef: formRefProp, onReset, subaction, resetAfterSubmit, disableFocusOnError, method, replace, id, ...rest }: FormProps<DataType>): JSX.Element;
50
+ export declare function ValidatedForm<DataType>({ validator, onSubmit, children, fetcher, action, defaultValues: unMemoizedDefaults, formRef: formRefProp, onReset, subaction, resetAfterSubmit, disableFocusOnError, method, replace, id, ...rest }: FormProps<DataType>): JSX.Element;
@@ -100,13 +100,35 @@ const FormResetter = ({ resetAfterSubmit, formRef, }) => {
100
100
  });
101
101
  return null;
102
102
  };
103
+ function formEventProxy(event) {
104
+ let defaultPrevented = false;
105
+ return new Proxy(event, {
106
+ get: (target, prop) => {
107
+ if (prop === "preventDefault") {
108
+ return () => {
109
+ defaultPrevented = true;
110
+ };
111
+ }
112
+ if (prop === "defaultPrevented") {
113
+ return defaultPrevented;
114
+ }
115
+ return target[prop];
116
+ },
117
+ });
118
+ }
119
+ const useFormAtom = (formId) => {
120
+ const formAtom = (0, state_1.formRegistry)(formId);
121
+ (0, react_2.useEffect)(() => () => state_1.formRegistry.remove(formId), [formId]);
122
+ return formAtom;
123
+ };
103
124
  /**
104
125
  * The primary form component of `remix-validated-form`.
105
126
  */
106
- function ValidatedForm({ validator, onSubmit, children, fetcher, action, defaultValues: providedDefaultValues, formRef: formRefProp, onReset, subaction, resetAfterSubmit = false, disableFocusOnError, method, replace, id, ...rest }) {
127
+ function ValidatedForm({ validator, onSubmit, children, fetcher, action, defaultValues: unMemoizedDefaults, formRef: formRefProp, onReset, subaction, resetAfterSubmit = false, disableFocusOnError, method, replace, id, ...rest }) {
107
128
  var _a;
108
129
  const formId = useFormId(id);
109
- const formAtom = (0, state_1.formRegistry)(formId);
130
+ const formAtom = useFormAtom(formId);
131
+ const providedDefaultValues = (0, util_1.useDeepEqualsMemo)(unMemoizedDefaults);
110
132
  const contextValue = (0, react_2.useMemo)(() => ({
111
133
  formId,
112
134
  action,
@@ -195,28 +217,36 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
195
217
  window.removeEventListener("click", handleClick);
196
218
  };
197
219
  }, []);
198
- return ((0, jsx_runtime_1.jsx)(Form, { ref: (0, util_1.mergeRefs)([formRef, formRefProp]), ...rest, id: id, action: action, method: method, replace: replace, onSubmit: async (e) => {
199
- e.preventDefault();
200
- startSubmit({ formAtom });
201
- const result = await validator.validate(getDataFromForm(e.currentTarget));
202
- if (result.error) {
203
- endSubmit({ formAtom });
204
- setFieldErrors({ fieldErrors: result.error.fieldErrors, formAtom });
205
- if (!disableFocusOnError) {
206
- focusFirstInvalidInput(result.error.fieldErrors, customFocusHandlers(), formRef.current);
207
- }
220
+ const handleSubmit = async (e) => {
221
+ startSubmit({ formAtom });
222
+ const result = await validator.validate(getDataFromForm(e.currentTarget));
223
+ if (result.error) {
224
+ endSubmit({ formAtom });
225
+ setFieldErrors({ fieldErrors: result.error.fieldErrors, formAtom });
226
+ if (!disableFocusOnError) {
227
+ focusFirstInvalidInput(result.error.fieldErrors, customFocusHandlers(), formRef.current);
208
228
  }
209
- else {
210
- onSubmit === null || onSubmit === void 0 ? void 0 : onSubmit(result.data, e);
211
- if (fetcher)
212
- fetcher.submit(clickedButtonRef.current || e.currentTarget);
213
- else
214
- submit(clickedButtonRef.current || e.currentTarget, {
215
- method,
216
- replace,
217
- });
218
- clickedButtonRef.current = null;
229
+ }
230
+ else {
231
+ const eventProxy = formEventProxy(e);
232
+ await (onSubmit === null || onSubmit === void 0 ? void 0 : onSubmit(result.data, eventProxy));
233
+ if (eventProxy.defaultPrevented) {
234
+ endSubmit({ formAtom });
235
+ return;
219
236
  }
237
+ if (fetcher)
238
+ fetcher.submit(clickedButtonRef.current || e.currentTarget);
239
+ else
240
+ submit(clickedButtonRef.current || e.currentTarget, {
241
+ method,
242
+ replace,
243
+ });
244
+ clickedButtonRef.current = null;
245
+ }
246
+ };
247
+ return ((0, jsx_runtime_1.jsx)(Form, { ref: (0, util_1.mergeRefs)([formRef, formRefProp]), ...rest, id: id, action: action, method: method, replace: replace, onSubmit: (e) => {
248
+ e.preventDefault();
249
+ handleSubmit(e);
220
250
  }, onReset: (event) => {
221
251
  onReset === null || onReset === void 0 ? void 0 : onReset(event);
222
252
  if (event.defaultPrevented)
package/build/hooks.js CHANGED
@@ -12,14 +12,20 @@ const state_1 = require("./internal/state");
12
12
  *
13
13
  * @param formId
14
14
  */
15
- const useIsSubmitting = (formId) => (0, hooks_1.useUnknownFormContextSelectAtom)(formId, state_1.isSubmittingAtom, "useIsSubmitting");
15
+ const useIsSubmitting = (formId) => {
16
+ const formContext = (0, hooks_1.useInternalFormContext)(formId, "useIsSubmitting");
17
+ return (0, hooks_1.useContextSelectAtom)(formContext.formId, state_1.isSubmittingAtom);
18
+ };
16
19
  exports.useIsSubmitting = useIsSubmitting;
17
20
  /**
18
21
  * Returns whether or not the current form is valid.
19
22
  *
20
23
  * @param formId the id of the form. Only necessary if being used outside a ValidatedForm.
21
24
  */
22
- const useIsValid = (formId) => (0, hooks_1.useUnknownFormContextSelectAtom)(formId, state_1.isValidAtom, "useIsValid");
25
+ const useIsValid = (formId) => {
26
+ const formContext = (0, hooks_1.useInternalFormContext)(formId, "useIsValid");
27
+ return (0, hooks_1.useContextSelectAtom)(formContext.formId, state_1.isValidAtom);
28
+ };
23
29
  exports.useIsValid = useIsValid;
24
30
  /**
25
31
  * Provides the data and helpers necessary to set up a field.
@@ -34,11 +34,11 @@ class MultiValueMap {
34
34
  exports.MultiValueMap = MultiValueMap;
35
35
  const useMultiValueMap = () => {
36
36
  const ref = (0, react_1.useRef)(null);
37
- return () => {
37
+ return (0, react_1.useCallback)(() => {
38
38
  if (ref.current)
39
39
  return ref.current;
40
40
  ref.current = new MultiValueMap();
41
41
  return ref.current;
42
- };
42
+ }, []);
43
43
  };
44
44
  exports.useMultiValueMap = useMultiValueMap;
@@ -7,7 +7,6 @@ declare type FormSelectorAtomCreator<T> = (formState: FormAtom) => Atom<T>;
7
7
  declare const USE_HYDRATED_STATE: unique symbol;
8
8
  export declare const useInternalFormContext: (formId?: string | symbol | undefined, hookName?: string | undefined) => InternalFormContextValue;
9
9
  export declare const useContextSelectAtom: <T>(formId: string | symbol, selectorAtomCreator: FormSelectorAtomCreator<T>) => T extends Promise<infer V> ? V : T;
10
- export declare const useUnknownFormContextSelectAtom: <T>(formId: string | symbol | undefined, selectorAtomCreator: FormSelectorAtomCreator<T>, hookName: string) => T extends Promise<infer V> ? V : T;
11
10
  export declare const useHydratableSelector: <T, U>({ formId }: InternalFormContextValue, atomCreator: FormSelectorAtomCreator<T>, dataToUse: U | typeof USE_HYDRATED_STATE, selector?: (data: U) => T) => T | (T extends Promise<infer V> ? V : T);
12
11
  export declare function useErrorResponseForForm({ fetcher, subaction, formId, }: InternalFormContextValue): ValidationErrorResponseData | null;
13
12
  export declare const useFieldErrorsForForm: (context: InternalFormContextValue) => import("..").FieldErrors | typeof USE_HYDRATED_STATE | undefined;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.useSetTouched = exports.useClearError = exports.useFormUpdateAtom = exports.useFieldDefaultValue = exports.useFieldError = exports.useFieldTouched = exports.useHasActiveFormSubmit = exports.useDefaultValuesForForm = exports.useDefaultValuesFromLoader = exports.useFieldErrorsForForm = exports.useErrorResponseForForm = exports.useHydratableSelector = exports.useUnknownFormContextSelectAtom = exports.useContextSelectAtom = exports.useInternalFormContext = void 0;
6
+ exports.useSetTouched = exports.useClearError = exports.useFormUpdateAtom = exports.useFieldDefaultValue = exports.useFieldError = exports.useFieldTouched = exports.useHasActiveFormSubmit = exports.useDefaultValuesForForm = exports.useDefaultValuesFromLoader = exports.useFieldErrorsForForm = exports.useErrorResponseForForm = exports.useHydratableSelector = exports.useContextSelectAtom = exports.useInternalFormContext = void 0;
7
7
  const react_1 = require("@remix-run/react");
8
8
  const utils_1 = require("jotai/utils");
9
9
  const get_1 = __importDefault(require("lodash/get"));
@@ -28,11 +28,6 @@ const useContextSelectAtom = (formId, selectorAtomCreator) => {
28
28
  return (0, utils_1.useAtomValue)(selectorAtom, state_1.ATOM_SCOPE);
29
29
  };
30
30
  exports.useContextSelectAtom = useContextSelectAtom;
31
- const useUnknownFormContextSelectAtom = (formId, selectorAtomCreator, hookName) => {
32
- const formContext = (0, exports.useInternalFormContext)(formId, hookName);
33
- return (0, exports.useContextSelectAtom)(formContext.formId, selectorAtomCreator);
34
- };
35
- exports.useUnknownFormContextSelectAtom = useUnknownFormContextSelectAtom;
36
31
  const useHydratableSelector = ({ formId }, atomCreator, dataToUse, selector = identity_1.default) => {
37
32
  const dataFromState = (0, exports.useContextSelectAtom)(formId, atomCreator);
38
33
  return dataToUse === USE_HYDRATED_STATE ? dataFromState : selector(dataToUse);
@@ -0,0 +1 @@
1
+ export declare const getCheckboxChecked: (checkboxValue: string | undefined, newValue: unknown) => boolean | undefined;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getCheckboxChecked = void 0;
4
+ const getCheckboxChecked = (checkboxValue = "on", newValue) => {
5
+ if (Array.isArray(newValue))
6
+ return newValue.includes(checkboxValue);
7
+ if (typeof newValue === "boolean")
8
+ return newValue;
9
+ if (typeof newValue === "string")
10
+ return newValue === checkboxValue;
11
+ return undefined;
12
+ };
13
+ exports.getCheckboxChecked = getCheckboxChecked;
@@ -0,0 +1 @@
1
+ export declare const getRadioChecked: (radioValue: string | undefined, newValue: unknown) => boolean | undefined;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getRadioChecked = void 0;
4
+ const getRadioChecked = (radioValue = "on", newValue) => {
5
+ if (typeof newValue === "string")
6
+ return newValue === radioValue;
7
+ return undefined;
8
+ };
9
+ exports.getRadioChecked = getRadioChecked;
@@ -0,0 +1 @@
1
+ export declare const setFieldValue: (formElement: HTMLFormElement, name: string, value: unknown) => void;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.setFieldValue = void 0;
7
+ const tiny_invariant_1 = __importDefault(require("tiny-invariant"));
8
+ const getCheckboxChecked_1 = require("./getCheckboxChecked");
9
+ const getRadioChecked_1 = require("./getRadioChecked");
10
+ const setElementValue = (element, value, name) => {
11
+ if (element instanceof HTMLSelectElement && element.multiple) {
12
+ (0, tiny_invariant_1.default)(Array.isArray(value), "Must specify an array to set the value for a multi-select");
13
+ for (const option of element.options) {
14
+ option.selected = value.includes(option.value);
15
+ }
16
+ return;
17
+ }
18
+ if (element instanceof HTMLInputElement && element.type === "checkbox") {
19
+ const newChecked = (0, getCheckboxChecked_1.getCheckboxChecked)(element.value, value);
20
+ (0, tiny_invariant_1.default)(newChecked, `Unable to determine if checkbox should be checked. Provided value was ${value} for checkbox ${name}.`);
21
+ element.checked = newChecked;
22
+ return;
23
+ }
24
+ if (element instanceof HTMLInputElement && element.type === "radio") {
25
+ const newChecked = (0, getRadioChecked_1.getRadioChecked)(element.value, value);
26
+ (0, tiny_invariant_1.default)(newChecked, `Unable to determine if radio should be checked. Provided value was ${value} for radio ${name}.`);
27
+ element.checked = newChecked;
28
+ return;
29
+ }
30
+ (0, tiny_invariant_1.default)(typeof value === "string", `Must specify a string to set the value of ${element.constructor.name}`);
31
+ const input = element;
32
+ input.value = value;
33
+ };
34
+ const setFieldValue = (formElement, name, value) => {
35
+ const controlElement = formElement.elements.namedItem(name);
36
+ if (!controlElement)
37
+ return;
38
+ if (controlElement instanceof RadioNodeList) {
39
+ for (const element of controlElement) {
40
+ setElementValue(element, value, name);
41
+ }
42
+ }
43
+ else {
44
+ setElementValue(controlElement, value, name);
45
+ }
46
+ };
47
+ exports.setFieldValue = setFieldValue;
@@ -2,3 +2,4 @@ import type React from "react";
2
2
  export declare const omit: (obj: any, ...keys: string[]) => any;
3
3
  export declare const mergeRefs: <T = any>(refs: (React.MutableRefObject<T> | React.LegacyRef<T> | undefined)[]) => (instance: T | null) => void;
4
4
  export declare const useIsomorphicLayoutEffect: typeof React.useEffect;
5
+ export declare const useDeepEqualsMemo: <T>(item: T) => T;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useIsomorphicLayoutEffect = exports.mergeRefs = exports.omit = void 0;
3
+ exports.useDeepEqualsMemo = exports.useIsomorphicLayoutEffect = exports.mergeRefs = exports.omit = void 0;
4
+ const lodash_1 = require("lodash");
4
5
  const react_1 = require("react");
5
6
  const omit = (obj, ...keys) => {
6
7
  const result = { ...obj };
@@ -24,3 +25,14 @@ const mergeRefs = (refs) => {
24
25
  };
25
26
  exports.mergeRefs = mergeRefs;
26
27
  exports.useIsomorphicLayoutEffect = typeof window !== "undefined" ? react_1.useLayoutEffect : react_1.useEffect;
28
+ const useDeepEqualsMemo = (item) => {
29
+ const ref = (0, react_1.useRef)(item);
30
+ const areEqual = ref.current === item || (0, lodash_1.isEqual)(ref.current, item);
31
+ (0, react_1.useEffect)(() => {
32
+ if (!areEqual) {
33
+ ref.current = item;
34
+ }
35
+ });
36
+ return areEqual ? ref.current : item;
37
+ };
38
+ exports.useDeepEqualsMemo = useDeepEqualsMemo;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remix-validated-form",
3
- "version": "4.1.0-beta.1",
3
+ "version": "4.1.1",
4
4
  "description": "Form component and utils for easy form validation in remix",
5
5
  "browser": "./browser/index.js",
6
6
  "main": "./build/index.js",
@@ -2,6 +2,7 @@ import { Form as RemixForm, useFetcher, useSubmit } from "@remix-run/react";
2
2
  import uniq from "lodash/uniq";
3
3
  import React, {
4
4
  ComponentProps,
5
+ FormEvent,
5
6
  RefObject,
6
7
  useCallback,
7
8
  useEffect,
@@ -37,6 +38,7 @@ import {
37
38
  import { useSubmitComplete } from "./internal/submissionCallbacks";
38
39
  import {
39
40
  mergeRefs,
41
+ useDeepEqualsMemo,
40
42
  useIsomorphicLayoutEffect as useLayoutEffect,
41
43
  } from "./internal/util";
42
44
  import { FieldErrors, Validator } from "./validation/types";
@@ -53,7 +55,7 @@ export type FormProps<DataType> = {
53
55
  onSubmit?: (
54
56
  data: DataType,
55
57
  event: React.FormEvent<HTMLFormElement>
56
- ) => Promise<void>;
58
+ ) => void | Promise<void>;
57
59
  /**
58
60
  * Allows you to provide a `fetcher` from remix's `useFetcher` hook.
59
61
  * The form will use the fetcher for loading states, action data, etc
@@ -171,6 +173,31 @@ const FormResetter = ({
171
173
  return null;
172
174
  };
173
175
 
176
+ function formEventProxy<T extends object>(event: T): T {
177
+ let defaultPrevented = false;
178
+ return new Proxy(event, {
179
+ get: (target, prop) => {
180
+ if (prop === "preventDefault") {
181
+ return () => {
182
+ defaultPrevented = true;
183
+ };
184
+ }
185
+
186
+ if (prop === "defaultPrevented") {
187
+ return defaultPrevented;
188
+ }
189
+
190
+ return target[prop as keyof T];
191
+ },
192
+ }) as T;
193
+ }
194
+
195
+ const useFormAtom = (formId: string | symbol) => {
196
+ const formAtom = formRegistry(formId);
197
+ useEffect(() => () => formRegistry.remove(formId), [formId]);
198
+ return formAtom;
199
+ };
200
+
174
201
  /**
175
202
  * The primary form component of `remix-validated-form`.
176
203
  */
@@ -180,7 +207,7 @@ export function ValidatedForm<DataType>({
180
207
  children,
181
208
  fetcher,
182
209
  action,
183
- defaultValues: providedDefaultValues,
210
+ defaultValues: unMemoizedDefaults,
184
211
  formRef: formRefProp,
185
212
  onReset,
186
213
  subaction,
@@ -192,7 +219,8 @@ export function ValidatedForm<DataType>({
192
219
  ...rest
193
220
  }: FormProps<DataType>) {
194
221
  const formId = useFormId(id);
195
- const formAtom = formRegistry(formId);
222
+ const formAtom = useFormAtom(formId);
223
+ const providedDefaultValues = useDeepEqualsMemo(unMemoizedDefaults);
196
224
  const contextValue = useMemo<InternalFormContextValue>(
197
225
  () => ({
198
226
  formId,
@@ -305,6 +333,37 @@ export function ValidatedForm<DataType>({
305
333
  };
306
334
  }, []);
307
335
 
336
+ const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
337
+ startSubmit({ formAtom });
338
+ const result = await validator.validate(getDataFromForm(e.currentTarget));
339
+ if (result.error) {
340
+ endSubmit({ formAtom });
341
+ setFieldErrors({ fieldErrors: result.error.fieldErrors, formAtom });
342
+ if (!disableFocusOnError) {
343
+ focusFirstInvalidInput(
344
+ result.error.fieldErrors,
345
+ customFocusHandlers(),
346
+ formRef.current!
347
+ );
348
+ }
349
+ } else {
350
+ const eventProxy = formEventProxy(e);
351
+ await onSubmit?.(result.data, eventProxy);
352
+ if (eventProxy.defaultPrevented) {
353
+ endSubmit({ formAtom });
354
+ return;
355
+ }
356
+
357
+ if (fetcher) fetcher.submit(clickedButtonRef.current || e.currentTarget);
358
+ else
359
+ submit(clickedButtonRef.current || e.currentTarget, {
360
+ method,
361
+ replace,
362
+ });
363
+ clickedButtonRef.current = null;
364
+ }
365
+ };
366
+
308
367
  return (
309
368
  <Form
310
369
  ref={mergeRefs([formRef, formRefProp])}
@@ -313,33 +372,9 @@ export function ValidatedForm<DataType>({
313
372
  action={action}
314
373
  method={method}
315
374
  replace={replace}
316
- onSubmit={async (e) => {
375
+ onSubmit={(e) => {
317
376
  e.preventDefault();
318
- startSubmit({ formAtom });
319
- const result = await validator.validate(
320
- getDataFromForm(e.currentTarget)
321
- );
322
- if (result.error) {
323
- endSubmit({ formAtom });
324
- setFieldErrors({ fieldErrors: result.error.fieldErrors, formAtom });
325
- if (!disableFocusOnError) {
326
- focusFirstInvalidInput(
327
- result.error.fieldErrors,
328
- customFocusHandlers(),
329
- formRef.current!
330
- );
331
- }
332
- } else {
333
- onSubmit?.(result.data, e);
334
- if (fetcher)
335
- fetcher.submit(clickedButtonRef.current || e.currentTarget);
336
- else
337
- submit(clickedButtonRef.current || e.currentTarget, {
338
- method,
339
- replace,
340
- });
341
- clickedButtonRef.current = null;
342
- }
377
+ handleSubmit(e);
343
378
  }}
344
379
  onReset={(event) => {
345
380
  onReset?.(event);
package/src/hooks.ts CHANGED
@@ -5,7 +5,6 @@ import {
5
5
  ValidationBehaviorOptions,
6
6
  } from "./internal/getInputProps";
7
7
  import {
8
- useUnknownFormContextSelectAtom,
9
8
  useInternalFormContext,
10
9
  useFieldTouched,
11
10
  useFieldError,
@@ -29,16 +28,20 @@ import {
29
28
  *
30
29
  * @param formId
31
30
  */
32
- export const useIsSubmitting = (formId?: string) =>
33
- useUnknownFormContextSelectAtom(formId, isSubmittingAtom, "useIsSubmitting");
31
+ export const useIsSubmitting = (formId?: string) => {
32
+ const formContext = useInternalFormContext(formId, "useIsSubmitting");
33
+ return useContextSelectAtom(formContext.formId, isSubmittingAtom);
34
+ };
34
35
 
35
36
  /**
36
37
  * Returns whether or not the current form is valid.
37
38
  *
38
39
  * @param formId the id of the form. Only necessary if being used outside a ValidatedForm.
39
40
  */
40
- export const useIsValid = (formId?: string) =>
41
- useUnknownFormContextSelectAtom(formId, isValidAtom, "useIsValid");
41
+ export const useIsValid = (formId?: string) => {
42
+ const formContext = useInternalFormContext(formId, "useIsValid");
43
+ return useContextSelectAtom(formContext.formId, isValidAtom);
44
+ };
42
45
 
43
46
  export type FieldProps = {
44
47
  /**
@@ -1,4 +1,4 @@
1
- import { useRef } from "react";
1
+ import { useCallback, useRef } from "react";
2
2
 
3
3
  export class MultiValueMap<Key, Value> {
4
4
  private dict: Map<Key, Value[]> = new Map();
@@ -30,9 +30,9 @@ export class MultiValueMap<Key, Value> {
30
30
 
31
31
  export const useMultiValueMap = <Key, Value>() => {
32
32
  const ref = useRef<MultiValueMap<Key, Value> | null>(null);
33
- return () => {
33
+ return useCallback(() => {
34
34
  if (ref.current) return ref.current;
35
35
  ref.current = new MultiValueMap();
36
36
  return ref.current;
37
- };
37
+ }, []);
38
38
  };
@@ -48,15 +48,6 @@ export const useContextSelectAtom = <T>(
48
48
  return useAtomValue(selectorAtom, ATOM_SCOPE);
49
49
  };
50
50
 
51
- export const useUnknownFormContextSelectAtom = <T>(
52
- formId: string | symbol | undefined,
53
- selectorAtomCreator: FormSelectorAtomCreator<T>,
54
- hookName: string
55
- ) => {
56
- const formContext = useInternalFormContext(formId, hookName);
57
- return useContextSelectAtom(formContext.formId, selectorAtomCreator);
58
- };
59
-
60
51
  export const useHydratableSelector = <T, U>(
61
52
  { formId }: InternalFormContextValue,
62
53
  atomCreator: FormSelectorAtomCreator<T>,
@@ -1,5 +1,6 @@
1
+ import { isEqual } from "lodash";
1
2
  import type React from "react";
2
- import { useEffect, useLayoutEffect } from "react";
3
+ import { useEffect, useLayoutEffect, useRef } from "react";
3
4
 
4
5
  export const omit = (obj: any, ...keys: string[]) => {
5
6
  const result = { ...obj };
@@ -25,3 +26,14 @@ export const mergeRefs = <T = any>(
25
26
 
26
27
  export const useIsomorphicLayoutEffect =
27
28
  typeof window !== "undefined" ? useLayoutEffect : useEffect;
29
+
30
+ export const useDeepEqualsMemo = <T>(item: T): T => {
31
+ const ref = useRef<T>(item);
32
+ const areEqual = ref.current === item || isEqual(ref.current, item);
33
+ useEffect(() => {
34
+ if (!areEqual) {
35
+ ref.current = item;
36
+ }
37
+ });
38
+ return areEqual ? ref.current : item;
39
+ };