remix-validated-form 4.1.3 → 4.1.5

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 (52) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/browser/ValidatedForm.js +31 -36
  3. package/browser/hooks.js +13 -16
  4. package/browser/internal/customState.d.ts +105 -0
  5. package/browser/internal/customState.js +46 -0
  6. package/browser/internal/getInputProps.js +4 -14
  7. package/browser/internal/hooks.d.ts +14 -15
  8. package/browser/internal/hooks.js +36 -38
  9. package/browser/internal/logic/elementUtils.d.ts +3 -0
  10. package/browser/internal/logic/elementUtils.js +3 -0
  11. package/browser/internal/logic/setInputValueInForm.js +9 -52
  12. package/browser/internal/setFieldValue.d.ts +0 -0
  13. package/browser/internal/setFieldValue.js +0 -0
  14. package/browser/internal/setFormValues.d.ts +0 -0
  15. package/browser/internal/setFormValues.js +0 -0
  16. package/browser/internal/state.d.ts +339 -238
  17. package/browser/internal/state.js +59 -72
  18. package/browser/internal/watch.d.ts +18 -0
  19. package/browser/internal/watch.js +122 -0
  20. package/browser/unreleased/formStateHooks.d.ts +39 -0
  21. package/browser/unreleased/formStateHooks.js +54 -0
  22. package/browser/userFacingFormContext.js +9 -23
  23. package/build/ValidatedForm.js +30 -35
  24. package/build/hooks.js +11 -14
  25. package/build/internal/getInputProps.js +4 -14
  26. package/build/internal/hooks.d.ts +14 -15
  27. package/build/internal/hooks.js +38 -40
  28. package/build/internal/logic/elementUtils.d.ts +3 -0
  29. package/build/internal/logic/elementUtils.js +9 -0
  30. package/build/internal/logic/setInputValueInForm.js +12 -55
  31. package/build/internal/setFormValues.d.ts +0 -0
  32. package/build/internal/setFormValues.js +0 -0
  33. package/build/internal/state/controlledFields.js +11 -2
  34. package/build/internal/state.d.ts +339 -238
  35. package/build/internal/state.js +61 -77
  36. package/build/internal/watch.d.ts +20 -0
  37. package/build/internal/watch.js +126 -0
  38. package/build/unreleased/formStateHooks.d.ts +39 -0
  39. package/build/unreleased/formStateHooks.js +59 -0
  40. package/build/userFacingFormContext.js +9 -23
  41. package/package.json +3 -4
  42. package/src/ValidatedForm.tsx +48 -52
  43. package/src/hooks.ts +15 -26
  44. package/src/internal/getInputProps.ts +4 -14
  45. package/src/internal/hooks.ts +59 -71
  46. package/src/internal/hydratable.ts +28 -0
  47. package/src/internal/logic/getCheckboxChecked.ts +10 -0
  48. package/src/internal/logic/getRadioChecked.ts +7 -0
  49. package/src/internal/state/atomUtils.ts +13 -0
  50. package/src/internal/state.ts +99 -177
  51. package/src/unreleased/formStateHooks.ts +113 -0
  52. package/src/userFacingFormContext.ts +14 -53
@@ -25,15 +25,16 @@ import {
25
25
  } from "./internal/hooks";
26
26
  import { MultiValueMap, useMultiValueMap } from "./internal/MultiValueMap";
27
27
  import {
28
- addErrorAtom,
29
- clearErrorAtom,
28
+ cleanupFormState,
30
29
  endSubmitAtom,
31
- formRegistry,
32
- FormState,
30
+ fieldErrorsAtom,
31
+ formElementAtom,
32
+ formPropsAtom,
33
+ isHydratedAtom,
33
34
  resetAtom,
34
- setFieldErrorsAtom,
35
+ setFieldErrorAtom,
35
36
  startSubmitAtom,
36
- syncFormContextAtom,
37
+ SyncedFormProps,
37
38
  } from "./internal/state";
38
39
  import { useSubmitComplete } from "./internal/submissionCallbacks";
39
40
  import {
@@ -192,12 +193,6 @@ function formEventProxy<T extends object>(event: T): T {
192
193
  }) as T;
193
194
  }
194
195
 
195
- const useFormAtom = (formId: string | symbol) => {
196
- const formAtom = formRegistry(formId);
197
- useEffect(() => () => formRegistry.remove(formId), [formId]);
198
- return formAtom;
199
- };
200
-
201
196
  /**
202
197
  * The primary form component of `remix-validated-form`.
203
198
  */
@@ -219,7 +214,6 @@ export function ValidatedForm<DataType>({
219
214
  ...rest
220
215
  }: FormProps<DataType>) {
221
216
  const formId = useFormId(id);
222
- const formAtom = useFormAtom(formId);
223
217
  const providedDefaultValues = useDeepEqualsMemo(unMemoizedDefaults);
224
218
  const contextValue = useMemo<InternalFormContextValue>(
225
219
  () => ({
@@ -238,73 +232,75 @@ export function ValidatedForm<DataType>({
238
232
  const Form = fetcher?.Form ?? RemixForm;
239
233
 
240
234
  const submit = useSubmit();
241
- const clearError = useFormUpdateAtom(clearErrorAtom);
242
- const addError = useFormUpdateAtom(addErrorAtom);
243
- const setFieldErrors = useFormUpdateAtom(setFieldErrorsAtom);
244
- const reset = useFormUpdateAtom(resetAtom);
245
- const startSubmit = useFormUpdateAtom(startSubmitAtom);
246
- const endSubmit = useFormUpdateAtom(endSubmitAtom);
247
- const syncFormContext = useFormUpdateAtom(syncFormContextAtom);
235
+ const setFieldErrors = useFormUpdateAtom(fieldErrorsAtom(formId));
236
+ const setFieldError = useFormUpdateAtom(setFieldErrorAtom(formId));
237
+ const reset = useFormUpdateAtom(resetAtom(formId));
238
+ const startSubmit = useFormUpdateAtom(startSubmitAtom(formId));
239
+ const endSubmit = useFormUpdateAtom(endSubmitAtom(formId));
240
+ const syncFormProps = useFormUpdateAtom(formPropsAtom(formId));
241
+ const setHydrated = useFormUpdateAtom(isHydratedAtom(formId));
242
+ const setFormElementInState = useFormUpdateAtom(formElementAtom(formId));
248
243
 
249
- const validateField: FormState["validateField"] = useCallback(
250
- async (fieldName) => {
244
+ useEffect(() => {
245
+ setHydrated(true);
246
+ return () => cleanupFormState(formId);
247
+ }, [formId, setHydrated]);
248
+
249
+ const validateField: SyncedFormProps["validateField"] = useCallback(
250
+ async (field) => {
251
251
  invariant(formRef.current, "Cannot find reference to form");
252
252
  const { error } = await validator.validateField(
253
253
  getDataFromForm(formRef.current),
254
- fieldName as any
254
+ field
255
255
  );
256
256
 
257
257
  if (error) {
258
- addError({ formAtom, name: fieldName, error });
258
+ setFieldError({ field, error });
259
259
  return error;
260
260
  } else {
261
- clearError({ name: fieldName, formAtom });
261
+ setFieldError({ field, error: undefined });
262
262
  return null;
263
263
  }
264
264
  },
265
- [addError, clearError, formAtom, validator]
265
+ [setFieldError, validator]
266
266
  );
267
267
 
268
268
  const customFocusHandlers = useMultiValueMap<string, () => void>();
269
- const registerReceiveFocus: FormState["registerReceiveFocus"] = useCallback(
270
- (fieldName, handler) => {
271
- customFocusHandlers().add(fieldName, handler);
272
- return () => {
273
- customFocusHandlers().remove(fieldName, handler);
274
- };
275
- },
276
- [customFocusHandlers]
277
- );
269
+ const registerReceiveFocus: SyncedFormProps["registerReceiveFocus"] =
270
+ useCallback(
271
+ (fieldName, handler) => {
272
+ customFocusHandlers().add(fieldName, handler);
273
+ return () => {
274
+ customFocusHandlers().remove(fieldName, handler);
275
+ };
276
+ },
277
+ [customFocusHandlers]
278
+ );
278
279
 
279
280
  useLayoutEffect(() => {
280
- syncFormContext({
281
- formAtom,
281
+ syncFormProps({
282
282
  action,
283
- defaultValues: providedDefaultValues ?? backendDefaultValues,
283
+ defaultValues: providedDefaultValues ?? backendDefaultValues ?? {},
284
284
  subaction,
285
285
  validateField,
286
286
  registerReceiveFocus,
287
287
  });
288
288
  }, [
289
289
  action,
290
- formAtom,
291
290
  providedDefaultValues,
292
291
  registerReceiveFocus,
293
292
  subaction,
294
- syncFormContext,
293
+ syncFormProps,
295
294
  validateField,
296
295
  backendDefaultValues,
297
296
  ]);
298
297
 
299
298
  useEffect(() => {
300
- setFieldErrors({
301
- fieldErrors: backendError?.fieldErrors ?? {},
302
- formAtom,
303
- });
304
- }, [backendError?.fieldErrors, formAtom, setFieldErrors]);
299
+ setFieldErrors(backendError?.fieldErrors ?? {});
300
+ }, [backendError?.fieldErrors, setFieldErrors, setFieldError]);
305
301
 
306
302
  useSubmitComplete(hasActiveSubmission, () => {
307
- endSubmit({ formAtom });
303
+ endSubmit();
308
304
  });
309
305
 
310
306
  let clickedButtonRef = React.useRef<any>();
@@ -334,11 +330,11 @@ export function ValidatedForm<DataType>({
334
330
  }, []);
335
331
 
336
332
  const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
337
- startSubmit({ formAtom });
333
+ startSubmit();
338
334
  const result = await validator.validate(getDataFromForm(e.currentTarget));
339
335
  if (result.error) {
340
- endSubmit({ formAtom });
341
- setFieldErrors({ fieldErrors: result.error.fieldErrors, formAtom });
336
+ endSubmit();
337
+ setFieldErrors(result.error.fieldErrors);
342
338
  if (!disableFocusOnError) {
343
339
  focusFirstInvalidInput(
344
340
  result.error.fieldErrors,
@@ -350,7 +346,7 @@ export function ValidatedForm<DataType>({
350
346
  const eventProxy = formEventProxy(e);
351
347
  await onSubmit?.(result.data, eventProxy);
352
348
  if (eventProxy.defaultPrevented) {
353
- endSubmit({ formAtom });
349
+ endSubmit();
354
350
  return;
355
351
  }
356
352
 
@@ -372,7 +368,7 @@ export function ValidatedForm<DataType>({
372
368
 
373
369
  return (
374
370
  <Form
375
- ref={mergeRefs([formRef, formRefProp])}
371
+ ref={mergeRefs([formRef, formRefProp, setFormElementInState])}
376
372
  {...rest}
377
373
  id={id}
378
374
  action={action}
@@ -385,7 +381,7 @@ export function ValidatedForm<DataType>({
385
381
  onReset={(event) => {
386
382
  onReset?.(event);
387
383
  if (event.defaultPrevented) return;
388
- reset({ formAtom });
384
+ reset();
389
385
  }}
390
386
  >
391
387
  <InternalFormContext.Provider value={contextValue}>
package/src/hooks.ts CHANGED
@@ -8,17 +8,14 @@ import {
8
8
  useInternalFormContext,
9
9
  useFieldTouched,
10
10
  useFieldError,
11
+ useFormAtomValue,
11
12
  useFieldDefaultValue,
12
- useContextSelectAtom,
13
- useClearError,
14
- useSetTouched,
15
13
  } from "./internal/hooks";
16
14
  import {
15
+ formPropsAtom,
17
16
  hasBeenSubmittedAtom,
18
17
  isSubmittingAtom,
19
18
  isValidAtom,
20
- registerReceiveFocusAtom,
21
- validateFieldAtom,
22
19
  } from "./internal/state";
23
20
 
24
21
  /**
@@ -30,7 +27,7 @@ import {
30
27
  */
31
28
  export const useIsSubmitting = (formId?: string) => {
32
29
  const formContext = useInternalFormContext(formId, "useIsSubmitting");
33
- return useContextSelectAtom(formContext.formId, isSubmittingAtom);
30
+ return useFormAtomValue(isSubmittingAtom(formContext.formId));
34
31
  };
35
32
 
36
33
  /**
@@ -40,7 +37,7 @@ export const useIsSubmitting = (formId?: string) => {
40
37
  */
41
38
  export const useIsValid = (formId?: string) => {
42
39
  const formContext = useInternalFormContext(formId, "useIsValid");
43
- return useContextSelectAtom(formContext.formId, isValidAtom);
40
+ return useFormAtomValue(isValidAtom(formContext.formId));
44
41
  };
45
42
 
46
43
  export type FieldProps = {
@@ -97,26 +94,18 @@ export const useField = (
97
94
  formId?: string;
98
95
  }
99
96
  ): FieldProps => {
100
- const { handleReceiveFocus, formId: providedFormId } = options ?? {};
97
+ const { formId: providedFormId, handleReceiveFocus } = options ?? {};
101
98
  const formContext = useInternalFormContext(providedFormId, "useField");
102
99
 
103
100
  const defaultValue = useFieldDefaultValue(name, formContext);
104
- const touched = useFieldTouched(name, formContext);
105
- const error = useFieldError(name, formContext);
101
+ const [touched, setTouched] = useFieldTouched(name, formContext);
102
+ const [error, setError] = useFieldError(name, formContext);
106
103
 
107
- const clearError = useClearError(formContext);
108
- const setTouched = useSetTouched(formContext);
109
- const hasBeenSubmitted = useContextSelectAtom(
110
- formContext.formId,
111
- hasBeenSubmittedAtom
104
+ const hasBeenSubmitted = useFormAtomValue(
105
+ hasBeenSubmittedAtom(formContext.formId)
112
106
  );
113
- const validateField = useContextSelectAtom(
114
- formContext.formId,
115
- validateFieldAtom
116
- );
117
- const registerReceiveFocus = useContextSelectAtom(
118
- formContext.formId,
119
- registerReceiveFocusAtom
107
+ const { validateField, registerReceiveFocus } = useFormAtomValue(
108
+ formPropsAtom(formContext.formId)
120
109
  );
121
110
 
122
111
  useEffect(() => {
@@ -127,13 +116,13 @@ export const useField = (
127
116
  const field = useMemo<FieldProps>(() => {
128
117
  const helpers = {
129
118
  error,
130
- clearError: () => clearError(name),
119
+ clearError: () => setError(undefined),
131
120
  validate: () => {
132
121
  validateField(name);
133
122
  },
134
123
  defaultValue,
135
124
  touched,
136
- setTouched: (touched: boolean) => setTouched(name, touched),
125
+ setTouched,
137
126
  };
138
127
  const getInputProps = createGetInputProps({
139
128
  ...helpers,
@@ -149,12 +138,12 @@ export const useField = (
149
138
  error,
150
139
  defaultValue,
151
140
  touched,
141
+ setTouched,
152
142
  name,
153
143
  hasBeenSubmitted,
154
144
  options?.validationBehavior,
155
- clearError,
145
+ setError,
156
146
  validateField,
157
- setTouched,
158
147
  ]);
159
148
 
160
149
  return field;
@@ -1,4 +1,6 @@
1
1
  import omitBy from "lodash/omitBy";
2
+ import { getCheckboxChecked } from "./logic/getCheckboxChecked";
3
+ import { getRadioChecked } from "./logic/getRadioChecked";
2
4
 
3
5
  export type ValidationBehavior = "onBlur" | "onChange" | "onSubmit";
4
6
 
@@ -41,13 +43,6 @@ const defaultValidationBehavior: ValidationBehaviorOptions = {
41
43
  whenSubmitted: "onChange",
42
44
  };
43
45
 
44
- const getCheckboxDefaultChecked = (value: string, defaultValue: any) => {
45
- if (Array.isArray(defaultValue)) return defaultValue.includes(value);
46
- if (typeof defaultValue === "boolean") return defaultValue;
47
- if (typeof defaultValue === "string") return defaultValue === value;
48
- return undefined;
49
- };
50
-
51
46
  export const createGetInputProps = ({
52
47
  clearError,
53
48
  validate,
@@ -86,14 +81,9 @@ export const createGetInputProps = ({
86
81
  };
87
82
 
88
83
  if (props.type === "checkbox") {
89
- const value = props.value ?? "on";
90
- inputProps.defaultChecked = getCheckboxDefaultChecked(
91
- value,
92
- defaultValue
93
- );
84
+ inputProps.defaultChecked = getCheckboxChecked(props.value, defaultValue);
94
85
  } else if (props.type === "radio") {
95
- const value = props.value ?? "on";
96
- inputProps.defaultChecked = defaultValue === value;
86
+ inputProps.defaultChecked = getRadioChecked(props.value, defaultValue);
97
87
  } else {
98
88
  inputProps.defaultValue = defaultValue;
99
89
  }
@@ -1,26 +1,32 @@
1
1
  import { useActionData, useMatches, useTransition } from "@remix-run/react";
2
- import { Atom } from "jotai";
2
+ import { Atom, useAtom, WritableAtom } from "jotai";
3
3
  import { useAtomValue, useUpdateAtom } from "jotai/utils";
4
4
  import lodashGet from "lodash/get";
5
- import identity from "lodash/identity";
6
- import { useCallback, useContext, useMemo } from "react";
7
- import { ValidationErrorResponseData } from "..";
5
+ import { useCallback, useContext } from "react";
6
+ import invariant from "tiny-invariant";
7
+ import { FieldErrors, ValidationErrorResponseData } from "..";
8
8
  import { formDefaultValuesKey } from "./constants";
9
9
  import { InternalFormContext, InternalFormContextValue } from "./formContext";
10
+ import { Hydratable, hydratable } from "./hydratable";
10
11
  import {
11
12
  ATOM_SCOPE,
12
- clearErrorAtom,
13
- fieldDefaultValueAtom,
14
13
  fieldErrorAtom,
15
14
  fieldTouchedAtom,
16
- FormAtom,
17
- formRegistry,
15
+ formPropsAtom,
18
16
  isHydratedAtom,
17
+ setFieldErrorAtom,
19
18
  setTouchedAtom,
20
19
  } from "./state";
21
20
 
22
- type FormSelectorAtomCreator<T> = (formState: FormAtom) => Atom<T>;
23
- const USE_HYDRATED_STATE = Symbol("USE_HYDRATED_STATE");
21
+ export const useFormUpdateAtom: typeof useUpdateAtom = (atom) =>
22
+ useUpdateAtom(atom, ATOM_SCOPE);
23
+
24
+ export const useFormAtom = <Value, Update, Result extends void | Promise<void>>(
25
+ anAtom: WritableAtom<Value, Update, Result>
26
+ ) => useAtom(anAtom, ATOM_SCOPE);
27
+
28
+ export const useFormAtomValue = <Value>(anAtom: Atom<Value>) =>
29
+ useAtomValue(anAtom, ATOM_SCOPE);
24
30
 
25
31
  export const useInternalFormContext = (
26
32
  formId?: string | symbol,
@@ -36,28 +42,6 @@ export const useInternalFormContext = (
36
42
  );
37
43
  };
38
44
 
39
- export const useContextSelectAtom = <T>(
40
- formId: string | symbol,
41
- selectorAtomCreator: FormSelectorAtomCreator<T>
42
- ) => {
43
- const formAtom = formRegistry(formId);
44
- const selectorAtom = useMemo(
45
- () => selectorAtomCreator(formAtom),
46
- [formAtom, selectorAtomCreator]
47
- );
48
- return useAtomValue(selectorAtom, ATOM_SCOPE);
49
- };
50
-
51
- export const useHydratableSelector = <T, U>(
52
- { formId }: InternalFormContextValue,
53
- atomCreator: FormSelectorAtomCreator<T>,
54
- dataToUse: U | typeof USE_HYDRATED_STATE,
55
- selector: (data: U) => T = identity
56
- ) => {
57
- const dataFromState = useContextSelectAtom(formId, atomCreator);
58
- return dataToUse === USE_HYDRATED_STATE ? dataFromState : selector(dataToUse);
59
- };
60
-
61
45
  export function useErrorResponseForForm({
62
46
  fetcher,
63
47
  subaction,
@@ -84,10 +68,12 @@ export function useErrorResponseForForm({
84
68
  return null;
85
69
  }
86
70
 
87
- export const useFieldErrorsForForm = (context: InternalFormContextValue) => {
71
+ export const useFieldErrorsForForm = (
72
+ context: InternalFormContextValue
73
+ ): Hydratable<FieldErrors | undefined> => {
88
74
  const response = useErrorResponseForForm(context);
89
- const hydrated = useContextSelectAtom(context.formId, isHydratedAtom);
90
- return hydrated ? USE_HYDRATED_STATE : response?.fieldErrors;
75
+ const hydrated = useFormAtomValue(isHydratedAtom(context.formId));
76
+ return hydratable.from(response?.fieldErrors, hydrated);
91
77
  };
92
78
 
93
79
  export const useDefaultValuesFromLoader = ({
@@ -107,9 +93,11 @@ export const useDefaultValuesFromLoader = ({
107
93
  return null;
108
94
  };
109
95
 
110
- export const useDefaultValuesForForm = (context: InternalFormContextValue) => {
96
+ export const useDefaultValuesForForm = (
97
+ context: InternalFormContextValue
98
+ ): Hydratable<{ [fieldName: string]: any }> => {
111
99
  const { formId, defaultValuesProp } = context;
112
- const hydrated = useContextSelectAtom(formId, isHydratedAtom);
100
+ const hydrated = useFormAtomValue(isHydratedAtom(formId));
113
101
  const errorResponse = useErrorResponseForForm(context);
114
102
  const defaultValuesFromLoader = useDefaultValuesFromLoader(context);
115
103
 
@@ -119,10 +107,17 @@ export const useDefaultValuesForForm = (context: InternalFormContextValue) => {
119
107
  // - State gets hydrated with default values
120
108
  // - After submit, we may need to use values from the error
121
109
 
122
- if (hydrated) return USE_HYDRATED_STATE;
123
- if (errorResponse?.repopulateFields) return errorResponse.repopulateFields;
124
- if (defaultValuesProp) return defaultValuesProp;
125
- return defaultValuesFromLoader;
110
+ if (hydrated) return hydratable.hydratedData();
111
+ if (errorResponse?.repopulateFields) {
112
+ invariant(
113
+ typeof errorResponse.repopulateFields === "object",
114
+ "repopulateFields returned something other than an object"
115
+ );
116
+ return hydratable.serverData(errorResponse.repopulateFields);
117
+ }
118
+ if (defaultValuesProp) return hydratable.serverData(defaultValuesProp);
119
+
120
+ return hydratable.serverData(defaultValuesFromLoader);
126
121
  };
127
122
 
128
123
  export const useHasActiveFormSubmit = ({
@@ -136,56 +131,49 @@ export const useHasActiveFormSubmit = ({
136
131
  };
137
132
 
138
133
  export const useFieldTouched = (
139
- name: string,
134
+ field: string,
140
135
  { formId }: InternalFormContextValue
141
- ) => {
142
- const atomCreator = useMemo(() => fieldTouchedAtom(name), [name]);
143
- return useContextSelectAtom(formId, atomCreator);
144
- };
136
+ ) => useFormAtom(fieldTouchedAtom({ formId, field }));
145
137
 
146
138
  export const useFieldError = (
147
139
  name: string,
148
140
  context: InternalFormContextValue
149
141
  ) => {
150
- return useHydratableSelector(
151
- context,
152
- useMemo(() => fieldErrorAtom(name), [name]),
153
- useFieldErrorsForForm(context),
154
- (fieldErrors) => fieldErrors?.[name]
142
+ const fieldErrors = useFieldErrorsForForm(context);
143
+ const [state, set] = useFormAtom(
144
+ fieldErrorAtom({ formId: context.formId, field: name })
155
145
  );
146
+ return [
147
+ fieldErrors.map((fieldErrors) => fieldErrors?.[name]).hydrateTo(state),
148
+ set,
149
+ ] as const;
156
150
  };
157
151
 
158
152
  export const useFieldDefaultValue = (
159
153
  name: string,
160
154
  context: InternalFormContextValue
161
155
  ) => {
162
- return useHydratableSelector(
163
- context,
164
- useMemo(() => fieldDefaultValueAtom(name), [name]),
165
- useDefaultValuesForForm(context),
166
- (val) => lodashGet(val, name)
156
+ const defaultValues = useDefaultValuesForForm(context);
157
+ const { defaultValues: state } = useFormAtomValue(
158
+ formPropsAtom(context.formId)
167
159
  );
160
+ return defaultValues
161
+ .map((val) => lodashGet(val, name))
162
+ .hydrateTo(state[name]);
168
163
  };
169
164
 
170
- export const useFormUpdateAtom: typeof useUpdateAtom = (atom) =>
171
- useUpdateAtom(atom, ATOM_SCOPE);
172
-
173
- export const useClearError = (context: InternalFormContextValue) => {
174
- const clearError = useFormUpdateAtom(clearErrorAtom);
165
+ export const useClearError = ({ formId }: InternalFormContextValue) => {
166
+ const updateError = useFormUpdateAtom(setFieldErrorAtom(formId));
175
167
  return useCallback(
176
- (name: string) => {
177
- clearError({ name, formAtom: formRegistry(context.formId) });
178
- },
179
- [clearError, context.formId]
168
+ (name: string) => updateError({ field: name, error: undefined }),
169
+ [updateError]
180
170
  );
181
171
  };
182
172
 
183
- export const useSetTouched = (context: InternalFormContextValue) => {
184
- const setTouched = useFormUpdateAtom(setTouchedAtom);
173
+ export const useSetTouched = ({ formId }: InternalFormContextValue) => {
174
+ const setTouched = useFormUpdateAtom(setTouchedAtom(formId));
185
175
  return useCallback(
186
- (name: string, touched: boolean) => {
187
- setTouched({ name, formAtom: formRegistry(context.formId), touched });
188
- },
189
- [setTouched, context.formId]
176
+ (name: string, touched: boolean) => setTouched({ field: name, touched }),
177
+ [setTouched]
190
178
  );
191
179
  };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The purpose of this type is to simplify the logic
3
+ * around data that needs to come from the server initially,
4
+ * but from the internal state after hydration.
5
+ */
6
+ export type Hydratable<T> = {
7
+ hydrateTo: (data: T) => T;
8
+ map: <U>(fn: (data: T) => U) => Hydratable<U>;
9
+ };
10
+
11
+ const serverData = <T>(data: T): Hydratable<T> => ({
12
+ hydrateTo: () => data,
13
+ map: (fn) => serverData(fn(data)),
14
+ });
15
+
16
+ const hydratedData = <T>(): Hydratable<T> => ({
17
+ hydrateTo: (hydratedData: T) => hydratedData,
18
+ map: <U>() => hydratedData<U>(),
19
+ });
20
+
21
+ const from = <T>(data: T, hydrated: boolean): Hydratable<T> =>
22
+ hydrated ? hydratedData<T>() : serverData<T>(data);
23
+
24
+ export const hydratable = {
25
+ serverData,
26
+ hydratedData,
27
+ from,
28
+ };
@@ -0,0 +1,10 @@
1
+ export const getCheckboxChecked = (
2
+ checkboxValue: string | undefined = "on",
3
+ newValue: unknown
4
+ ): boolean | undefined => {
5
+ if (Array.isArray(newValue))
6
+ return newValue.some((val) => val === true || val === checkboxValue);
7
+ if (typeof newValue === "boolean") return newValue;
8
+ if (typeof newValue === "string") return newValue === checkboxValue;
9
+ return undefined;
10
+ };
@@ -0,0 +1,7 @@
1
+ export const getRadioChecked = (
2
+ radioValue: string | undefined = "on",
3
+ newValue: unknown
4
+ ) => {
5
+ if (typeof newValue === "string") return newValue === radioValue;
6
+ return undefined;
7
+ };
@@ -0,0 +1,13 @@
1
+ import { Atom, atom } from "jotai";
2
+ import { atomFamily } from "jotai/utils";
3
+ import isEqual from "lodash/isEqual";
4
+
5
+ export type InternalFormId = string | symbol;
6
+
7
+ export const formAtomFamily = <T>(data: T) =>
8
+ atomFamily((_: InternalFormId) => atom(data));
9
+
10
+ export type FieldAtomKey = { formId: InternalFormId; field: string };
11
+ export const fieldAtomFamily = <T extends Atom<unknown>>(
12
+ func: (key: FieldAtomKey) => T
13
+ ) => atomFamily(func, isEqual);