remix-validated-form 4.2.0 → 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.
@@ -1,9 +1,9 @@
1
1
  $ npm run build:browser && npm run build:main
2
2
 
3
- > remix-validated-form@4.1.9 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.9 build:main
7
+ > remix-validated-form@4.2.0 build:main
8
8
  > tsc --module CommonJS --outDir ./build
9
9
 
@@ -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,5 +1,6 @@
1
1
  import { PrimitiveAtom } from "jotai";
2
- import { InternalFormId } from "./atomUtils";
2
+ import { InternalFormContextValue } from "../formContext";
3
+ import { FieldAtomKey, InternalFormId } from "./atomUtils";
3
4
  export declare const controlledFieldsAtom: {
4
5
  (param: InternalFormId): import("jotai").Atom<Record<string, PrimitiveAtom<unknown>>> & {
5
6
  write: (get: {
@@ -27,6 +28,60 @@ export declare const controlledFieldsAtom: {
27
28
  remove(param: InternalFormId): void;
28
29
  setShouldRemove(shouldRemove: ((createdAt: number, param: InternalFormId) => boolean) | null): void;
29
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
+ };
30
85
  export declare const setControlledFieldValueAtom: import("jotai").Atom<null> & {
31
86
  write: (get: {
32
87
  <Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
@@ -49,14 +104,16 @@ export declare const setControlledFieldValueAtom: import("jotai").Atom<null> & {
49
104
  formId: InternalFormId;
50
105
  field: string;
51
106
  value: unknown;
52
- }) => Promise<void>;
107
+ }) => void;
53
108
  onMount?: (<S extends (update: {
54
109
  formId: InternalFormId;
55
110
  field: string;
56
111
  value: unknown;
57
- }) => Promise<void>>(setAtom: S) => void | (() => void)) | undefined;
112
+ }) => void>(setAtom: S) => void | (() => void)) | undefined;
58
113
  } & {
59
114
  init: null;
60
115
  };
61
- export declare const useControlledFieldValue: (formId: InternalFormId, field: string) => any;
62
- export declare const useControllableValue: (formId: InternalFormId, field: string) => readonly [any, (value: unknown) => Promise<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,7 +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
+ export const valueUpdatePromiseAtom = fieldAtomFamily(() => atom(undefined));
13
+ export const resolveValueUpdateAtom = fieldAtomFamily(() => atom(undefined));
12
14
  const registerAtom = atom(null, (get, set, { formId, field }) => {
13
15
  set(refCountAtom({ formId, field }), (prev) => prev + 1);
14
16
  const newRefCount = get(refCountAtom({ formId, field }));
@@ -27,22 +29,27 @@ const unregisterAtom = atom(null, (get, set, { formId, field }) => {
27
29
  if (newRefCount === 0) {
28
30
  set(controlledFieldsAtom(formId), (prev) => omit(prev, field));
29
31
  fieldValueAtom.remove({ formId, field });
30
- pendingValidateAtom.remove({ formId, field });
32
+ resolveValueUpdateAtom.remove({ formId, field });
31
33
  fieldValueHydratedAtom.remove({ formId, field });
32
34
  }
33
35
  });
34
- export const setControlledFieldValueAtom = atom(null, async (_get, set, { formId, field, value, }) => {
36
+ export const setControlledFieldValueAtom = atom(null, (_get, set, { formId, field, value, }) => {
35
37
  set(fieldValueAtom({ formId, field }), value);
36
- const pending = pendingValidateAtom({ formId, field });
37
- await new Promise((resolve) => set(pending, resolve));
38
- 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);
39
46
  });
40
- export const useControlledFieldValue = (formId, field) => {
41
- const fieldAtom = fieldValueAtom({ formId, field });
47
+ export const useControlledFieldValue = (context, field) => {
48
+ const fieldAtom = fieldValueAtom({ formId: context.formId, field });
42
49
  const [value, setValue] = useFormAtom(fieldAtom);
43
- const defaultValue = useFieldDefaultValue(field, { formId });
44
- const isHydrated = useFormAtomValue(isHydratedAtom(formId));
45
- 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 }));
46
53
  useEffect(() => {
47
54
  if (isHydrated && !isFieldHydrated) {
48
55
  setValue(defaultValue);
@@ -51,7 +58,7 @@ export const useControlledFieldValue = (formId, field) => {
51
58
  }, [
52
59
  defaultValue,
53
60
  field,
54
- formId,
61
+ context.formId,
55
62
  isFieldHydrated,
56
63
  isHydrated,
57
64
  setIsFieldHydrated,
@@ -59,19 +66,28 @@ export const useControlledFieldValue = (formId, field) => {
59
66
  ]);
60
67
  return isFieldHydrated ? value : defaultValue;
61
68
  };
62
- export const useControllableValue = (formId, field) => {
63
- const pending = useFormAtomValue(pendingValidateAtom({ formId, field }));
69
+ export const useControllableValue = (context, field) => {
70
+ const resolveUpdate = useFormAtomValue(resolveValueUpdateAtom({ formId: context.formId, field }));
64
71
  useEffect(() => {
65
- pending === null || pending === void 0 ? void 0 : pending();
66
- }, [pending]);
72
+ resolveUpdate === null || resolveUpdate === void 0 ? void 0 : resolveUpdate();
73
+ }, [resolveUpdate]);
67
74
  const register = useFormUpdateAtom(registerAtom);
68
75
  const unregister = useFormUpdateAtom(unregisterAtom);
69
76
  useEffect(() => {
70
- register({ formId, field });
71
- return () => unregister({ formId, field });
72
- }, [field, formId, register, unregister]);
77
+ register({ formId: context.formId, field });
78
+ return () => unregister({ formId: context.formId, field });
79
+ }, [context.formId, field, register, unregister]);
73
80
  const setControlledFieldValue = useFormUpdateAtom(setControlledFieldValueAtom);
74
- const setValue = useCallback((value) => setControlledFieldValue({ formId, field, value }), [field, formId, setControlledFieldValue]);
75
- 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);
76
83
  return [value, setValue];
77
84
  };
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
+ };
@@ -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);
@@ -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
+ };
@@ -0,0 +1,19 @@
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.resetAtom = void 0;
7
+ const jotai_1 = require("jotai");
8
+ const utils_1 = require("jotai/utils");
9
+ const get_1 = __importDefault(require("lodash/get"));
10
+ const state_1 = require("./state");
11
+ const controlledFields_1 = require("./state/controlledFields");
12
+ exports.resetAtom = (0, utils_1.atomFamily)((formId) => (0, jotai_1.atom)(null, (get, set) => {
13
+ set((0, state_1.fieldErrorsAtom)(formId), {});
14
+ set((0, state_1.touchedFieldsAtom)(formId), {});
15
+ set((0, state_1.hasBeenSubmittedAtom)(formId), false);
16
+ const { defaultValues } = get((0, state_1.formPropsAtom)(formId));
17
+ const controlledFields = get((0, controlledFields_1.controlledFieldsAtom)(formId));
18
+ Object.entries(controlledFields).forEach(([name, atom]) => set(atom, (0, get_1.default)(defaultValues, name)));
19
+ }));
@@ -1,5 +1,6 @@
1
1
  import { PrimitiveAtom } from "jotai";
2
- import { InternalFormId } from "./atomUtils";
2
+ import { InternalFormContextValue } from "../formContext";
3
+ import { FieldAtomKey, InternalFormId } from "./atomUtils";
3
4
  export declare const controlledFieldsAtom: {
4
5
  (param: InternalFormId): import("jotai").Atom<Record<string, PrimitiveAtom<unknown>>> & {
5
6
  write: (get: {
@@ -27,6 +28,60 @@ export declare const controlledFieldsAtom: {
27
28
  remove(param: InternalFormId): void;
28
29
  setShouldRemove(shouldRemove: ((createdAt: number, param: InternalFormId) => boolean) | null): void;
29
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
+ };
30
85
  export declare const setControlledFieldValueAtom: import("jotai").Atom<null> & {
31
86
  write: (get: {
32
87
  <Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
@@ -49,14 +104,16 @@ export declare const setControlledFieldValueAtom: import("jotai").Atom<null> & {
49
104
  formId: InternalFormId;
50
105
  field: string;
51
106
  value: unknown;
52
- }) => Promise<void>;
107
+ }) => void;
53
108
  onMount?: (<S extends (update: {
54
109
  formId: InternalFormId;
55
110
  field: string;
56
111
  value: unknown;
57
- }) => Promise<void>>(setAtom: S) => void | (() => void)) | undefined;
112
+ }) => void>(setAtom: S) => void | (() => void)) | undefined;
58
113
  } & {
59
114
  init: null;
60
115
  };
61
- export declare const useControlledFieldValue: (formId: InternalFormId, field: string) => any;
62
- export declare const useControllableValue: (formId: InternalFormId, field: string) => readonly [any, (value: unknown) => Promise<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>;
@@ -3,8 +3,9 @@ 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.useControllableValue = exports.useControlledFieldValue = exports.setControlledFieldValueAtom = exports.controlledFieldsAtom = void 0;
6
+ exports.useAwaitValue = exports.useUpdateControllableValue = exports.useControllableValue = exports.useControlledFieldValue = exports.setControlledFieldValueAtom = exports.resolveValueUpdateAtom = exports.valueUpdatePromiseAtom = exports.controlledFieldsAtom = void 0;
7
7
  const jotai_1 = require("jotai");
8
+ const utils_1 = require("jotai/utils");
8
9
  const omit_1 = __importDefault(require("lodash/omit"));
9
10
  const react_1 = require("react");
10
11
  const hooks_1 = require("../hooks");
@@ -14,7 +15,8 @@ exports.controlledFieldsAtom = (0, atomUtils_1.formAtomFamily)({});
14
15
  const refCountAtom = (0, atomUtils_1.fieldAtomFamily)(() => (0, jotai_1.atom)(0));
15
16
  const fieldValueAtom = (0, atomUtils_1.fieldAtomFamily)(() => (0, jotai_1.atom)(undefined));
16
17
  const fieldValueHydratedAtom = (0, atomUtils_1.fieldAtomFamily)(() => (0, jotai_1.atom)(false));
17
- const pendingValidateAtom = (0, atomUtils_1.fieldAtomFamily)(() => (0, jotai_1.atom)(undefined));
18
+ exports.valueUpdatePromiseAtom = (0, atomUtils_1.fieldAtomFamily)(() => (0, jotai_1.atom)(undefined));
19
+ exports.resolveValueUpdateAtom = (0, atomUtils_1.fieldAtomFamily)(() => (0, jotai_1.atom)(undefined));
18
20
  const registerAtom = (0, jotai_1.atom)(null, (get, set, { formId, field }) => {
19
21
  set(refCountAtom({ formId, field }), (prev) => prev + 1);
20
22
  const newRefCount = get(refCountAtom({ formId, field }));
@@ -33,22 +35,27 @@ const unregisterAtom = (0, jotai_1.atom)(null, (get, set, { formId, field }) =>
33
35
  if (newRefCount === 0) {
34
36
  set((0, exports.controlledFieldsAtom)(formId), (prev) => (0, omit_1.default)(prev, field));
35
37
  fieldValueAtom.remove({ formId, field });
36
- pendingValidateAtom.remove({ formId, field });
38
+ exports.resolveValueUpdateAtom.remove({ formId, field });
37
39
  fieldValueHydratedAtom.remove({ formId, field });
38
40
  }
39
41
  });
40
- exports.setControlledFieldValueAtom = (0, jotai_1.atom)(null, async (_get, set, { formId, field, value, }) => {
42
+ exports.setControlledFieldValueAtom = (0, jotai_1.atom)(null, (_get, set, { formId, field, value, }) => {
41
43
  set(fieldValueAtom({ formId, field }), value);
42
- const pending = pendingValidateAtom({ formId, field });
43
- await new Promise((resolve) => set(pending, resolve));
44
- set(pending, undefined);
44
+ const resolveAtom = (0, exports.resolveValueUpdateAtom)({ formId, field });
45
+ const promiseAtom = (0, exports.valueUpdatePromiseAtom)({ formId, field });
46
+ const promise = new Promise((resolve) => set(resolveAtom, () => {
47
+ resolve();
48
+ set(resolveAtom, undefined);
49
+ set(promiseAtom, undefined);
50
+ }));
51
+ set(promiseAtom, promise);
45
52
  });
46
- const useControlledFieldValue = (formId, field) => {
47
- const fieldAtom = fieldValueAtom({ formId, field });
53
+ const useControlledFieldValue = (context, field) => {
54
+ const fieldAtom = fieldValueAtom({ formId: context.formId, field });
48
55
  const [value, setValue] = (0, hooks_1.useFormAtom)(fieldAtom);
49
- const defaultValue = (0, hooks_1.useFieldDefaultValue)(field, { formId });
50
- const isHydrated = (0, hooks_1.useFormAtomValue)((0, state_1.isHydratedAtom)(formId));
51
- const [isFieldHydrated, setIsFieldHydrated] = (0, hooks_1.useFormAtom)(fieldValueHydratedAtom({ formId, field }));
56
+ const defaultValue = (0, hooks_1.useFieldDefaultValue)(field, context);
57
+ const isHydrated = (0, hooks_1.useFormAtomValue)((0, state_1.isHydratedAtom)(context.formId));
58
+ const [isFieldHydrated, setIsFieldHydrated] = (0, hooks_1.useFormAtom)(fieldValueHydratedAtom({ formId: context.formId, field }));
52
59
  (0, react_1.useEffect)(() => {
53
60
  if (isHydrated && !isFieldHydrated) {
54
61
  setValue(defaultValue);
@@ -57,7 +64,7 @@ const useControlledFieldValue = (formId, field) => {
57
64
  }, [
58
65
  defaultValue,
59
66
  field,
60
- formId,
67
+ context.formId,
61
68
  isFieldHydrated,
62
69
  isHydrated,
63
70
  setIsFieldHydrated,
@@ -66,20 +73,31 @@ const useControlledFieldValue = (formId, field) => {
66
73
  return isFieldHydrated ? value : defaultValue;
67
74
  };
68
75
  exports.useControlledFieldValue = useControlledFieldValue;
69
- const useControllableValue = (formId, field) => {
70
- const pending = (0, hooks_1.useFormAtomValue)(pendingValidateAtom({ formId, field }));
76
+ const useControllableValue = (context, field) => {
77
+ const resolveUpdate = (0, hooks_1.useFormAtomValue)((0, exports.resolveValueUpdateAtom)({ formId: context.formId, field }));
71
78
  (0, react_1.useEffect)(() => {
72
- pending === null || pending === void 0 ? void 0 : pending();
73
- }, [pending]);
79
+ resolveUpdate === null || resolveUpdate === void 0 ? void 0 : resolveUpdate();
80
+ }, [resolveUpdate]);
74
81
  const register = (0, hooks_1.useFormUpdateAtom)(registerAtom);
75
82
  const unregister = (0, hooks_1.useFormUpdateAtom)(unregisterAtom);
76
83
  (0, react_1.useEffect)(() => {
77
- register({ formId, field });
78
- return () => unregister({ formId, field });
79
- }, [field, formId, register, unregister]);
84
+ register({ formId: context.formId, field });
85
+ return () => unregister({ formId: context.formId, field });
86
+ }, [context.formId, field, register, unregister]);
80
87
  const setControlledFieldValue = (0, hooks_1.useFormUpdateAtom)(exports.setControlledFieldValueAtom);
81
- const setValue = (0, react_1.useCallback)((value) => setControlledFieldValue({ formId, field, value }), [field, formId, setControlledFieldValue]);
82
- const value = (0, exports.useControlledFieldValue)(formId, field);
88
+ const setValue = (0, react_1.useCallback)((value) => setControlledFieldValue({ formId: context.formId, field, value }), [field, context.formId, setControlledFieldValue]);
89
+ const value = (0, exports.useControlledFieldValue)(context, field);
83
90
  return [value, setValue];
84
91
  };
85
92
  exports.useControllableValue = useControllableValue;
93
+ const useUpdateControllableValue = (formId) => {
94
+ const setControlledFieldValue = (0, hooks_1.useFormUpdateAtom)(exports.setControlledFieldValueAtom);
95
+ return (0, react_1.useCallback)((field, value) => setControlledFieldValue({ formId, field, value }), [formId, setControlledFieldValue]);
96
+ };
97
+ exports.useUpdateControllableValue = useUpdateControllableValue;
98
+ const useAwaitValue = (formId) => {
99
+ return (0, utils_1.useAtomCallback)((0, react_1.useCallback)(async (get, _set, field) => {
100
+ await get((0, exports.valueUpdatePromiseAtom)({ formId, field }));
101
+ }, [formId]));
102
+ };
103
+ exports.useAwaitValue = useAwaitValue;
@@ -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: {
@@ -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.fieldDefaultValueAtom = exports.fieldErrorAtom = exports.fieldTouchedAtom = exports.setFieldErrorAtom = exports.setTouchedAtom = exports.endSubmitAtom = exports.startSubmitAtom = exports.resetAtom = exports.isValidAtom = exports.cleanupFormState = exports.formElementAtom = exports.formPropsAtom = exports.touchedFieldsAtom = exports.fieldErrorsAtom = exports.hasBeenSubmittedAtom = exports.isSubmittingAtom = exports.isHydratedAtom = exports.ATOM_SCOPE = void 0;
6
+ exports.fieldDefaultValueAtom = exports.fieldErrorAtom = exports.fieldTouchedAtom = exports.setFieldErrorAtom = exports.setTouchedAtom = exports.endSubmitAtom = exports.startSubmitAtom = exports.isValidAtom = exports.cleanupFormState = exports.formElementAtom = exports.formPropsAtom = exports.touchedFieldsAtom = exports.fieldErrorsAtom = exports.hasBeenSubmittedAtom = exports.isSubmittingAtom = exports.isHydratedAtom = exports.ATOM_SCOPE = void 0;
7
7
  const jotai_1 = require("jotai");
8
8
  const utils_1 = require("jotai/utils");
9
9
  const omit_1 = __importDefault(require("lodash/omit"));
@@ -33,11 +33,6 @@ const cleanupFormState = (formId) => {
33
33
  };
34
34
  exports.cleanupFormState = cleanupFormState;
35
35
  exports.isValidAtom = (0, utils_1.atomFamily)((formId) => (0, jotai_1.atom)((get) => Object.keys(get((0, exports.fieldErrorsAtom)(formId))).length === 0));
36
- exports.resetAtom = (0, utils_1.atomFamily)((formId) => (0, jotai_1.atom)(null, (_get, set) => {
37
- set((0, exports.fieldErrorsAtom)(formId), {});
38
- set((0, exports.touchedFieldsAtom)(formId), {});
39
- set((0, exports.hasBeenSubmittedAtom)(formId), false);
40
- }));
41
36
  exports.startSubmitAtom = (0, utils_1.atomFamily)((formId) => (0, jotai_1.atom)(null, (_get, set) => {
42
37
  set((0, exports.isSubmittingAtom)(formId), true);
43
38
  set((0, exports.hasBeenSubmittedAtom)(formId), true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remix-validated-form",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
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",
@@ -1,4 +1,5 @@
1
1
  import { Form as RemixForm, useFetcher, useSubmit } from "@remix-run/react";
2
+ import { useAtomCallback } from "jotai/utils";
2
3
  import uniq from "lodash/uniq";
3
4
  import React, {
4
5
  ComponentProps,
@@ -24,6 +25,7 @@ import {
24
25
  useHasActiveFormSubmit,
25
26
  } from "./internal/hooks";
26
27
  import { MultiValueMap, useMultiValueMap } from "./internal/MultiValueMap";
28
+ import { resetAtom } from "./internal/reset";
27
29
  import {
28
30
  cleanupFormState,
29
31
  endSubmitAtom,
@@ -31,11 +33,11 @@ import {
31
33
  formElementAtom,
32
34
  formPropsAtom,
33
35
  isHydratedAtom,
34
- resetAtom,
35
36
  setFieldErrorAtom,
36
37
  startSubmitAtom,
37
38
  SyncedFormProps,
38
39
  } from "./internal/state";
40
+ import { useAwaitValue } from "./internal/state/controlledFields";
39
41
  import { useSubmitComplete } from "./internal/submissionCallbacks";
40
42
  import {
41
43
  mergeRefs,
@@ -246,9 +248,11 @@ export function ValidatedForm<DataType>({
246
248
  return () => cleanupFormState(formId);
247
249
  }, [formId, setHydrated]);
248
250
 
251
+ const awaitValue = useAwaitValue(formId);
249
252
  const validateField: SyncedFormProps["validateField"] = useCallback(
250
253
  async (field) => {
251
254
  invariant(formRef.current, "Cannot find reference to form");
255
+ await awaitValue(field);
252
256
  const { error } = await validator.validateField(
253
257
  getDataFromForm(formRef.current),
254
258
  field
@@ -262,7 +266,7 @@ export function ValidatedForm<DataType>({
262
266
  return null;
263
267
  }
264
268
  },
265
- [setFieldError, validator]
269
+ [awaitValue, setFieldError, validator]
266
270
  );
267
271
 
268
272
  const customFocusHandlers = useMultiValueMap<string, () => void>();
package/src/hooks.ts CHANGED
@@ -17,6 +17,10 @@ import {
17
17
  isSubmittingAtom,
18
18
  isValidAtom,
19
19
  } from "./internal/state";
20
+ import {
21
+ useControllableValue,
22
+ useUpdateControllableValue,
23
+ } from "./internal/state/controlledFields";
20
24
 
21
25
  /**
22
26
  * Returns whether or not the parent form is currently being submitted.
@@ -148,3 +152,14 @@ export const useField = (
148
152
 
149
153
  return field;
150
154
  };
155
+
156
+ export const useControlField = <T>(name: string, formId?: string) => {
157
+ const context = useInternalFormContext(formId, "useControlField");
158
+ const [value, setValue] = useControllableValue(context, name);
159
+ return [value as T, setValue as (value: T) => void] as const;
160
+ };
161
+
162
+ export const useUpdateControlledField = (formId?: string) => {
163
+ const context = useInternalFormContext(formId, "useControlField");
164
+ return useUpdateControllableValue(context.formId);
165
+ };
@@ -11,6 +11,10 @@ export class MultiValueMap<Key, Value> {
11
11
  }
12
12
  };
13
13
 
14
+ delete = (key: Key) => {
15
+ this.dict.delete(key);
16
+ };
17
+
14
18
  remove = (key: Key, value: Value) => {
15
19
  if (!this.dict.has(key)) return;
16
20
  const array = this.dict.get(key)!;
@@ -25,6 +29,8 @@ export class MultiValueMap<Key, Value> {
25
29
 
26
30
  entries = (): IterableIterator<[Key, Value[]]> => this.dict.entries();
27
31
 
32
+ values = (): IterableIterator<Value[]> => this.dict.values();
33
+
28
34
  has = (key: Key): boolean => this.dict.has(key);
29
35
  }
30
36
 
@@ -84,7 +84,8 @@ export const createGetInputProps = ({
84
84
  inputProps.defaultChecked = getCheckboxChecked(props.value, defaultValue);
85
85
  } else if (props.type === "radio") {
86
86
  inputProps.defaultChecked = getRadioChecked(props.value, defaultValue);
87
- } else {
87
+ } else if (props.value === undefined) {
88
+ // We should only set the defaultValue if the input is uncontrolled.
88
89
  inputProps.defaultValue = defaultValue;
89
90
  }
90
91
 
@@ -0,0 +1,26 @@
1
+ import { atom } from "jotai";
2
+ import { atomFamily } from "jotai/utils";
3
+ import lodashGet from "lodash/get";
4
+ import {
5
+ fieldErrorsAtom,
6
+ formPropsAtom,
7
+ hasBeenSubmittedAtom,
8
+ touchedFieldsAtom,
9
+ } from "./state";
10
+ import { InternalFormId } from "./state/atomUtils";
11
+ import { controlledFieldsAtom } from "./state/controlledFields";
12
+
13
+ export const resetAtom = atomFamily((formId: InternalFormId) =>
14
+ atom(null, (get, set) => {
15
+ set(fieldErrorsAtom(formId), {});
16
+ set(touchedFieldsAtom(formId), {});
17
+ set(hasBeenSubmittedAtom(formId), false);
18
+
19
+ const { defaultValues } = get(formPropsAtom(formId));
20
+
21
+ const controlledFields = get(controlledFieldsAtom(formId));
22
+ Object.entries(controlledFields).forEach(([name, atom]) =>
23
+ set(atom, lodashGet(defaultValues, name))
24
+ );
25
+ })
26
+ );
@@ -0,0 +1,170 @@
1
+ import { atom, PrimitiveAtom } from "jotai";
2
+ import { useAtomCallback } from "jotai/utils";
3
+ import omit from "lodash/omit";
4
+ import { useCallback, useEffect } from "react";
5
+ import { InternalFormContextValue } from "../formContext";
6
+ import {
7
+ useFieldDefaultValue,
8
+ useFormAtomValue,
9
+ useFormAtom,
10
+ useFormUpdateAtom,
11
+ } from "../hooks";
12
+ import { isHydratedAtom } from "../state";
13
+ import {
14
+ fieldAtomFamily,
15
+ FieldAtomKey,
16
+ formAtomFamily,
17
+ InternalFormId,
18
+ } from "./atomUtils";
19
+
20
+ export const controlledFieldsAtom = formAtomFamily<
21
+ Record<string, PrimitiveAtom<unknown>>
22
+ >({});
23
+ const refCountAtom = fieldAtomFamily(() => atom(0));
24
+ const fieldValueAtom = fieldAtomFamily(() => atom<unknown>(undefined));
25
+ const fieldValueHydratedAtom = fieldAtomFamily(() => atom(false));
26
+
27
+ export const valueUpdatePromiseAtom = fieldAtomFamily(() =>
28
+ atom<Promise<void> | undefined>(undefined)
29
+ );
30
+ export const resolveValueUpdateAtom = fieldAtomFamily(() =>
31
+ atom<(() => void) | undefined>(undefined)
32
+ );
33
+
34
+ const registerAtom = atom(null, (get, set, { formId, field }: FieldAtomKey) => {
35
+ set(refCountAtom({ formId, field }), (prev) => prev + 1);
36
+ const newRefCount = get(refCountAtom({ formId, field }));
37
+ // We don't set hydrated here because it gets set when we know
38
+ // we have the right default values
39
+ if (newRefCount === 1) {
40
+ set(controlledFieldsAtom(formId), (prev) => ({
41
+ ...prev,
42
+ [field]: fieldValueAtom({ formId, field }),
43
+ }));
44
+ }
45
+ });
46
+
47
+ const unregisterAtom = atom(
48
+ null,
49
+ (get, set, { formId, field }: FieldAtomKey) => {
50
+ set(refCountAtom({ formId, field }), (prev) => prev - 1);
51
+ const newRefCount = get(refCountAtom({ formId, field }));
52
+ if (newRefCount === 0) {
53
+ set(controlledFieldsAtom(formId), (prev) => omit(prev, field));
54
+ fieldValueAtom.remove({ formId, field });
55
+ resolveValueUpdateAtom.remove({ formId, field });
56
+ fieldValueHydratedAtom.remove({ formId, field });
57
+ }
58
+ }
59
+ );
60
+
61
+ export const setControlledFieldValueAtom = atom(
62
+ null,
63
+ (
64
+ _get,
65
+ set,
66
+ {
67
+ formId,
68
+ field,
69
+ value,
70
+ }: { formId: InternalFormId; field: string; value: unknown }
71
+ ) => {
72
+ set(fieldValueAtom({ formId, field }), value);
73
+ const resolveAtom = resolveValueUpdateAtom({ formId, field });
74
+ const promiseAtom = valueUpdatePromiseAtom({ formId, field });
75
+
76
+ const promise = new Promise<void>((resolve) =>
77
+ set(resolveAtom, () => {
78
+ resolve();
79
+ set(resolveAtom, undefined);
80
+ set(promiseAtom, undefined);
81
+ })
82
+ );
83
+ set(promiseAtom, promise);
84
+ }
85
+ );
86
+
87
+ export const useControlledFieldValue = (
88
+ context: InternalFormContextValue,
89
+ field: string
90
+ ) => {
91
+ const fieldAtom = fieldValueAtom({ formId: context.formId, field });
92
+ const [value, setValue] = useFormAtom(fieldAtom);
93
+
94
+ const defaultValue = useFieldDefaultValue(field, context);
95
+ const isHydrated = useFormAtomValue(isHydratedAtom(context.formId));
96
+ const [isFieldHydrated, setIsFieldHydrated] = useFormAtom(
97
+ fieldValueHydratedAtom({ formId: context.formId, field })
98
+ );
99
+
100
+ useEffect(() => {
101
+ if (isHydrated && !isFieldHydrated) {
102
+ setValue(defaultValue);
103
+ setIsFieldHydrated(true);
104
+ }
105
+ }, [
106
+ defaultValue,
107
+ field,
108
+ context.formId,
109
+ isFieldHydrated,
110
+ isHydrated,
111
+ setIsFieldHydrated,
112
+ setValue,
113
+ ]);
114
+
115
+ return isFieldHydrated ? value : defaultValue;
116
+ };
117
+
118
+ export const useControllableValue = (
119
+ context: InternalFormContextValue,
120
+ field: string
121
+ ) => {
122
+ const resolveUpdate = useFormAtomValue(
123
+ resolveValueUpdateAtom({ formId: context.formId, field })
124
+ );
125
+ useEffect(() => {
126
+ resolveUpdate?.();
127
+ }, [resolveUpdate]);
128
+
129
+ const register = useFormUpdateAtom(registerAtom);
130
+ const unregister = useFormUpdateAtom(unregisterAtom);
131
+ useEffect(() => {
132
+ register({ formId: context.formId, field });
133
+ return () => unregister({ formId: context.formId, field });
134
+ }, [context.formId, field, register, unregister]);
135
+
136
+ const setControlledFieldValue = useFormUpdateAtom(
137
+ setControlledFieldValueAtom
138
+ );
139
+ const setValue = useCallback(
140
+ (value: unknown) =>
141
+ setControlledFieldValue({ formId: context.formId, field, value }),
142
+ [field, context.formId, setControlledFieldValue]
143
+ );
144
+
145
+ const value = useControlledFieldValue(context, field);
146
+
147
+ return [value, setValue] as const;
148
+ };
149
+
150
+ export const useUpdateControllableValue = (formId: InternalFormId) => {
151
+ const setControlledFieldValue = useFormUpdateAtom(
152
+ setControlledFieldValueAtom
153
+ );
154
+ return useCallback(
155
+ (field: string, value: unknown) =>
156
+ setControlledFieldValue({ formId, field, value }),
157
+ [formId, setControlledFieldValue]
158
+ );
159
+ };
160
+
161
+ export const useAwaitValue = (formId: InternalFormId) => {
162
+ return useAtomCallback(
163
+ useCallback(
164
+ async (get, _set, field: string) => {
165
+ await get(valueUpdatePromiseAtom({ formId, field }));
166
+ },
167
+ [formId]
168
+ )
169
+ );
170
+ };
@@ -48,14 +48,6 @@ export const isValidAtom = atomFamily((formId: InternalFormId) =>
48
48
  atom((get) => Object.keys(get(fieldErrorsAtom(formId))).length === 0)
49
49
  );
50
50
 
51
- export const resetAtom = atomFamily((formId: InternalFormId) =>
52
- atom(null, (_get, set) => {
53
- set(fieldErrorsAtom(formId), {});
54
- set(touchedFieldsAtom(formId), {});
55
- set(hasBeenSubmittedAtom(formId), false);
56
- })
57
- );
58
-
59
51
  export const startSubmitAtom = atomFamily((formId: InternalFormId) =>
60
52
  atom(null, (_get, set) => {
61
53
  set(isSubmittingAtom(formId), true);