remix-validated-form 4.0.1-beta.2 → 4.1.0-beta.2

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 (66) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/README.md +4 -4
  3. package/browser/ValidatedForm.d.ts +2 -2
  4. package/browser/ValidatedForm.js +137 -149
  5. package/browser/components.d.ts +5 -8
  6. package/browser/components.js +5 -5
  7. package/browser/hooks.d.ts +19 -14
  8. package/browser/hooks.js +41 -39
  9. package/browser/index.d.ts +1 -1
  10. package/browser/index.js +1 -0
  11. package/browser/internal/constants.d.ts +3 -0
  12. package/browser/internal/constants.js +3 -0
  13. package/browser/internal/formContext.d.ts +7 -49
  14. package/browser/internal/formContext.js +1 -1
  15. package/browser/internal/getInputProps.js +4 -3
  16. package/browser/internal/hooks.d.ts +22 -0
  17. package/browser/internal/hooks.js +110 -0
  18. package/browser/internal/state.d.ts +269 -0
  19. package/browser/internal/state.js +82 -0
  20. package/browser/internal/util.d.ts +1 -0
  21. package/browser/internal/util.js +2 -0
  22. package/browser/lowLevelHooks.d.ts +0 -0
  23. package/browser/lowLevelHooks.js +1 -0
  24. package/browser/server.d.ts +5 -0
  25. package/browser/server.js +5 -0
  26. package/browser/userFacingFormContext.d.ts +56 -0
  27. package/browser/userFacingFormContext.js +40 -0
  28. package/browser/validation/createValidator.js +4 -0
  29. package/browser/validation/types.d.ts +3 -0
  30. package/build/ValidatedForm.d.ts +2 -2
  31. package/build/ValidatedForm.js +133 -145
  32. package/build/hooks.d.ts +19 -14
  33. package/build/hooks.js +43 -45
  34. package/build/index.d.ts +1 -1
  35. package/build/index.js +1 -0
  36. package/build/internal/constants.d.ts +3 -0
  37. package/build/internal/constants.js +7 -0
  38. package/build/internal/formContext.d.ts +7 -49
  39. package/build/internal/formContext.js +2 -2
  40. package/build/internal/getInputProps.js +7 -3
  41. package/build/internal/hooks.d.ts +22 -0
  42. package/build/internal/hooks.js +130 -0
  43. package/build/internal/state.d.ts +269 -0
  44. package/build/internal/state.js +92 -0
  45. package/build/internal/util.d.ts +1 -0
  46. package/build/internal/util.js +3 -1
  47. package/build/server.d.ts +5 -0
  48. package/build/server.js +7 -1
  49. package/build/userFacingFormContext.d.ts +56 -0
  50. package/build/userFacingFormContext.js +44 -0
  51. package/build/validation/createValidator.js +4 -0
  52. package/build/validation/types.d.ts +3 -0
  53. package/package.json +3 -1
  54. package/src/ValidatedForm.tsx +199 -200
  55. package/src/hooks.ts +71 -54
  56. package/src/index.ts +1 -1
  57. package/src/internal/constants.ts +4 -0
  58. package/src/internal/formContext.ts +8 -49
  59. package/src/internal/getInputProps.ts +6 -4
  60. package/src/internal/hooks.ts +191 -0
  61. package/src/internal/state.ts +210 -0
  62. package/src/internal/util.ts +4 -0
  63. package/src/server.ts +16 -0
  64. package/src/userFacingFormContext.ts +129 -0
  65. package/src/validation/createValidator.ts +4 -0
  66. package/src/validation/types.ts +3 -1
@@ -1,3 +1,4 @@
1
+ import omitBy from "lodash/omitBy";
1
2
  const defaultValidationBehavior = {
2
3
  initial: "onBlur",
3
4
  whenTouched: "onChange",
@@ -43,17 +44,17 @@ export const createGetInputProps = ({ clearError, validate, defaultValue, touche
43
44
  },
44
45
  name,
45
46
  };
46
- if (inputProps.type === "checkbox") {
47
+ if (props.type === "checkbox") {
47
48
  const value = (_a = props.value) !== null && _a !== void 0 ? _a : "on";
48
49
  inputProps.defaultChecked = getCheckboxDefaultChecked(value, defaultValue);
49
50
  }
50
- else if (inputProps.type === "radio") {
51
+ else if (props.type === "radio") {
51
52
  const value = (_b = props.value) !== null && _b !== void 0 ? _b : "on";
52
53
  inputProps.defaultChecked = defaultValue === value;
53
54
  }
54
55
  else {
55
56
  inputProps.defaultValue = defaultValue;
56
57
  }
57
- return inputProps;
58
+ return omitBy(inputProps, (value) => value === undefined);
58
59
  };
59
60
  };
@@ -0,0 +1,22 @@
1
+ import { Atom } from "jotai";
2
+ import { useUpdateAtom } from "jotai/utils";
3
+ import { ValidationErrorResponseData } from "..";
4
+ import { InternalFormContextValue } from "./formContext";
5
+ import { FormAtom } from "./state";
6
+ declare type FormSelectorAtomCreator<T> = (formState: FormAtom) => Atom<T>;
7
+ declare const USE_HYDRATED_STATE: unique symbol;
8
+ export declare const useInternalFormContext: (formId?: string | symbol | undefined, hookName?: string | undefined) => InternalFormContextValue;
9
+ export declare const useContextSelectAtom: <T>(formId: string | symbol, selectorAtomCreator: FormSelectorAtomCreator<T>) => T extends Promise<infer V> ? V : T;
10
+ export declare const useHydratableSelector: <T, U>({ formId }: InternalFormContextValue, atomCreator: FormSelectorAtomCreator<T>, dataToUse: U | typeof USE_HYDRATED_STATE, selector?: (data: U) => T) => T | (T extends Promise<infer V> ? V : T);
11
+ export declare function useErrorResponseForForm({ fetcher, subaction, formId, }: InternalFormContextValue): ValidationErrorResponseData | null;
12
+ export declare const useFieldErrorsForForm: (context: InternalFormContextValue) => import("..").FieldErrors | typeof USE_HYDRATED_STATE | undefined;
13
+ export declare const useDefaultValuesFromLoader: ({ formId, }: InternalFormContextValue) => any;
14
+ export declare const useDefaultValuesForForm: (context: InternalFormContextValue) => any;
15
+ export declare const useHasActiveFormSubmit: ({ fetcher, }: InternalFormContextValue) => boolean;
16
+ export declare const useFieldTouched: (name: string, { formId }: InternalFormContextValue) => boolean;
17
+ export declare const useFieldError: (name: string, context: InternalFormContextValue) => string | undefined;
18
+ export declare const useFieldDefaultValue: (name: string, context: InternalFormContextValue) => any;
19
+ export declare const useFormUpdateAtom: typeof useUpdateAtom;
20
+ export declare const useClearError: (context: InternalFormContextValue) => (name: string) => void;
21
+ export declare const useSetTouched: (context: InternalFormContextValue) => (name: string, touched: boolean) => void;
22
+ export {};
@@ -0,0 +1,110 @@
1
+ import { useActionData, useMatches, useTransition } from "@remix-run/react";
2
+ import { useAtomValue, useUpdateAtom } from "jotai/utils";
3
+ import lodashGet from "lodash/get";
4
+ import identity from "lodash/identity";
5
+ import { useCallback, useContext, useMemo } from "react";
6
+ import { formDefaultValuesKey } from "./constants";
7
+ import { InternalFormContext } from "./formContext";
8
+ import { ATOM_SCOPE, clearErrorAtom, fieldDefaultValueAtom, fieldErrorAtom, fieldTouchedAtom, formRegistry, isHydratedAtom, setTouchedAtom, } from "./state";
9
+ const USE_HYDRATED_STATE = Symbol("USE_HYDRATED_STATE");
10
+ export const useInternalFormContext = (formId, hookName) => {
11
+ const formContext = useContext(InternalFormContext);
12
+ if (formId)
13
+ return { formId };
14
+ if (formContext)
15
+ return formContext;
16
+ throw new Error(`Unable to determine form for ${hookName}. Please use it inside a form or pass a 'formId'.`);
17
+ };
18
+ export const useContextSelectAtom = (formId, selectorAtomCreator) => {
19
+ const formAtom = formRegistry(formId);
20
+ const selectorAtom = useMemo(() => selectorAtomCreator(formAtom), [formAtom, selectorAtomCreator]);
21
+ return useAtomValue(selectorAtom, ATOM_SCOPE);
22
+ };
23
+ export const useHydratableSelector = ({ formId }, atomCreator, dataToUse, selector = identity) => {
24
+ const dataFromState = useContextSelectAtom(formId, atomCreator);
25
+ return dataToUse === USE_HYDRATED_STATE ? dataFromState : selector(dataToUse);
26
+ };
27
+ export function useErrorResponseForForm({ fetcher, subaction, formId, }) {
28
+ var _a;
29
+ const actionData = useActionData();
30
+ if (fetcher) {
31
+ if ((_a = fetcher.data) === null || _a === void 0 ? void 0 : _a.fieldErrors)
32
+ return fetcher.data;
33
+ return null;
34
+ }
35
+ if (!(actionData === null || actionData === void 0 ? void 0 : actionData.fieldErrors))
36
+ return null;
37
+ // If there's an explicit id, we should ignore data that has the wrong id
38
+ if (typeof formId === "string" && actionData.formId)
39
+ return actionData.formId === formId ? actionData : null;
40
+ if ((!subaction && !actionData.subaction) ||
41
+ actionData.subaction === subaction)
42
+ return actionData;
43
+ return null;
44
+ }
45
+ export const useFieldErrorsForForm = (context) => {
46
+ const response = useErrorResponseForForm(context);
47
+ const hydrated = useContextSelectAtom(context.formId, isHydratedAtom);
48
+ return hydrated ? USE_HYDRATED_STATE : response === null || response === void 0 ? void 0 : response.fieldErrors;
49
+ };
50
+ export const useDefaultValuesFromLoader = ({ formId, }) => {
51
+ const matches = useMatches();
52
+ if (typeof formId === "string") {
53
+ const dataKey = formDefaultValuesKey(formId);
54
+ // If multiple loaders declare the same default values,
55
+ // we should use the data from the deepest route.
56
+ const match = matches
57
+ .reverse()
58
+ .find((match) => match.data && dataKey in match.data);
59
+ return match === null || match === void 0 ? void 0 : match.data[dataKey];
60
+ }
61
+ return null;
62
+ };
63
+ export const useDefaultValuesForForm = (context) => {
64
+ const { formId, defaultValuesProp } = context;
65
+ const hydrated = useContextSelectAtom(formId, isHydratedAtom);
66
+ const errorResponse = useErrorResponseForForm(context);
67
+ const defaultValuesFromLoader = useDefaultValuesFromLoader(context);
68
+ // Typical flow is:
69
+ // - Default values only available from props or server
70
+ // - Props have a higher priority than server
71
+ // - State gets hydrated with default values
72
+ // - After submit, we may need to use values from the error
73
+ if (hydrated)
74
+ return USE_HYDRATED_STATE;
75
+ if (errorResponse === null || errorResponse === void 0 ? void 0 : errorResponse.repopulateFields)
76
+ return errorResponse.repopulateFields;
77
+ if (defaultValuesProp)
78
+ return defaultValuesProp;
79
+ return defaultValuesFromLoader;
80
+ };
81
+ export const useHasActiveFormSubmit = ({ fetcher, }) => {
82
+ const transition = useTransition();
83
+ const hasActiveSubmission = fetcher
84
+ ? fetcher.state === "submitting"
85
+ : !!transition.submission;
86
+ return hasActiveSubmission;
87
+ };
88
+ export const useFieldTouched = (name, { formId }) => {
89
+ const atomCreator = useMemo(() => fieldTouchedAtom(name), [name]);
90
+ return useContextSelectAtom(formId, atomCreator);
91
+ };
92
+ export const useFieldError = (name, context) => {
93
+ return useHydratableSelector(context, useMemo(() => fieldErrorAtom(name), [name]), useFieldErrorsForForm(context), (fieldErrors) => fieldErrors === null || fieldErrors === void 0 ? void 0 : fieldErrors[name]);
94
+ };
95
+ export const useFieldDefaultValue = (name, context) => {
96
+ return useHydratableSelector(context, useMemo(() => fieldDefaultValueAtom(name), [name]), useDefaultValuesForForm(context), (val) => lodashGet(val, name));
97
+ };
98
+ export const useFormUpdateAtom = (atom) => useUpdateAtom(atom, ATOM_SCOPE);
99
+ export const useClearError = (context) => {
100
+ const clearError = useFormUpdateAtom(clearErrorAtom);
101
+ return useCallback((name) => {
102
+ clearError({ name, formAtom: formRegistry(context.formId) });
103
+ }, [clearError, context.formId]);
104
+ };
105
+ export const useSetTouched = (context) => {
106
+ const setTouched = useFormUpdateAtom(setTouchedAtom);
107
+ return useCallback((name, touched) => {
108
+ setTouched({ name, formAtom: formRegistry(context.formId), touched });
109
+ }, [setTouched, context.formId]);
110
+ };
@@ -0,0 +1,269 @@
1
+ import { FieldErrors, TouchedFields } from "../validation/types";
2
+ export declare const ATOM_SCOPE: unique symbol;
3
+ export declare type FormState = {
4
+ hydrated: boolean;
5
+ fieldErrors?: FieldErrors;
6
+ isSubmitting: boolean;
7
+ hasBeenSubmitted: boolean;
8
+ touchedFields: TouchedFields;
9
+ formId?: string;
10
+ action?: string;
11
+ subaction?: string;
12
+ defaultValues?: {
13
+ [fieldName: string]: any;
14
+ };
15
+ validateField: (fieldName: string) => Promise<string | null>;
16
+ registerReceiveFocus: (fieldName: string, handler: () => void) => () => void;
17
+ };
18
+ export declare type FormAtom = ReturnType<typeof formRegistry>;
19
+ export declare type FieldState = {
20
+ touched: boolean;
21
+ defaultValue?: any;
22
+ error?: string;
23
+ };
24
+ export declare const formRegistry: {
25
+ (param: string | symbol): import("jotai").WritableAtom<FormState, FormState | ((draft: import("immer/dist/internal").WritableDraft<FormState>) => void), void>;
26
+ remove(param: string | symbol): void;
27
+ setShouldRemove(shouldRemove: ((createdAt: number, param: string | symbol) => boolean) | null): void;
28
+ };
29
+ export declare const fieldErrorAtom: (name: string) => (formAtom: FormAtom) => import("jotai").Atom<string | undefined>;
30
+ export declare const fieldTouchedAtom: (name: string) => (formAtom: FormAtom) => import("jotai").Atom<boolean>;
31
+ export declare const fieldDefaultValueAtom: (name: string) => (formAtom: FormAtom) => import("jotai").Atom<any>;
32
+ export declare const formSelectorAtom: <T>(selector: (state: FormState) => T) => (formAtom: FormAtom) => import("jotai").Atom<T>;
33
+ export declare const fieldErrorsAtom: (formAtom: FormAtom) => import("jotai").Atom<FieldErrors | undefined>;
34
+ export declare const touchedFieldsAtom: (formAtom: FormAtom) => import("jotai").Atom<TouchedFields>;
35
+ export declare const actionAtom: (formAtom: FormAtom) => import("jotai").Atom<string | undefined>;
36
+ export declare const hasBeenSubmittedAtom: (formAtom: FormAtom) => import("jotai").Atom<boolean>;
37
+ export declare const validateFieldAtom: (formAtom: FormAtom) => import("jotai").Atom<(fieldName: string) => Promise<string | null>>;
38
+ export declare const registerReceiveFocusAtom: (formAtom: FormAtom) => import("jotai").Atom<(fieldName: string, handler: () => void) => () => void>;
39
+ export declare const isSubmittingAtom: (formAtom: FormAtom) => import("jotai").Atom<boolean>;
40
+ export declare const defaultValuesAtom: (formAtom: FormAtom) => import("jotai").Atom<{
41
+ [fieldName: string]: any;
42
+ } | undefined>;
43
+ export declare const isValidAtom: (formAtom: FormAtom) => import("jotai").Atom<boolean>;
44
+ export declare const isHydratedAtom: (formAtom: FormAtom) => import("jotai").Atom<boolean>;
45
+ export declare type FieldAtomArgs = {
46
+ name: string;
47
+ formAtom: FormAtom;
48
+ };
49
+ export declare const clearErrorAtom: import("jotai").Atom<null> & {
50
+ write: (get: {
51
+ <Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
52
+ <Value_1>(atom: import("jotai").Atom<Promise<Value_1>>): Value_1;
53
+ <Value_2>(atom: import("jotai").Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
54
+ } & {
55
+ <Value_3>(atom: import("jotai").Atom<Value_3 | Promise<Value_3>>, options: {
56
+ unstable_promise: true;
57
+ }): Value_3 | Promise<Value_3>;
58
+ <Value_4>(atom: import("jotai").Atom<Promise<Value_4>>, options: {
59
+ unstable_promise: true;
60
+ }): Value_4 | Promise<Value_4>;
61
+ <Value_5>(atom: import("jotai").Atom<Value_5>, options: {
62
+ unstable_promise: true;
63
+ }): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
64
+ }, set: {
65
+ <Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
66
+ <Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
67
+ }, update: FieldAtomArgs) => void;
68
+ onMount?: (<S extends (update: FieldAtomArgs) => void>(setAtom: S) => void | (() => void)) | undefined;
69
+ } & {
70
+ init: null;
71
+ };
72
+ export declare const addErrorAtom: import("jotai").Atom<null> & {
73
+ write: (get: {
74
+ <Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
75
+ <Value_1>(atom: import("jotai").Atom<Promise<Value_1>>): Value_1;
76
+ <Value_2>(atom: import("jotai").Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
77
+ } & {
78
+ <Value_3>(atom: import("jotai").Atom<Value_3 | Promise<Value_3>>, options: {
79
+ unstable_promise: true;
80
+ }): Value_3 | Promise<Value_3>;
81
+ <Value_4>(atom: import("jotai").Atom<Promise<Value_4>>, options: {
82
+ unstable_promise: true;
83
+ }): Value_4 | Promise<Value_4>;
84
+ <Value_5>(atom: import("jotai").Atom<Value_5>, options: {
85
+ unstable_promise: true;
86
+ }): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
87
+ }, set: {
88
+ <Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
89
+ <Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
90
+ }, update: FieldAtomArgs & {
91
+ error: string;
92
+ }) => void;
93
+ onMount?: (<S extends (update: FieldAtomArgs & {
94
+ error: string;
95
+ }) => void>(setAtom: S) => void | (() => void)) | undefined;
96
+ } & {
97
+ init: null;
98
+ };
99
+ export declare const setFieldErrorsAtom: import("jotai").Atom<null> & {
100
+ write: (get: {
101
+ <Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
102
+ <Value_1>(atom: import("jotai").Atom<Promise<Value_1>>): Value_1;
103
+ <Value_2>(atom: import("jotai").Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
104
+ } & {
105
+ <Value_3>(atom: import("jotai").Atom<Value_3 | Promise<Value_3>>, options: {
106
+ unstable_promise: true;
107
+ }): Value_3 | Promise<Value_3>;
108
+ <Value_4>(atom: import("jotai").Atom<Promise<Value_4>>, options: {
109
+ unstable_promise: true;
110
+ }): Value_4 | Promise<Value_4>;
111
+ <Value_5>(atom: import("jotai").Atom<Value_5>, options: {
112
+ unstable_promise: true;
113
+ }): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
114
+ }, set: {
115
+ <Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
116
+ <Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
117
+ }, update: {
118
+ fieldErrors: FieldErrors;
119
+ formAtom: FormAtom;
120
+ }) => void;
121
+ onMount?: (<S extends (update: {
122
+ fieldErrors: FieldErrors;
123
+ formAtom: FormAtom;
124
+ }) => void>(setAtom: S) => void | (() => void)) | undefined;
125
+ } & {
126
+ init: null;
127
+ };
128
+ export declare const setTouchedAtom: import("jotai").Atom<null> & {
129
+ write: (get: {
130
+ <Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
131
+ <Value_1>(atom: import("jotai").Atom<Promise<Value_1>>): Value_1;
132
+ <Value_2>(atom: import("jotai").Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
133
+ } & {
134
+ <Value_3>(atom: import("jotai").Atom<Value_3 | Promise<Value_3>>, options: {
135
+ unstable_promise: true;
136
+ }): Value_3 | Promise<Value_3>;
137
+ <Value_4>(atom: import("jotai").Atom<Promise<Value_4>>, options: {
138
+ unstable_promise: true;
139
+ }): Value_4 | Promise<Value_4>;
140
+ <Value_5>(atom: import("jotai").Atom<Value_5>, options: {
141
+ unstable_promise: true;
142
+ }): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
143
+ }, set: {
144
+ <Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
145
+ <Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
146
+ }, update: FieldAtomArgs & {
147
+ touched: boolean;
148
+ }) => void;
149
+ onMount?: (<S extends (update: FieldAtomArgs & {
150
+ touched: boolean;
151
+ }) => void>(setAtom: S) => void | (() => void)) | undefined;
152
+ } & {
153
+ init: null;
154
+ };
155
+ export declare const resetAtom: import("jotai").Atom<null> & {
156
+ write: (get: {
157
+ <Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
158
+ <Value_1>(atom: import("jotai").Atom<Promise<Value_1>>): Value_1;
159
+ <Value_2>(atom: import("jotai").Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
160
+ } & {
161
+ <Value_3>(atom: import("jotai").Atom<Value_3 | Promise<Value_3>>, options: {
162
+ unstable_promise: true;
163
+ }): Value_3 | Promise<Value_3>;
164
+ <Value_4>(atom: import("jotai").Atom<Promise<Value_4>>, options: {
165
+ unstable_promise: true;
166
+ }): Value_4 | Promise<Value_4>;
167
+ <Value_5>(atom: import("jotai").Atom<Value_5>, options: {
168
+ unstable_promise: true;
169
+ }): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
170
+ }, set: {
171
+ <Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
172
+ <Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
173
+ }, update: {
174
+ formAtom: FormAtom;
175
+ }) => void;
176
+ onMount?: (<S extends (update: {
177
+ formAtom: FormAtom;
178
+ }) => void>(setAtom: S) => void | (() => void)) | undefined;
179
+ } & {
180
+ init: null;
181
+ };
182
+ export declare const startSubmitAtom: import("jotai").Atom<null> & {
183
+ write: (get: {
184
+ <Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
185
+ <Value_1>(atom: import("jotai").Atom<Promise<Value_1>>): Value_1;
186
+ <Value_2>(atom: import("jotai").Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
187
+ } & {
188
+ <Value_3>(atom: import("jotai").Atom<Value_3 | Promise<Value_3>>, options: {
189
+ unstable_promise: true;
190
+ }): Value_3 | Promise<Value_3>;
191
+ <Value_4>(atom: import("jotai").Atom<Promise<Value_4>>, options: {
192
+ unstable_promise: true;
193
+ }): Value_4 | Promise<Value_4>;
194
+ <Value_5>(atom: import("jotai").Atom<Value_5>, options: {
195
+ unstable_promise: true;
196
+ }): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
197
+ }, set: {
198
+ <Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
199
+ <Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
200
+ }, update: {
201
+ formAtom: FormAtom;
202
+ }) => void;
203
+ onMount?: (<S extends (update: {
204
+ formAtom: FormAtom;
205
+ }) => void>(setAtom: S) => void | (() => void)) | undefined;
206
+ } & {
207
+ init: null;
208
+ };
209
+ export declare const endSubmitAtom: import("jotai").Atom<null> & {
210
+ write: (get: {
211
+ <Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
212
+ <Value_1>(atom: import("jotai").Atom<Promise<Value_1>>): Value_1;
213
+ <Value_2>(atom: import("jotai").Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
214
+ } & {
215
+ <Value_3>(atom: import("jotai").Atom<Value_3 | Promise<Value_3>>, options: {
216
+ unstable_promise: true;
217
+ }): Value_3 | Promise<Value_3>;
218
+ <Value_4>(atom: import("jotai").Atom<Promise<Value_4>>, options: {
219
+ unstable_promise: true;
220
+ }): Value_4 | Promise<Value_4>;
221
+ <Value_5>(atom: import("jotai").Atom<Value_5>, options: {
222
+ unstable_promise: true;
223
+ }): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
224
+ }, set: {
225
+ <Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
226
+ <Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
227
+ }, update: {
228
+ formAtom: FormAtom;
229
+ }) => void;
230
+ onMount?: (<S extends (update: {
231
+ formAtom: FormAtom;
232
+ }) => void>(setAtom: S) => void | (() => void)) | undefined;
233
+ } & {
234
+ init: null;
235
+ };
236
+ declare type SyncFormContextArgs = {
237
+ defaultValues?: {
238
+ [fieldName: string]: any;
239
+ };
240
+ action?: string;
241
+ subaction?: string;
242
+ validateField: FormState["validateField"];
243
+ registerReceiveFocus: FormState["registerReceiveFocus"];
244
+ formAtom: FormAtom;
245
+ };
246
+ export declare const syncFormContextAtom: import("jotai").Atom<null> & {
247
+ write: (get: {
248
+ <Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
249
+ <Value_1>(atom: import("jotai").Atom<Promise<Value_1>>): Value_1;
250
+ <Value_2>(atom: import("jotai").Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
251
+ } & {
252
+ <Value_3>(atom: import("jotai").Atom<Value_3 | Promise<Value_3>>, options: {
253
+ unstable_promise: true;
254
+ }): Value_3 | Promise<Value_3>;
255
+ <Value_4>(atom: import("jotai").Atom<Promise<Value_4>>, options: {
256
+ unstable_promise: true;
257
+ }): Value_4 | Promise<Value_4>;
258
+ <Value_5>(atom: import("jotai").Atom<Value_5>, options: {
259
+ unstable_promise: true;
260
+ }): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
261
+ }, set: {
262
+ <Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
263
+ <Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
264
+ }, update: SyncFormContextArgs) => void;
265
+ onMount?: (<S extends (update: SyncFormContextArgs) => void>(setAtom: S) => void | (() => void)) | undefined;
266
+ } & {
267
+ init: null;
268
+ };
269
+ export {};
@@ -0,0 +1,82 @@
1
+ import { atom } from "jotai";
2
+ import { atomWithImmer } from "jotai/immer";
3
+ import { atomFamily, selectAtom } from "jotai/utils";
4
+ import lodashGet from "lodash/get";
5
+ export const ATOM_SCOPE = Symbol("remix-validated-form-scope");
6
+ export const formRegistry = atomFamily((formId) => atomWithImmer({
7
+ hydrated: false,
8
+ isSubmitting: false,
9
+ hasBeenSubmitted: false,
10
+ touchedFields: {},
11
+ // The symbol version is just to keep things straight with the `atomFamily`
12
+ formId: typeof formId === "string" ? formId : undefined,
13
+ // Will change upon hydration -- these will never actually be used
14
+ validateField: () => Promise.resolve(null),
15
+ registerReceiveFocus: () => () => { },
16
+ }));
17
+ export const fieldErrorAtom = (name) => (formAtom) => selectAtom(formAtom, (formState) => { var _a; return (_a = formState.fieldErrors) === null || _a === void 0 ? void 0 : _a[name]; });
18
+ export const fieldTouchedAtom = (name) => (formAtom) => selectAtom(formAtom, (formState) => formState.touchedFields[name]);
19
+ export const fieldDefaultValueAtom = (name) => (formAtom) => selectAtom(formAtom, (formState) => formState.defaultValues && lodashGet(formState.defaultValues, name));
20
+ // Selector atoms
21
+ export const formSelectorAtom = (selector) => (formAtom) => selectAtom(formAtom, selector);
22
+ export const fieldErrorsAtom = formSelectorAtom((state) => state.fieldErrors);
23
+ export const touchedFieldsAtom = formSelectorAtom((state) => state.touchedFields);
24
+ export const actionAtom = formSelectorAtom((state) => state.action);
25
+ export const hasBeenSubmittedAtom = formSelectorAtom((state) => state.hasBeenSubmitted);
26
+ export const validateFieldAtom = formSelectorAtom((state) => state.validateField);
27
+ export const registerReceiveFocusAtom = formSelectorAtom((state) => state.registerReceiveFocus);
28
+ export const isSubmittingAtom = formSelectorAtom((state) => state.isSubmitting);
29
+ export const defaultValuesAtom = formSelectorAtom((state) => state.defaultValues);
30
+ export const isValidAtom = formSelectorAtom((state) => { var _a; return Object.keys((_a = state.fieldErrors) !== null && _a !== void 0 ? _a : {}).length === 0; });
31
+ export const isHydratedAtom = formSelectorAtom((state) => state.hydrated);
32
+ export const clearErrorAtom = atom(null, (get, set, { name, formAtom }) => set(formAtom, (state) => {
33
+ var _a;
34
+ (_a = state.fieldErrors) === null || _a === void 0 ? true : delete _a[name];
35
+ return state;
36
+ }));
37
+ export const addErrorAtom = atom(null, (get, set, { name, formAtom, error }) => set(formAtom, (state) => {
38
+ if (!state.fieldErrors)
39
+ state.fieldErrors = {};
40
+ state.fieldErrors[name] = error;
41
+ return state;
42
+ }));
43
+ export const setFieldErrorsAtom = atom(null, (get, set, { formAtom, fieldErrors }) => set(formAtom, (state) => {
44
+ state.fieldErrors = fieldErrors;
45
+ return state;
46
+ }));
47
+ export const setTouchedAtom = atom(null, (get, set, { name, formAtom, touched }) => set(formAtom, (state) => {
48
+ state.touchedFields[name] = touched;
49
+ return state;
50
+ }));
51
+ export const resetAtom = atom(null, (get, set, { formAtom }) => {
52
+ set(formAtom, (state) => {
53
+ state.fieldErrors = {};
54
+ state.touchedFields = {};
55
+ state.hasBeenSubmitted = false;
56
+ return state;
57
+ });
58
+ });
59
+ export const startSubmitAtom = atom(null, (get, set, { formAtom }) => {
60
+ set(formAtom, (state) => {
61
+ state.hasBeenSubmitted = true;
62
+ state.isSubmitting = true;
63
+ return state;
64
+ });
65
+ });
66
+ export const endSubmitAtom = atom(null, (get, set, { formAtom }) => {
67
+ set(formAtom, (state) => {
68
+ state.isSubmitting = false;
69
+ return state;
70
+ });
71
+ });
72
+ export const syncFormContextAtom = atom(null, (get, set, { defaultValues, action, subaction, formAtom, validateField, registerReceiveFocus, }) => {
73
+ set(formAtom, (state) => {
74
+ state.defaultValues = defaultValues;
75
+ state.action = action;
76
+ state.subaction = subaction;
77
+ state.registerReceiveFocus = registerReceiveFocus;
78
+ state.validateField = validateField;
79
+ state.hydrated = true;
80
+ return state;
81
+ });
82
+ });
@@ -1,3 +1,4 @@
1
1
  import type React from "react";
2
2
  export declare const omit: (obj: any, ...keys: string[]) => any;
3
3
  export declare const mergeRefs: <T = any>(refs: (React.MutableRefObject<T> | React.LegacyRef<T> | undefined)[]) => (instance: T | null) => void;
4
+ export declare const useIsomorphicLayoutEffect: typeof React.useEffect;
@@ -1,3 +1,4 @@
1
+ import { useEffect, useLayoutEffect } from "react";
1
2
  export const omit = (obj, ...keys) => {
2
3
  const result = { ...obj };
3
4
  for (const key of keys) {
@@ -17,3 +18,4 @@ export const mergeRefs = (refs) => {
17
18
  });
18
19
  };
19
20
  };
21
+ export const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
File without changes
@@ -0,0 +1 @@
1
+ "use strict";
@@ -1,3 +1,4 @@
1
+ import { FORM_DEFAULTS_FIELD } from "./internal/constants";
1
2
  import { ValidatorError } from "./validation/types";
2
3
  /**
3
4
  * Takes the errors from a `Validator` and returns a `Response`.
@@ -14,3 +15,7 @@ import { ValidatorError } from "./validation/types";
14
15
  * ```
15
16
  */
16
17
  export declare function validationError(error: ValidatorError, repopulateFields?: unknown): Response;
18
+ export declare type FormDefaults = {
19
+ [formDefaultsKey: `${typeof FORM_DEFAULTS_FIELD}_${string}`]: any;
20
+ };
21
+ export declare const setFormDefaults: <DataType = any>(formId: string, defaultValues: Partial<DataType>) => FormDefaults;
package/browser/server.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { json } from "@remix-run/server-runtime";
2
+ import { formDefaultValuesKey, } from "./internal/constants";
2
3
  /**
3
4
  * Takes the errors from a `Validator` and returns a `Response`.
4
5
  * When you return this from your action, `ValidatedForm` on the frontend will automatically
@@ -18,5 +19,9 @@ export function validationError(error, repopulateFields) {
18
19
  fieldErrors: error.fieldErrors,
19
20
  subaction: error.subaction,
20
21
  repopulateFields,
22
+ formId: error.formId,
21
23
  }, { status: 422 });
22
24
  }
25
+ export const setFormDefaults = (formId, defaultValues) => ({
26
+ [formDefaultValuesKey(formId)]: defaultValues,
27
+ });
@@ -0,0 +1,56 @@
1
+ import { FieldErrors, TouchedFields } from "./validation/types";
2
+ export declare type FormContextValue = {
3
+ /**
4
+ * All the errors in all the fields in the form.
5
+ */
6
+ fieldErrors: FieldErrors;
7
+ /**
8
+ * Clear the errors of the specified fields.
9
+ */
10
+ clearError: (...names: string[]) => void;
11
+ /**
12
+ * Validate the specified field.
13
+ */
14
+ validateField: (fieldName: string) => Promise<string | null>;
15
+ /**
16
+ * The `action` prop of the form.
17
+ */
18
+ action?: string;
19
+ /**
20
+ * Whether or not the form is submitting.
21
+ */
22
+ isSubmitting: boolean;
23
+ /**
24
+ * Whether or not a submission has been attempted.
25
+ * This is true once the form has been submitted, even if there were validation errors.
26
+ * Resets to false when the form is reset.
27
+ */
28
+ hasBeenSubmitted: boolean;
29
+ /**
30
+ * Whether or not the form is valid.
31
+ */
32
+ isValid: boolean;
33
+ /**
34
+ * The default values of the form.
35
+ */
36
+ defaultValues?: {
37
+ [fieldName: string]: any;
38
+ };
39
+ /**
40
+ * Register a custom focus handler to be used when
41
+ * the field needs to receive focus due to a validation error.
42
+ */
43
+ registerReceiveFocus: (fieldName: string, handler: () => void) => () => void;
44
+ /**
45
+ * Any fields that have been touched by the user.
46
+ */
47
+ touchedFields: TouchedFields;
48
+ /**
49
+ * Change the touched state of the specified field.
50
+ */
51
+ setFieldTouched: (fieldName: string, touched: boolean) => void;
52
+ };
53
+ /**
54
+ * Provides access to some of the internal state of the form.
55
+ */
56
+ export declare const useFormContext: (formId?: string | undefined) => FormContextValue;
@@ -0,0 +1,40 @@
1
+ import { useCallback } from "react";
2
+ import { useIsSubmitting, useIsValid } from "./hooks";
3
+ import { useClearError, useContextSelectAtom, useDefaultValuesForForm, useFieldErrorsForForm, useHydratableSelector, useInternalFormContext, useSetTouched, } from "./internal/hooks";
4
+ import { actionAtom, defaultValuesAtom, fieldErrorsAtom, hasBeenSubmittedAtom, registerReceiveFocusAtom, touchedFieldsAtom, validateFieldAtom, } from "./internal/state";
5
+ /**
6
+ * Provides access to some of the internal state of the form.
7
+ */
8
+ export const useFormContext = (formId) => {
9
+ // Try to access context so we get our error specific to this hook if it's not there
10
+ const context = useInternalFormContext(formId, "useFormContext");
11
+ const action = useContextSelectAtom(context.formId, actionAtom);
12
+ const isSubmitting = useIsSubmitting(formId);
13
+ const hasBeenSubmitted = useContextSelectAtom(context.formId, hasBeenSubmittedAtom);
14
+ const isValid = useIsValid(formId);
15
+ const defaultValues = useHydratableSelector(context, defaultValuesAtom, useDefaultValuesForForm(context));
16
+ const fieldErrors = useHydratableSelector(context, fieldErrorsAtom, useFieldErrorsForForm(context));
17
+ const setFieldTouched = useSetTouched(context);
18
+ const touchedFields = useContextSelectAtom(context.formId, touchedFieldsAtom);
19
+ const validateField = useContextSelectAtom(context.formId, validateFieldAtom);
20
+ const registerReceiveFocus = useContextSelectAtom(context.formId, registerReceiveFocusAtom);
21
+ const internalClearError = useClearError(context);
22
+ const clearError = useCallback((...names) => {
23
+ names.forEach((name) => {
24
+ internalClearError(name);
25
+ });
26
+ }, [internalClearError]);
27
+ return {
28
+ isSubmitting,
29
+ hasBeenSubmitted,
30
+ isValid,
31
+ defaultValues,
32
+ clearError,
33
+ fieldErrors: fieldErrors !== null && fieldErrors !== void 0 ? fieldErrors : {},
34
+ action,
35
+ setFieldTouched,
36
+ touchedFields,
37
+ validateField,
38
+ registerReceiveFocus,
39
+ };
40
+ };