remix-validated-form 4.1.8 → 4.3.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 (39) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/README.md +7 -5
  3. package/browser/ValidatedForm.js +6 -2
  4. package/browser/hooks.d.ts +2 -0
  5. package/browser/hooks.js +10 -0
  6. package/browser/internal/MultiValueMap.d.ts +2 -0
  7. package/browser/internal/MultiValueMap.js +4 -0
  8. package/browser/internal/getInputProps.js +2 -1
  9. package/browser/internal/reset.d.ts +28 -0
  10. package/browser/internal/reset.js +13 -0
  11. package/browser/internal/state/controlledFields.d.ts +62 -9
  12. package/browser/internal/state/controlledFields.js +36 -36
  13. package/browser/internal/state.d.ts +0 -27
  14. package/browser/internal/state.js +0 -5
  15. package/browser/server.d.ts +1 -1
  16. package/browser/server.js +2 -2
  17. package/build/ValidatedForm.js +6 -2
  18. package/build/hooks.d.ts +2 -0
  19. package/build/hooks.js +13 -1
  20. package/build/internal/MultiValueMap.d.ts +2 -0
  21. package/build/internal/MultiValueMap.js +4 -0
  22. package/build/internal/getInputProps.js +2 -1
  23. package/build/internal/reset.d.ts +28 -0
  24. package/build/internal/reset.js +19 -0
  25. package/build/internal/state/controlledFields.d.ts +62 -9
  26. package/build/internal/state/controlledFields.js +39 -40
  27. package/build/internal/state.d.ts +0 -27
  28. package/build/internal/state.js +1 -6
  29. package/build/server.d.ts +1 -1
  30. package/build/server.js +2 -2
  31. package/package.json +2 -2
  32. package/src/ValidatedForm.tsx +6 -2
  33. package/src/hooks.ts +15 -0
  34. package/src/internal/MultiValueMap.ts +6 -0
  35. package/src/internal/getInputProps.ts +2 -1
  36. package/src/internal/reset.ts +26 -0
  37. package/src/internal/state/controlledFields.ts +170 -0
  38. package/src/internal/state.ts +0 -8
  39. package/src/server.ts +3 -2
@@ -1,9 +1,9 @@
1
1
  $ npm run build:browser && npm run build:main
2
2
 
3
- > remix-validated-form@4.1.7 build:browser
3
+ > remix-validated-form@4.2.0 build:browser
4
4
  > tsc --module ESNext --outDir ./browser
5
5
 
6
6
 
7
- > remix-validated-form@4.1.7 build:main
7
+ > remix-validated-form@4.2.0 build:main
8
8
  > tsc --module CommonJS --outDir ./build
9
9
 
package/README.md CHANGED
@@ -71,9 +71,7 @@ export const MyInput = ({ name, label }: MyInputProps) => {
71
71
  <div>
72
72
  <label htmlFor={name}>{label}</label>
73
73
  <input {...getInputProps({ id: name })} />
74
- {error && (
75
- <span className="my-error-class">{error}</span>
76
- )}
74
+ {error && <span className="my-error-class">{error}</span>}
77
75
  </div>
78
76
  );
79
77
  };
@@ -92,14 +90,17 @@ export const MySubmitButton = () => {
92
90
  const disabled = isSubmitting || !isValid;
93
91
 
94
92
  return (
95
- <button type="submit" disabled={disabled} className={disabled ? "disabled-btn" : "btn"}>
93
+ <button
94
+ type="submit"
95
+ disabled={disabled}
96
+ className={disabled ? "disabled-btn" : "btn"}
97
+ >
96
98
  {isSubmitting ? "Submitting..." : "Submit"}
97
99
  </button>
98
100
  );
99
101
  };
100
102
  ```
101
103
 
102
-
103
104
  ## Use the form!
104
105
 
105
106
  Now that we have our components, making a form is easy!
@@ -258,5 +259,6 @@ Problem: how do we trigger a toast message on success if the action redirects aw
258
259
  See the [Remix](https://remix.run/docs/en/v1/api/remix#sessionflashkey-value) documentation for more information.
259
260
 
260
261
  ## Why is my cancel button triggering form submission?
262
+
261
263
  Problem: the cancel button has an onClick handler to navigate away from the form route but instead it is submitting the form.
262
264
  A button defaults to `type="submit"` in a form which will submit the form by default. If you want to prevent this you can add `type="reset"` or `type="button"` to the cancel button.
@@ -8,7 +8,9 @@ import { FORM_ID_FIELD } from "./internal/constants";
8
8
  import { InternalFormContext, } from "./internal/formContext";
9
9
  import { useDefaultValuesFromLoader, useErrorResponseForForm, useFormUpdateAtom, useHasActiveFormSubmit, } from "./internal/hooks";
10
10
  import { useMultiValueMap } from "./internal/MultiValueMap";
11
- import { cleanupFormState, endSubmitAtom, fieldErrorsAtom, formElementAtom, formPropsAtom, isHydratedAtom, resetAtom, setFieldErrorAtom, startSubmitAtom, } from "./internal/state";
11
+ import { resetAtom } from "./internal/reset";
12
+ import { cleanupFormState, endSubmitAtom, fieldErrorsAtom, formElementAtom, formPropsAtom, isHydratedAtom, setFieldErrorAtom, startSubmitAtom, } from "./internal/state";
13
+ import { useAwaitValue } from "./internal/state/controlledFields";
12
14
  import { useSubmitComplete } from "./internal/submissionCallbacks";
13
15
  import { mergeRefs, useDeepEqualsMemo, useIsomorphicLayoutEffect as useLayoutEffect, } from "./internal/util";
14
16
  const getDataFromForm = (el) => new FormData(el);
@@ -123,8 +125,10 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
123
125
  setHydrated(true);
124
126
  return () => cleanupFormState(formId);
125
127
  }, [formId, setHydrated]);
128
+ const awaitValue = useAwaitValue(formId);
126
129
  const validateField = useCallback(async (field) => {
127
130
  invariant(formRef.current, "Cannot find reference to form");
131
+ await awaitValue(field);
128
132
  const { error } = await validator.validateField(getDataFromForm(formRef.current), field);
129
133
  if (error) {
130
134
  setFieldError({ field, error });
@@ -134,7 +138,7 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
134
138
  setFieldError({ field, error: undefined });
135
139
  return null;
136
140
  }
137
- }, [setFieldError, validator]);
141
+ }, [awaitValue, setFieldError, validator]);
138
142
  const customFocusHandlers = useMultiValueMap();
139
143
  const registerReceiveFocus = useCallback((fieldName, handler) => {
140
144
  customFocusHandlers().add(fieldName, handler);
@@ -63,3 +63,5 @@ export declare const useField: (name: string, options?: {
63
63
  */
64
64
  formId?: string | undefined;
65
65
  } | undefined) => FieldProps;
66
+ export declare const useControlField: <T>(name: string, formId?: string | undefined) => readonly [T, (value: T) => void];
67
+ export declare const useUpdateControlledField: (formId?: string | undefined) => (field: string, value: unknown) => void;
package/browser/hooks.js CHANGED
@@ -2,6 +2,7 @@ import { useEffect, useMemo } from "react";
2
2
  import { createGetInputProps, } from "./internal/getInputProps";
3
3
  import { useInternalFormContext, useFieldTouched, useFieldError, useFormAtomValue, useFieldDefaultValue, } from "./internal/hooks";
4
4
  import { formPropsAtom, hasBeenSubmittedAtom, isSubmittingAtom, isValidAtom, } from "./internal/state";
5
+ import { useControllableValue, useUpdateControllableValue, } from "./internal/state/controlledFields";
5
6
  /**
6
7
  * Returns whether or not the parent form is currently being submitted.
7
8
  * This is different from remix's `useTransition().submission` in that it
@@ -71,3 +72,12 @@ export const useField = (name, options) => {
71
72
  ]);
72
73
  return field;
73
74
  };
75
+ export const useControlField = (name, formId) => {
76
+ const context = useInternalFormContext(formId, "useControlField");
77
+ const [value, setValue] = useControllableValue(context, name);
78
+ return [value, setValue];
79
+ };
80
+ export const useUpdateControlledField = (formId) => {
81
+ const context = useInternalFormContext(formId, "useControlField");
82
+ return useUpdateControllableValue(context.formId);
83
+ };
@@ -1,9 +1,11 @@
1
1
  export declare class MultiValueMap<Key, Value> {
2
2
  private dict;
3
3
  add: (key: Key, value: Value) => void;
4
+ delete: (key: Key) => void;
4
5
  remove: (key: Key, value: Value) => void;
5
6
  getAll: (key: Key) => Value[];
6
7
  entries: () => IterableIterator<[Key, Value[]]>;
8
+ values: () => IterableIterator<Value[]>;
7
9
  has: (key: Key) => boolean;
8
10
  }
9
11
  export declare const useMultiValueMap: <Key, Value>() => () => MultiValueMap<Key, Value>;
@@ -10,6 +10,9 @@ export class MultiValueMap {
10
10
  this.dict.set(key, [value]);
11
11
  }
12
12
  };
13
+ this.delete = (key) => {
14
+ this.dict.delete(key);
15
+ };
13
16
  this.remove = (key, value) => {
14
17
  if (!this.dict.has(key))
15
18
  return;
@@ -25,6 +28,7 @@ export class MultiValueMap {
25
28
  return (_a = this.dict.get(key)) !== null && _a !== void 0 ? _a : [];
26
29
  };
27
30
  this.entries = () => this.dict.entries();
31
+ this.values = () => this.dict.values();
28
32
  this.has = (key) => this.dict.has(key);
29
33
  }
30
34
  }
@@ -42,7 +42,8 @@ export const createGetInputProps = ({ clearError, validate, defaultValue, touche
42
42
  else if (props.type === "radio") {
43
43
  inputProps.defaultChecked = getRadioChecked(props.value, defaultValue);
44
44
  }
45
- else {
45
+ else if (props.value === undefined) {
46
+ // We should only set the defaultValue if the input is uncontrolled.
46
47
  inputProps.defaultValue = defaultValue;
47
48
  }
48
49
  return omitBy(inputProps, (value) => value === undefined);
@@ -0,0 +1,28 @@
1
+ import { InternalFormId } from "./state/atomUtils";
2
+ export declare const resetAtom: {
3
+ (param: InternalFormId): import("jotai").Atom<null> & {
4
+ write: (get: {
5
+ <Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
6
+ <Value_1>(atom: import("jotai").Atom<Promise<Value_1>>): Value_1;
7
+ <Value_2>(atom: import("jotai").Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
8
+ } & {
9
+ <Value_3>(atom: import("jotai").Atom<Value_3 | Promise<Value_3>>, options: {
10
+ unstable_promise: true;
11
+ }): Value_3 | Promise<Value_3>;
12
+ <Value_4>(atom: import("jotai").Atom<Promise<Value_4>>, options: {
13
+ unstable_promise: true;
14
+ }): Value_4 | Promise<Value_4>;
15
+ <Value_5>(atom: import("jotai").Atom<Value_5>, options: {
16
+ unstable_promise: true;
17
+ }): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
18
+ }, set: {
19
+ <Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
20
+ <Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
21
+ }, update: unknown) => void;
22
+ onMount?: (<S extends (update?: unknown) => void>(setAtom: S) => void | (() => void)) | undefined;
23
+ } & {
24
+ init: null;
25
+ };
26
+ remove(param: InternalFormId): void;
27
+ setShouldRemove(shouldRemove: ((createdAt: number, param: InternalFormId) => boolean) | null): void;
28
+ };
@@ -0,0 +1,13 @@
1
+ import { atom } from "jotai";
2
+ import { atomFamily } from "jotai/utils";
3
+ import lodashGet from "lodash/get";
4
+ import { fieldErrorsAtom, formPropsAtom, hasBeenSubmittedAtom, touchedFieldsAtom, } from "./state";
5
+ import { controlledFieldsAtom } from "./state/controlledFields";
6
+ export const resetAtom = atomFamily((formId) => atom(null, (get, set) => {
7
+ set(fieldErrorsAtom(formId), {});
8
+ set(touchedFieldsAtom(formId), {});
9
+ set(hasBeenSubmittedAtom(formId), false);
10
+ const { defaultValues } = get(formPropsAtom(formId));
11
+ const controlledFields = get(controlledFieldsAtom(formId));
12
+ Object.entries(controlledFields).forEach(([name, atom]) => set(atom, lodashGet(defaultValues, name)));
13
+ }));
@@ -1,6 +1,6 @@
1
1
  import { PrimitiveAtom } from "jotai";
2
- import { InternalFormId } from "./atomUtils";
3
- export declare type ValueSerializer = (val: unknown) => string;
2
+ import { InternalFormContextValue } from "../formContext";
3
+ import { FieldAtomKey, InternalFormId } from "./atomUtils";
4
4
  export declare const controlledFieldsAtom: {
5
5
  (param: InternalFormId): import("jotai").Atom<Record<string, PrimitiveAtom<unknown>>> & {
6
6
  write: (get: {
@@ -28,6 +28,60 @@ export declare const controlledFieldsAtom: {
28
28
  remove(param: InternalFormId): void;
29
29
  setShouldRemove(shouldRemove: ((createdAt: number, param: InternalFormId) => boolean) | null): void;
30
30
  };
31
+ export declare const valueUpdatePromiseAtom: {
32
+ (param: FieldAtomKey): import("jotai").Atom<Promise<void> | undefined> & {
33
+ write: (get: {
34
+ <Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
35
+ <Value_1>(atom: import("jotai").Atom<Promise<Value_1>>): Value_1;
36
+ <Value_2>(atom: import("jotai").Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
37
+ } & {
38
+ <Value_3>(atom: import("jotai").Atom<Value_3 | Promise<Value_3>>, options: {
39
+ unstable_promise: true;
40
+ }): Value_3 | Promise<Value_3>;
41
+ <Value_4>(atom: import("jotai").Atom<Promise<Value_4>>, options: {
42
+ unstable_promise: true;
43
+ }): Value_4 | Promise<Value_4>;
44
+ <Value_5>(atom: import("jotai").Atom<Value_5>, options: {
45
+ unstable_promise: true;
46
+ }): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
47
+ }, set: {
48
+ <Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
49
+ <Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
50
+ }, update: Promise<void> | ((prev: Promise<void> | undefined) => Promise<void> | undefined) | undefined) => void;
51
+ onMount?: (<S extends (update?: Promise<void> | ((prev: Promise<void> | undefined) => Promise<void> | undefined) | undefined) => void>(setAtom: S) => void | (() => void)) | undefined;
52
+ } & {
53
+ init: Promise<void> | undefined;
54
+ };
55
+ remove(param: FieldAtomKey): void;
56
+ setShouldRemove(shouldRemove: ((createdAt: number, param: FieldAtomKey) => boolean) | null): void;
57
+ };
58
+ export declare const resolveValueUpdateAtom: {
59
+ (param: FieldAtomKey): import("jotai").Atom<(() => void) | undefined> & {
60
+ write: (get: {
61
+ <Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
62
+ <Value_1>(atom: import("jotai").Atom<Promise<Value_1>>): Value_1;
63
+ <Value_2>(atom: import("jotai").Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
64
+ } & {
65
+ <Value_3>(atom: import("jotai").Atom<Value_3 | Promise<Value_3>>, options: {
66
+ unstable_promise: true;
67
+ }): Value_3 | Promise<Value_3>;
68
+ <Value_4>(atom: import("jotai").Atom<Promise<Value_4>>, options: {
69
+ unstable_promise: true;
70
+ }): Value_4 | Promise<Value_4>;
71
+ <Value_5>(atom: import("jotai").Atom<Value_5>, options: {
72
+ unstable_promise: true;
73
+ }): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
74
+ }, set: {
75
+ <Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
76
+ <Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
77
+ }, update: (() => void) | ((prev: (() => void) | undefined) => (() => void) | undefined) | undefined) => void;
78
+ onMount?: (<S extends (update?: (() => void) | ((prev: (() => void) | undefined) => (() => void) | undefined) | undefined) => void>(setAtom: S) => void | (() => void)) | undefined;
79
+ } & {
80
+ init: (() => void) | undefined;
81
+ };
82
+ remove(param: FieldAtomKey): void;
83
+ setShouldRemove(shouldRemove: ((createdAt: number, param: FieldAtomKey) => boolean) | null): void;
84
+ };
31
85
  export declare const setControlledFieldValueAtom: import("jotai").Atom<null> & {
32
86
  write: (get: {
33
87
  <Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
@@ -50,17 +104,16 @@ export declare const setControlledFieldValueAtom: import("jotai").Atom<null> & {
50
104
  formId: InternalFormId;
51
105
  field: string;
52
106
  value: unknown;
53
- }) => Promise<void>;
107
+ }) => void;
54
108
  onMount?: (<S extends (update: {
55
109
  formId: InternalFormId;
56
110
  field: string;
57
111
  value: unknown;
58
- }) => Promise<void>>(setAtom: S) => void | (() => void)) | undefined;
112
+ }) => void>(setAtom: S) => void | (() => void)) | undefined;
59
113
  } & {
60
114
  init: null;
61
115
  };
62
- export declare const useAllControlledFields: (formId: InternalFormId) => Record<string, PrimitiveAtom<unknown>>;
63
- export declare const useControlledFieldValue: (formId: InternalFormId, field: string) => any;
64
- export declare const useControllableValue: (formId: InternalFormId, field: string) => readonly [any, (value: unknown) => Promise<void>];
65
- export declare const useFieldSerializer: (formId: InternalFormId, field: string) => ValueSerializer;
66
- export declare const useRegisterFieldSerializer: (formId: InternalFormId, field: string, serializer?: ValueSerializer | undefined) => void;
116
+ export declare const useControlledFieldValue: (context: InternalFormContextValue, field: string) => any;
117
+ export declare const useControllableValue: (context: InternalFormContextValue, field: string) => readonly [any, (value: unknown) => void];
118
+ export declare const useUpdateControllableValue: (formId: InternalFormId) => (field: string, value: unknown) => void;
119
+ export declare const useAwaitValue: (formId: InternalFormId) => (arg: string) => Promise<void>;
@@ -1,4 +1,5 @@
1
1
  import { atom } from "jotai";
2
+ import { useAtomCallback } from "jotai/utils";
2
3
  import omit from "lodash/omit";
3
4
  import { useCallback, useEffect } from "react";
4
5
  import { useFieldDefaultValue, useFormAtomValue, useFormAtom, useFormUpdateAtom, } from "../hooks";
@@ -8,14 +9,8 @@ export const controlledFieldsAtom = formAtomFamily({});
8
9
  const refCountAtom = fieldAtomFamily(() => atom(0));
9
10
  const fieldValueAtom = fieldAtomFamily(() => atom(undefined));
10
11
  const fieldValueHydratedAtom = fieldAtomFamily(() => atom(false));
11
- const pendingValidateAtom = fieldAtomFamily(() => atom(undefined));
12
- const valueSerializerAtom = fieldAtomFamily(() => atom({
13
- serializer: (value) => {
14
- if (value === undefined)
15
- return "";
16
- return JSON.stringify(value);
17
- },
18
- }));
12
+ export const valueUpdatePromiseAtom = fieldAtomFamily(() => atom(undefined));
13
+ export const resolveValueUpdateAtom = fieldAtomFamily(() => atom(undefined));
19
14
  const registerAtom = atom(null, (get, set, { formId, field }) => {
20
15
  set(refCountAtom({ formId, field }), (prev) => prev + 1);
21
16
  const newRefCount = get(refCountAtom({ formId, field }));
@@ -34,23 +29,27 @@ const unregisterAtom = atom(null, (get, set, { formId, field }) => {
34
29
  if (newRefCount === 0) {
35
30
  set(controlledFieldsAtom(formId), (prev) => omit(prev, field));
36
31
  fieldValueAtom.remove({ formId, field });
37
- pendingValidateAtom.remove({ formId, field });
32
+ resolveValueUpdateAtom.remove({ formId, field });
38
33
  fieldValueHydratedAtom.remove({ formId, field });
39
34
  }
40
35
  });
41
- export const setControlledFieldValueAtom = atom(null, async (_get, set, { formId, field, value, }) => {
36
+ export const setControlledFieldValueAtom = atom(null, (_get, set, { formId, field, value, }) => {
42
37
  set(fieldValueAtom({ formId, field }), value);
43
- const pending = pendingValidateAtom({ formId, field });
44
- await new Promise((resolve) => set(pending, resolve));
45
- set(pending, undefined);
38
+ const resolveAtom = resolveValueUpdateAtom({ formId, field });
39
+ const promiseAtom = valueUpdatePromiseAtom({ formId, field });
40
+ const promise = new Promise((resolve) => set(resolveAtom, () => {
41
+ resolve();
42
+ set(resolveAtom, undefined);
43
+ set(promiseAtom, undefined);
44
+ }));
45
+ set(promiseAtom, promise);
46
46
  });
47
- export const useAllControlledFields = (formId) => useFormAtomValue(controlledFieldsAtom(formId));
48
- export const useControlledFieldValue = (formId, field) => {
49
- const fieldAtom = fieldValueAtom({ formId, field });
47
+ export const useControlledFieldValue = (context, field) => {
48
+ const fieldAtom = fieldValueAtom({ formId: context.formId, field });
50
49
  const [value, setValue] = useFormAtom(fieldAtom);
51
- const defaultValue = useFieldDefaultValue(field, { formId });
52
- const isHydrated = useFormAtomValue(isHydratedAtom(formId));
53
- const [isFieldHydrated, setIsFieldHydrated] = useFormAtom(fieldValueHydratedAtom({ formId, field }));
50
+ const defaultValue = useFieldDefaultValue(field, context);
51
+ const isHydrated = useFormAtomValue(isHydratedAtom(context.formId));
52
+ const [isFieldHydrated, setIsFieldHydrated] = useFormAtom(fieldValueHydratedAtom({ formId: context.formId, field }));
54
53
  useEffect(() => {
55
54
  if (isHydrated && !isFieldHydrated) {
56
55
  setValue(defaultValue);
@@ -59,7 +58,7 @@ export const useControlledFieldValue = (formId, field) => {
59
58
  }, [
60
59
  defaultValue,
61
60
  field,
62
- formId,
61
+ context.formId,
63
62
  isFieldHydrated,
64
63
  isHydrated,
65
64
  setIsFieldHydrated,
@@ -67,27 +66,28 @@ export const useControlledFieldValue = (formId, field) => {
67
66
  ]);
68
67
  return isFieldHydrated ? value : defaultValue;
69
68
  };
70
- export const useControllableValue = (formId, field) => {
71
- const pending = useFormAtomValue(pendingValidateAtom({ formId, field }));
69
+ export const useControllableValue = (context, field) => {
70
+ const resolveUpdate = useFormAtomValue(resolveValueUpdateAtom({ formId: context.formId, field }));
72
71
  useEffect(() => {
73
- pending === null || pending === void 0 ? void 0 : pending();
74
- }, [pending]);
72
+ resolveUpdate === null || resolveUpdate === void 0 ? void 0 : resolveUpdate();
73
+ }, [resolveUpdate]);
75
74
  const register = useFormUpdateAtom(registerAtom);
76
75
  const unregister = useFormUpdateAtom(unregisterAtom);
77
76
  useEffect(() => {
78
- register({ formId, field });
79
- return () => unregister({ formId, field });
80
- }, [field, formId, register, unregister]);
77
+ register({ formId: context.formId, field });
78
+ return () => unregister({ formId: context.formId, field });
79
+ }, [context.formId, field, register, unregister]);
81
80
  const setControlledFieldValue = useFormUpdateAtom(setControlledFieldValueAtom);
82
- const setValue = useCallback((value) => setControlledFieldValue({ formId, field, value }), [field, formId, setControlledFieldValue]);
83
- const value = useControlledFieldValue(formId, field);
81
+ const setValue = useCallback((value) => setControlledFieldValue({ formId: context.formId, field, value }), [field, context.formId, setControlledFieldValue]);
82
+ const value = useControlledFieldValue(context, field);
84
83
  return [value, setValue];
85
84
  };
86
- export const useFieldSerializer = (formId, field) => useFormAtomValue(valueSerializerAtom({ formId, field })).serializer;
87
- export const useRegisterFieldSerializer = (formId, field, serializer) => {
88
- const setSerializer = useFormUpdateAtom(valueSerializerAtom({ formId, field }));
89
- useEffect(() => {
90
- if (serializer)
91
- setSerializer({ serializer });
92
- }, [serializer, setSerializer]);
85
+ export const useUpdateControllableValue = (formId) => {
86
+ const setControlledFieldValue = useFormUpdateAtom(setControlledFieldValueAtom);
87
+ return useCallback((field, value) => setControlledFieldValue({ formId, field, value }), [formId, setControlledFieldValue]);
88
+ };
89
+ export const useAwaitValue = (formId) => {
90
+ return useAtomCallback(useCallback(async (get, _set, field) => {
91
+ await get(valueUpdatePromiseAtom({ formId, field }));
92
+ }, [formId]));
93
93
  };
@@ -206,33 +206,6 @@ export declare const isValidAtom: {
206
206
  remove(param: InternalFormId): void;
207
207
  setShouldRemove(shouldRemove: ((createdAt: number, param: InternalFormId) => boolean) | null): void;
208
208
  };
209
- export declare const resetAtom: {
210
- (param: InternalFormId): import("jotai").Atom<null> & {
211
- write: (get: {
212
- <Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
213
- <Value_1>(atom: import("jotai").Atom<Promise<Value_1>>): Value_1;
214
- <Value_2>(atom: import("jotai").Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
215
- } & {
216
- <Value_3>(atom: import("jotai").Atom<Value_3 | Promise<Value_3>>, options: {
217
- unstable_promise: true;
218
- }): Value_3 | Promise<Value_3>;
219
- <Value_4>(atom: import("jotai").Atom<Promise<Value_4>>, options: {
220
- unstable_promise: true;
221
- }): Value_4 | Promise<Value_4>;
222
- <Value_5>(atom: import("jotai").Atom<Value_5>, options: {
223
- unstable_promise: true;
224
- }): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
225
- }, set: {
226
- <Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
227
- <Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
228
- }, update: unknown) => void;
229
- onMount?: (<S extends (update?: unknown) => void>(setAtom: S) => void | (() => void)) | undefined;
230
- } & {
231
- init: null;
232
- };
233
- remove(param: InternalFormId): void;
234
- setShouldRemove(shouldRemove: ((createdAt: number, param: InternalFormId) => boolean) | null): void;
235
- };
236
209
  export declare const startSubmitAtom: {
237
210
  (param: InternalFormId): import("jotai").Atom<null> & {
238
211
  write: (get: {
@@ -26,11 +26,6 @@ export const cleanupFormState = (formId) => {
26
26
  ].forEach((formAtom) => formAtom.remove(formId));
27
27
  };
28
28
  export const isValidAtom = atomFamily((formId) => atom((get) => Object.keys(get(fieldErrorsAtom(formId))).length === 0));
29
- export const resetAtom = atomFamily((formId) => atom(null, (_get, set) => {
30
- set(fieldErrorsAtom(formId), {});
31
- set(touchedFieldsAtom(formId), {});
32
- set(hasBeenSubmittedAtom(formId), false);
33
- }));
34
29
  export const startSubmitAtom = atomFamily((formId) => atom(null, (_get, set) => {
35
30
  set(isSubmittingAtom(formId), true);
36
31
  set(hasBeenSubmittedAtom(formId), true);
@@ -14,7 +14,7 @@ import { ValidatorError } from "./validation/types";
14
14
  * if (result.error) return validationError(result.error, result.submittedData);
15
15
  * ```
16
16
  */
17
- export declare function validationError(error: ValidatorError, repopulateFields?: unknown): Response;
17
+ export declare function validationError(error: ValidatorError, repopulateFields?: unknown, init?: ResponseInit): Response;
18
18
  export declare type FormDefaults = {
19
19
  [formDefaultsKey: `${typeof FORM_DEFAULTS_FIELD}_${string}`]: any;
20
20
  };
package/browser/server.js CHANGED
@@ -14,13 +14,13 @@ import { formDefaultValuesKey, } from "./internal/constants";
14
14
  * if (result.error) return validationError(result.error, result.submittedData);
15
15
  * ```
16
16
  */
17
- export function validationError(error, repopulateFields) {
17
+ export function validationError(error, repopulateFields, init) {
18
18
  return json({
19
19
  fieldErrors: error.fieldErrors,
20
20
  subaction: error.subaction,
21
21
  repopulateFields,
22
22
  formId: error.formId,
23
- }, { status: 422 });
23
+ }, { status: 422, ...init });
24
24
  }
25
25
  export const setFormDefaults = (formId, defaultValues) => ({
26
26
  [formDefaultValuesKey(formId)]: defaultValues,
@@ -33,7 +33,9 @@ const constants_1 = require("./internal/constants");
33
33
  const formContext_1 = require("./internal/formContext");
34
34
  const hooks_2 = require("./internal/hooks");
35
35
  const MultiValueMap_1 = require("./internal/MultiValueMap");
36
+ const reset_1 = require("./internal/reset");
36
37
  const state_1 = require("./internal/state");
38
+ const controlledFields_1 = require("./internal/state/controlledFields");
37
39
  const submissionCallbacks_1 = require("./internal/submissionCallbacks");
38
40
  const util_1 = require("./internal/util");
39
41
  const getDataFromForm = (el) => new FormData(el);
@@ -138,7 +140,7 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
138
140
  const submit = (0, react_1.useSubmit)();
139
141
  const setFieldErrors = (0, hooks_2.useFormUpdateAtom)((0, state_1.fieldErrorsAtom)(formId));
140
142
  const setFieldError = (0, hooks_2.useFormUpdateAtom)((0, state_1.setFieldErrorAtom)(formId));
141
- const reset = (0, hooks_2.useFormUpdateAtom)((0, state_1.resetAtom)(formId));
143
+ const reset = (0, hooks_2.useFormUpdateAtom)((0, reset_1.resetAtom)(formId));
142
144
  const startSubmit = (0, hooks_2.useFormUpdateAtom)((0, state_1.startSubmitAtom)(formId));
143
145
  const endSubmit = (0, hooks_2.useFormUpdateAtom)((0, state_1.endSubmitAtom)(formId));
144
146
  const syncFormProps = (0, hooks_2.useFormUpdateAtom)((0, state_1.formPropsAtom)(formId));
@@ -148,8 +150,10 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
148
150
  setHydrated(true);
149
151
  return () => (0, state_1.cleanupFormState)(formId);
150
152
  }, [formId, setHydrated]);
153
+ const awaitValue = (0, controlledFields_1.useAwaitValue)(formId);
151
154
  const validateField = (0, react_2.useCallback)(async (field) => {
152
155
  (0, tiny_invariant_1.default)(formRef.current, "Cannot find reference to form");
156
+ await awaitValue(field);
153
157
  const { error } = await validator.validateField(getDataFromForm(formRef.current), field);
154
158
  if (error) {
155
159
  setFieldError({ field, error });
@@ -159,7 +163,7 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
159
163
  setFieldError({ field, error: undefined });
160
164
  return null;
161
165
  }
162
- }, [setFieldError, validator]);
166
+ }, [awaitValue, setFieldError, validator]);
163
167
  const customFocusHandlers = (0, MultiValueMap_1.useMultiValueMap)();
164
168
  const registerReceiveFocus = (0, react_2.useCallback)((fieldName, handler) => {
165
169
  customFocusHandlers().add(fieldName, handler);
package/build/hooks.d.ts CHANGED
@@ -63,3 +63,5 @@ export declare const useField: (name: string, options?: {
63
63
  */
64
64
  formId?: string | undefined;
65
65
  } | undefined) => FieldProps;
66
+ export declare const useControlField: <T>(name: string, formId?: string | undefined) => readonly [T, (value: T) => void];
67
+ export declare const useUpdateControlledField: (formId?: string | undefined) => (field: string, value: unknown) => void;
package/build/hooks.js CHANGED
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useField = exports.useIsValid = exports.useIsSubmitting = void 0;
3
+ exports.useUpdateControlledField = exports.useControlField = exports.useField = exports.useIsValid = exports.useIsSubmitting = void 0;
4
4
  const react_1 = require("react");
5
5
  const getInputProps_1 = require("./internal/getInputProps");
6
6
  const hooks_1 = require("./internal/hooks");
7
7
  const state_1 = require("./internal/state");
8
+ const controlledFields_1 = require("./internal/state/controlledFields");
8
9
  /**
9
10
  * Returns whether or not the parent form is currently being submitted.
10
11
  * This is different from remix's `useTransition().submission` in that it
@@ -77,3 +78,14 @@ const useField = (name, options) => {
77
78
  return field;
78
79
  };
79
80
  exports.useField = useField;
81
+ const useControlField = (name, formId) => {
82
+ const context = (0, hooks_1.useInternalFormContext)(formId, "useControlField");
83
+ const [value, setValue] = (0, controlledFields_1.useControllableValue)(context, name);
84
+ return [value, setValue];
85
+ };
86
+ exports.useControlField = useControlField;
87
+ const useUpdateControlledField = (formId) => {
88
+ const context = (0, hooks_1.useInternalFormContext)(formId, "useControlField");
89
+ return (0, controlledFields_1.useUpdateControllableValue)(context.formId);
90
+ };
91
+ exports.useUpdateControlledField = useUpdateControlledField;
@@ -1,9 +1,11 @@
1
1
  export declare class MultiValueMap<Key, Value> {
2
2
  private dict;
3
3
  add: (key: Key, value: Value) => void;
4
+ delete: (key: Key) => void;
4
5
  remove: (key: Key, value: Value) => void;
5
6
  getAll: (key: Key) => Value[];
6
7
  entries: () => IterableIterator<[Key, Value[]]>;
8
+ values: () => IterableIterator<Value[]>;
7
9
  has: (key: Key) => boolean;
8
10
  }
9
11
  export declare const useMultiValueMap: <Key, Value>() => () => MultiValueMap<Key, Value>;
@@ -13,6 +13,9 @@ class MultiValueMap {
13
13
  this.dict.set(key, [value]);
14
14
  }
15
15
  };
16
+ this.delete = (key) => {
17
+ this.dict.delete(key);
18
+ };
16
19
  this.remove = (key, value) => {
17
20
  if (!this.dict.has(key))
18
21
  return;
@@ -28,6 +31,7 @@ class MultiValueMap {
28
31
  return (_a = this.dict.get(key)) !== null && _a !== void 0 ? _a : [];
29
32
  };
30
33
  this.entries = () => this.dict.entries();
34
+ this.values = () => this.dict.values();
31
35
  this.has = (key) => this.dict.has(key);
32
36
  }
33
37
  }
@@ -48,7 +48,8 @@ const createGetInputProps = ({ clearError, validate, defaultValue, touched, setT
48
48
  else if (props.type === "radio") {
49
49
  inputProps.defaultChecked = (0, getRadioChecked_1.getRadioChecked)(props.value, defaultValue);
50
50
  }
51
- else {
51
+ else if (props.value === undefined) {
52
+ // We should only set the defaultValue if the input is uncontrolled.
52
53
  inputProps.defaultValue = defaultValue;
53
54
  }
54
55
  return (0, omitBy_1.default)(inputProps, (value) => value === undefined);
@@ -0,0 +1,28 @@
1
+ import { InternalFormId } from "./state/atomUtils";
2
+ export declare const resetAtom: {
3
+ (param: InternalFormId): import("jotai").Atom<null> & {
4
+ write: (get: {
5
+ <Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
6
+ <Value_1>(atom: import("jotai").Atom<Promise<Value_1>>): Value_1;
7
+ <Value_2>(atom: import("jotai").Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
8
+ } & {
9
+ <Value_3>(atom: import("jotai").Atom<Value_3 | Promise<Value_3>>, options: {
10
+ unstable_promise: true;
11
+ }): Value_3 | Promise<Value_3>;
12
+ <Value_4>(atom: import("jotai").Atom<Promise<Value_4>>, options: {
13
+ unstable_promise: true;
14
+ }): Value_4 | Promise<Value_4>;
15
+ <Value_5>(atom: import("jotai").Atom<Value_5>, options: {
16
+ unstable_promise: true;
17
+ }): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
18
+ }, set: {
19
+ <Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
20
+ <Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
21
+ }, update: unknown) => void;
22
+ onMount?: (<S extends (update?: unknown) => void>(setAtom: S) => void | (() => void)) | undefined;
23
+ } & {
24
+ init: null;
25
+ };
26
+ remove(param: InternalFormId): void;
27
+ setShouldRemove(shouldRemove: ((createdAt: number, param: InternalFormId) => boolean) | null): void;
28
+ };