remix-validated-form 4.1.1 → 4.1.4
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.
- package/.turbo/turbo-build.log +2 -2
- package/browser/ValidatedForm.js +9 -4
- package/browser/internal/hooks-valtio.d.ts +18 -0
- package/browser/internal/hooks-valtio.js +110 -0
- package/browser/internal/hooks-zustand.d.ts +16 -0
- package/browser/internal/hooks-zustand.js +100 -0
- package/browser/internal/hydratable.d.ts +14 -0
- package/browser/internal/hydratable.js +14 -0
- package/browser/internal/immerMiddleware.d.ts +6 -0
- package/browser/internal/immerMiddleware.js +7 -0
- package/browser/internal/logic/getCheckboxChecked.js +1 -1
- package/browser/internal/logic/setInputValueInForm.d.ts +1 -0
- package/browser/internal/logic/setInputValueInForm.js +120 -0
- package/browser/internal/setFieldValue.d.ts +20 -0
- package/browser/internal/setFieldValue.js +83 -0
- package/browser/internal/setFormValues.d.ts +2 -0
- package/browser/internal/setFormValues.js +26 -0
- package/browser/internal/state/atomUtils.d.ts +38 -0
- package/browser/internal/state/atomUtils.js +5 -0
- package/browser/internal/state/controlledFields.d.ts +66 -0
- package/browser/internal/state/controlledFields.js +93 -0
- package/browser/internal/state/setFieldValue.d.ts +0 -0
- package/browser/internal/state/setFieldValue.js +1 -0
- package/browser/internal/state-valtio.d.ts +62 -0
- package/browser/internal/state-valtio.js +69 -0
- package/browser/internal/state-zustand.d.ts +47 -0
- package/browser/internal/state-zustand.js +85 -0
- package/browser/internal/util.js +1 -1
- package/build/ValidatedForm.js +9 -4
- package/build/internal/hooks-valtio.d.ts +18 -0
- package/build/internal/hooks-valtio.js +128 -0
- package/build/internal/hooks-zustand.d.ts +16 -0
- package/build/internal/hooks-zustand.js +117 -0
- package/build/internal/hydratable.d.ts +14 -0
- package/build/internal/hydratable.js +17 -0
- package/build/internal/immerMiddleware.d.ts +6 -0
- package/build/internal/immerMiddleware.js +14 -0
- package/build/internal/logic/getCheckboxChecked.js +1 -1
- package/build/internal/logic/setFieldValue.js +3 -3
- package/build/internal/logic/setInputValueInForm.d.ts +1 -0
- package/build/internal/logic/setInputValueInForm.js +127 -0
- package/build/internal/setFormValues.d.ts +2 -0
- package/build/internal/setFormValues.js +33 -0
- package/build/internal/state/atomUtils.d.ts +38 -0
- package/build/internal/state/atomUtils.js +13 -0
- package/build/internal/state/controlledFields.d.ts +66 -0
- package/build/internal/state/controlledFields.js +95 -0
- package/build/internal/state-valtio.d.ts +62 -0
- package/build/internal/state-valtio.js +83 -0
- package/build/internal/state-zustand.d.ts +47 -0
- package/build/internal/state-zustand.js +91 -0
- package/build/internal/util.js +5 -2
- package/package.json +5 -5
- package/src/ValidatedForm.tsx +10 -4
- package/src/internal/util.ts +1 -1
package/.turbo/turbo-build.log
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
[2K[1G[2m$ npm run build:browser && npm run build:main[22m
|
2
2
|
|
3
|
-
> remix-validated-form@4.1.
|
3
|
+
> remix-validated-form@4.1.2 build:browser
|
4
4
|
> tsc --module ESNext --outDir ./browser
|
5
5
|
|
6
6
|
|
7
|
-
> remix-validated-form@4.1.
|
7
|
+
> remix-validated-form@4.1.2 build:main
|
8
8
|
> tsc --module CommonJS --outDir ./build
|
9
9
|
|
package/browser/ValidatedForm.js
CHANGED
@@ -187,9 +187,9 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
|
|
187
187
|
clickedButtonRef.current = submitButton;
|
188
188
|
}
|
189
189
|
}
|
190
|
-
window.addEventListener("click", handleClick);
|
190
|
+
window.addEventListener("click", handleClick, { capture: true });
|
191
191
|
return () => {
|
192
|
-
window.removeEventListener("click", handleClick);
|
192
|
+
window.removeEventListener("click", handleClick, { capture: true });
|
193
193
|
};
|
194
194
|
}, []);
|
195
195
|
const handleSubmit = async (e) => {
|
@@ -209,10 +209,15 @@ export function ValidatedForm({ validator, onSubmit, children, fetcher, action,
|
|
209
209
|
endSubmit({ formAtom });
|
210
210
|
return;
|
211
211
|
}
|
212
|
+
// We deviate from the remix code here a bit because of our async submit.
|
213
|
+
// In remix's `FormImpl`, they use `event.currentTarget` to get the form,
|
214
|
+
// but we already have the form in `formRef.current` so we can just use that.
|
215
|
+
// If we use `event.currentTarget` here, it will break because `currentTarget`
|
216
|
+
// will have changed since the start of the submission.
|
212
217
|
if (fetcher)
|
213
|
-
fetcher.submit(clickedButtonRef.current ||
|
218
|
+
fetcher.submit(clickedButtonRef.current || formRef.current);
|
214
219
|
else
|
215
|
-
submit(clickedButtonRef.current ||
|
220
|
+
submit(clickedButtonRef.current || formRef.current, {
|
216
221
|
method,
|
217
222
|
replace,
|
218
223
|
});
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import { useUpdateAtom } from "jotai/utils";
|
2
|
+
import { FieldErrors, ValidationErrorResponseData } from "..";
|
3
|
+
import { InternalFormContextValue } from "./formContext";
|
4
|
+
import { Hydratable } from "./hydratable";
|
5
|
+
export declare const useInternalFormContext: (formId?: string | symbol | undefined, hookName?: string | undefined) => InternalFormContextValue;
|
6
|
+
export declare function useErrorResponseForForm({ fetcher, subaction, formId, }: InternalFormContextValue): ValidationErrorResponseData | null;
|
7
|
+
export declare const useFieldErrorsForForm: (context: InternalFormContextValue) => Hydratable<FieldErrors | undefined>;
|
8
|
+
export declare const useDefaultValuesFromLoader: ({ formId, }: InternalFormContextValue) => any;
|
9
|
+
export declare const useDefaultValuesForForm: (context: InternalFormContextValue) => Hydratable<{
|
10
|
+
[fieldName: string]: any;
|
11
|
+
}>;
|
12
|
+
export declare const useHasActiveFormSubmit: ({ fetcher, }: InternalFormContextValue) => boolean;
|
13
|
+
export declare const useFieldTouched: (name: string, { formId }: InternalFormContextValue) => boolean;
|
14
|
+
export declare const useFieldError: (name: string, context: InternalFormContextValue) => string | undefined;
|
15
|
+
export declare const useFieldDefaultValue: (name: string, context: InternalFormContextValue) => any;
|
16
|
+
export declare const useFormUpdateAtom: typeof useUpdateAtom;
|
17
|
+
export declare const useClearError: (context: InternalFormContextValue) => (name: string) => void;
|
18
|
+
export declare const useSetTouched: (context: InternalFormContextValue) => (name: string, touched: boolean) => void;
|
@@ -0,0 +1,110 @@
|
|
1
|
+
import { useActionData, useMatches, useTransition } from "@remix-run/react";
|
2
|
+
import { useUpdateAtom } from "jotai/utils";
|
3
|
+
import lodashGet from "lodash/get";
|
4
|
+
import { useCallback, useContext } from "react";
|
5
|
+
import invariant from "tiny-invariant";
|
6
|
+
import { formDefaultValuesKey } from "./constants";
|
7
|
+
import { InternalFormContext } from "./formContext";
|
8
|
+
import { hydratable } from "./hydratable";
|
9
|
+
import { ATOM_SCOPE, clearErrorAtom, formRegistry, setTouchedAtom, } from "./state";
|
10
|
+
import { useFormData } from "./state-valtio";
|
11
|
+
export const useInternalFormContext = (formId, hookName) => {
|
12
|
+
const formContext = useContext(InternalFormContext);
|
13
|
+
if (formId)
|
14
|
+
return { formId };
|
15
|
+
if (formContext)
|
16
|
+
return formContext;
|
17
|
+
throw new Error(`Unable to determine form for ${hookName}. Please use it inside a form or pass a 'formId'.`);
|
18
|
+
};
|
19
|
+
export function useErrorResponseForForm({ fetcher, subaction, formId, }) {
|
20
|
+
var _a;
|
21
|
+
const actionData = useActionData();
|
22
|
+
if (fetcher) {
|
23
|
+
if ((_a = fetcher.data) === null || _a === void 0 ? void 0 : _a.fieldErrors)
|
24
|
+
return fetcher.data;
|
25
|
+
return null;
|
26
|
+
}
|
27
|
+
if (!(actionData === null || actionData === void 0 ? void 0 : actionData.fieldErrors))
|
28
|
+
return null;
|
29
|
+
// If there's an explicit id, we should ignore data that has the wrong id
|
30
|
+
if (typeof formId === "string" && actionData.formId)
|
31
|
+
return actionData.formId === formId ? actionData : null;
|
32
|
+
if ((!subaction && !actionData.subaction) ||
|
33
|
+
actionData.subaction === subaction)
|
34
|
+
return actionData;
|
35
|
+
return null;
|
36
|
+
}
|
37
|
+
export const useFieldErrorsForForm = (context) => {
|
38
|
+
const response = useErrorResponseForForm(context);
|
39
|
+
const form = useFormData(context.formId);
|
40
|
+
return hydratable.from(response === null || response === void 0 ? void 0 : response.fieldErrors, form.hydrated);
|
41
|
+
};
|
42
|
+
export const useDefaultValuesFromLoader = ({ formId, }) => {
|
43
|
+
const matches = useMatches();
|
44
|
+
if (typeof formId === "string") {
|
45
|
+
const dataKey = formDefaultValuesKey(formId);
|
46
|
+
// If multiple loaders declare the same default values,
|
47
|
+
// we should use the data from the deepest route.
|
48
|
+
const match = matches
|
49
|
+
.reverse()
|
50
|
+
.find((match) => match.data && dataKey in match.data);
|
51
|
+
return match === null || match === void 0 ? void 0 : match.data[dataKey];
|
52
|
+
}
|
53
|
+
return null;
|
54
|
+
};
|
55
|
+
export const useDefaultValuesForForm = (context) => {
|
56
|
+
const { formId, defaultValuesProp } = context;
|
57
|
+
const form = useFormData(formId);
|
58
|
+
const errorResponse = useErrorResponseForForm(context);
|
59
|
+
const defaultValuesFromLoader = useDefaultValuesFromLoader(context);
|
60
|
+
// Typical flow is:
|
61
|
+
// - Default values only available from props or server
|
62
|
+
// - Props have a higher priority than server
|
63
|
+
// - State gets hydrated with default values
|
64
|
+
// - After submit, we may need to use values from the error
|
65
|
+
if (form.hydrated)
|
66
|
+
return hydratable.hydratedData();
|
67
|
+
if (errorResponse === null || errorResponse === void 0 ? void 0 : errorResponse.repopulateFields) {
|
68
|
+
invariant(typeof errorResponse.repopulateFields === "object", "repopulateFields returned something other than an object");
|
69
|
+
return hydratable.serverData(errorResponse.repopulateFields);
|
70
|
+
}
|
71
|
+
if (defaultValuesProp)
|
72
|
+
return hydratable.serverData(defaultValuesProp);
|
73
|
+
return hydratable.serverData(defaultValuesFromLoader);
|
74
|
+
};
|
75
|
+
export const useHasActiveFormSubmit = ({ fetcher, }) => {
|
76
|
+
const transition = useTransition();
|
77
|
+
const hasActiveSubmission = fetcher
|
78
|
+
? fetcher.state === "submitting"
|
79
|
+
: !!transition.submission;
|
80
|
+
return hasActiveSubmission;
|
81
|
+
};
|
82
|
+
export const useFieldTouched = (name, { formId }) => {
|
83
|
+
const form = useFormData(formId);
|
84
|
+
return form.touchedFields[name];
|
85
|
+
};
|
86
|
+
export const useFieldError = (name, context) => {
|
87
|
+
const fieldErrors = useFieldErrorsForForm(context);
|
88
|
+
const form = useFormData(context.formId);
|
89
|
+
return fieldErrors
|
90
|
+
.map((fieldErrors) => fieldErrors === null || fieldErrors === void 0 ? void 0 : fieldErrors[name])
|
91
|
+
.hydrateTo(form.fieldErrors[name]);
|
92
|
+
};
|
93
|
+
export const useFieldDefaultValue = (name, context) => {
|
94
|
+
const defaultValues = useDefaultValuesForForm(context);
|
95
|
+
const state = useFormData(context.formId);
|
96
|
+
return defaultValues.map((val) => lodashGet(val, name)).hydrateTo(state);
|
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,16 @@
|
|
1
|
+
import { FieldErrors, ValidationErrorResponseData } from "..";
|
2
|
+
import { InternalFormContextValue } from "./formContext";
|
3
|
+
import { Hydratable } from "./hydratable";
|
4
|
+
export declare const useInternalFormContext: (formId?: string | symbol | undefined, hookName?: string | undefined) => InternalFormContextValue;
|
5
|
+
export declare function useErrorResponseForForm({ fetcher, subaction, formId, }: InternalFormContextValue): ValidationErrorResponseData | null;
|
6
|
+
export declare const useFieldErrorsForForm: (context: InternalFormContextValue) => Hydratable<FieldErrors | undefined>;
|
7
|
+
export declare const useDefaultValuesFromLoader: ({ formId, }: InternalFormContextValue) => any;
|
8
|
+
export declare const useDefaultValuesForForm: (context: InternalFormContextValue) => Hydratable<{
|
9
|
+
[fieldName: string]: any;
|
10
|
+
}>;
|
11
|
+
export declare const useHasActiveFormSubmit: ({ fetcher, }: InternalFormContextValue) => boolean;
|
12
|
+
export declare const useFieldTouched: (name: string, { formId }: InternalFormContextValue) => boolean;
|
13
|
+
export declare const useFieldError: (name: string, context: InternalFormContextValue) => string | undefined;
|
14
|
+
export declare const useFieldDefaultValue: (name: string, context: InternalFormContextValue) => any;
|
15
|
+
export declare const useClearError: (context: InternalFormContextValue) => (name: string) => void;
|
16
|
+
export declare const useSetTouched: (context: InternalFormContextValue) => (name: string, touched: boolean) => void;
|
@@ -0,0 +1,100 @@
|
|
1
|
+
import { useActionData, useMatches, useTransition } from "@remix-run/react";
|
2
|
+
import lodashGet from "lodash/get";
|
3
|
+
import { useContext } from "react";
|
4
|
+
import invariant from "tiny-invariant";
|
5
|
+
import { formDefaultValuesKey } from "./constants";
|
6
|
+
import { InternalFormContext } from "./formContext";
|
7
|
+
import { hydratable } from "./hydratable";
|
8
|
+
import { useStore } from "./state-zustand";
|
9
|
+
export const useInternalFormContext = (formId, hookName) => {
|
10
|
+
const formContext = useContext(InternalFormContext);
|
11
|
+
if (formId)
|
12
|
+
return { formId };
|
13
|
+
if (formContext)
|
14
|
+
return formContext;
|
15
|
+
throw new Error(`Unable to determine form for ${hookName}. Please use it inside a form or pass a 'formId'.`);
|
16
|
+
};
|
17
|
+
export function useErrorResponseForForm({ fetcher, subaction, formId, }) {
|
18
|
+
var _a;
|
19
|
+
const actionData = useActionData();
|
20
|
+
if (fetcher) {
|
21
|
+
if ((_a = fetcher.data) === null || _a === void 0 ? void 0 : _a.fieldErrors)
|
22
|
+
return fetcher.data;
|
23
|
+
return null;
|
24
|
+
}
|
25
|
+
if (!(actionData === null || actionData === void 0 ? void 0 : actionData.fieldErrors))
|
26
|
+
return null;
|
27
|
+
// If there's an explicit id, we should ignore data that has the wrong id
|
28
|
+
if (typeof formId === "string" && actionData.formId)
|
29
|
+
return actionData.formId === formId ? actionData : null;
|
30
|
+
if ((!subaction && !actionData.subaction) ||
|
31
|
+
actionData.subaction === subaction)
|
32
|
+
return actionData;
|
33
|
+
return null;
|
34
|
+
}
|
35
|
+
export const useFieldErrorsForForm = (context) => {
|
36
|
+
const response = useErrorResponseForForm(context);
|
37
|
+
const hydrated = useStore((state) => state.form(context.formId).hydrated);
|
38
|
+
return hydratable.from(response === null || response === void 0 ? void 0 : response.fieldErrors, hydrated);
|
39
|
+
};
|
40
|
+
export const useDefaultValuesFromLoader = ({ formId, }) => {
|
41
|
+
const matches = useMatches();
|
42
|
+
if (typeof formId === "string") {
|
43
|
+
const dataKey = formDefaultValuesKey(formId);
|
44
|
+
// If multiple loaders declare the same default values,
|
45
|
+
// we should use the data from the deepest route.
|
46
|
+
const match = matches
|
47
|
+
.reverse()
|
48
|
+
.find((match) => match.data && dataKey in match.data);
|
49
|
+
return match === null || match === void 0 ? void 0 : match.data[dataKey];
|
50
|
+
}
|
51
|
+
return null;
|
52
|
+
};
|
53
|
+
export const useDefaultValuesForForm = (context) => {
|
54
|
+
const { formId, defaultValuesProp } = context;
|
55
|
+
const hydrated = useStore((state) => state.form(formId).hydrated);
|
56
|
+
const errorResponse = useErrorResponseForForm(context);
|
57
|
+
const defaultValuesFromLoader = useDefaultValuesFromLoader(context);
|
58
|
+
// Typical flow is:
|
59
|
+
// - Default values only available from props or server
|
60
|
+
// - Props have a higher priority than server
|
61
|
+
// - State gets hydrated with default values
|
62
|
+
// - After submit, we may need to use values from the error
|
63
|
+
if (hydrated)
|
64
|
+
return hydratable.hydratedData();
|
65
|
+
if (errorResponse === null || errorResponse === void 0 ? void 0 : errorResponse.repopulateFields) {
|
66
|
+
invariant(typeof errorResponse.repopulateFields === "object", "repopulateFields returned something other than an object");
|
67
|
+
return hydratable.serverData(errorResponse.repopulateFields);
|
68
|
+
}
|
69
|
+
if (defaultValuesProp)
|
70
|
+
return hydratable.serverData(defaultValuesProp);
|
71
|
+
return hydratable.serverData(defaultValuesFromLoader);
|
72
|
+
};
|
73
|
+
export const useHasActiveFormSubmit = ({ fetcher, }) => {
|
74
|
+
const transition = useTransition();
|
75
|
+
const hasActiveSubmission = fetcher
|
76
|
+
? fetcher.state === "submitting"
|
77
|
+
: !!transition.submission;
|
78
|
+
return hasActiveSubmission;
|
79
|
+
};
|
80
|
+
export const useFieldTouched = (name, { formId }) => {
|
81
|
+
return useStore((state) => state.form(formId).touchedFields[name]);
|
82
|
+
};
|
83
|
+
export const useFieldError = (name, context) => {
|
84
|
+
const state = useStore((state) => state.form(context.formId).fieldErrors[name]);
|
85
|
+
return useFieldErrorsForForm(context)
|
86
|
+
.map((fieldErrors) => fieldErrors === null || fieldErrors === void 0 ? void 0 : fieldErrors[name])
|
87
|
+
.hydrateTo(state);
|
88
|
+
};
|
89
|
+
export const useFieldDefaultValue = (name, context) => {
|
90
|
+
const state = useStore((state) => state.form(context.formId).defaultValues[name]);
|
91
|
+
return useDefaultValuesForForm(context)
|
92
|
+
.map((val) => lodashGet(val, name))
|
93
|
+
.hydrateTo(state);
|
94
|
+
};
|
95
|
+
export const useClearError = (context) => {
|
96
|
+
return useStore((state) => state.helpers(context.formId).clearError);
|
97
|
+
};
|
98
|
+
export const useSetTouched = (context) => {
|
99
|
+
return useStore((state) => state.helpers(context.formId).setTouched);
|
100
|
+
};
|
@@ -0,0 +1,14 @@
|
|
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 declare type Hydratable<T> = {
|
7
|
+
hydrateTo: (data: T) => T;
|
8
|
+
map: <U>(fn: (data: T) => U) => Hydratable<U>;
|
9
|
+
};
|
10
|
+
export declare const hydratable: {
|
11
|
+
serverData: <T>(data: T) => Hydratable<T>;
|
12
|
+
hydratedData: <T_1>() => Hydratable<T_1>;
|
13
|
+
from: <T_2>(data: T_2, hydrated: boolean) => Hydratable<T_2>;
|
14
|
+
};
|
@@ -0,0 +1,14 @@
|
|
1
|
+
const serverData = (data) => ({
|
2
|
+
hydrateTo: () => data,
|
3
|
+
map: (fn) => serverData(fn(data)),
|
4
|
+
});
|
5
|
+
const hydratedData = () => ({
|
6
|
+
hydrateTo: (hydratedData) => hydratedData,
|
7
|
+
map: () => hydratedData(),
|
8
|
+
});
|
9
|
+
const from = (data, hydrated) => hydrated ? hydratedData() : serverData(data);
|
10
|
+
export const hydratable = {
|
11
|
+
serverData,
|
12
|
+
hydratedData,
|
13
|
+
from,
|
14
|
+
};
|
@@ -0,0 +1,6 @@
|
|
1
|
+
import { Draft } from "immer";
|
2
|
+
import { State, StateCreator } from "zustand";
|
3
|
+
declare type TImmerConfigFn<T extends State> = (partial: ((draft: Draft<T>) => void) | T, replace?: boolean) => void;
|
4
|
+
declare type TImmerConfig<T extends State> = StateCreator<T, TImmerConfigFn<T>>;
|
5
|
+
export declare const immer: <T extends object>(config: TImmerConfig<T>) => StateCreator<T, import("zustand").SetState<T>, import("zustand").GetState<T>, import("zustand").StoreApi<T>>;
|
6
|
+
export {};
|
@@ -1,6 +1,6 @@
|
|
1
1
|
export const getCheckboxChecked = (checkboxValue = "on", newValue) => {
|
2
2
|
if (Array.isArray(newValue))
|
3
|
-
return newValue.
|
3
|
+
return newValue.some((val) => val === true || val === checkboxValue);
|
4
4
|
if (typeof newValue === "boolean")
|
5
5
|
return newValue;
|
6
6
|
if (typeof newValue === "string")
|
@@ -0,0 +1 @@
|
|
1
|
+
export declare const setInputValueInForm: (formElement: HTMLFormElement, name: string, value: unknown[]) => void;
|
@@ -0,0 +1,120 @@
|
|
1
|
+
import invariant from "tiny-invariant";
|
2
|
+
import { getCheckboxChecked } from "./getCheckboxChecked";
|
3
|
+
/**
|
4
|
+
* Helper class to track the values being set on uncontrolled fields.
|
5
|
+
* HTML is super permissive with inputs that all share the same `name`.
|
6
|
+
*
|
7
|
+
* This class is strict in the sense that, if the user provides an array value,
|
8
|
+
* the values inside the array must be in the same order as the elements in the DOM.
|
9
|
+
* Doing this allows us to be flexible with what types of form controls the user is using.
|
10
|
+
*
|
11
|
+
* This is how HTML tracks inputs of the same name as well.
|
12
|
+
* `new FormData(formElement).getAll('myField')` will return values in DOM order.
|
13
|
+
*/
|
14
|
+
class Values {
|
15
|
+
constructor(values) {
|
16
|
+
this.hasSetRadioValue = false;
|
17
|
+
this.bool = (value) => {
|
18
|
+
if (getCheckboxChecked(value, this.values[0])) {
|
19
|
+
this.values.shift();
|
20
|
+
return true;
|
21
|
+
}
|
22
|
+
return false;
|
23
|
+
};
|
24
|
+
this.radio = (value) => {
|
25
|
+
if (this.hasSetRadioValue)
|
26
|
+
return false;
|
27
|
+
const result = this.bool(value);
|
28
|
+
if (result)
|
29
|
+
this.hasSetRadioValue = true;
|
30
|
+
return result;
|
31
|
+
};
|
32
|
+
this.str = () => { var _a; return String((_a = this.values.pop()) !== null && _a !== void 0 ? _a : ""); };
|
33
|
+
this.allValues = () => this.values;
|
34
|
+
this.warnIfLeftovers = (field) => {
|
35
|
+
if (this.values.length > 0) {
|
36
|
+
console.warn(`Could not determine how to use the value for the field ${field}. ` +
|
37
|
+
`Leftover values were: ${this.values.join(", ")}.`);
|
38
|
+
}
|
39
|
+
};
|
40
|
+
const unknownValues = Array.isArray(values) ? values : [values];
|
41
|
+
this.values = unknownValues;
|
42
|
+
}
|
43
|
+
}
|
44
|
+
/**
|
45
|
+
* This subclass is order-permissive, meaning the user doesn't have to worry about
|
46
|
+
* the order in which the inputs occur in the DOM.
|
47
|
+
* This is useful for multiselects and checkbox groups and provides a better DX than
|
48
|
+
* the order-strict version.
|
49
|
+
*/
|
50
|
+
class PermissiveValues extends Values {
|
51
|
+
constructor() {
|
52
|
+
super(...arguments);
|
53
|
+
this.remove = (value) => {
|
54
|
+
const index = this.values.indexOf(value);
|
55
|
+
const deleted = this.values.splice(index, 1);
|
56
|
+
return deleted.length > 0;
|
57
|
+
};
|
58
|
+
this.bool = (value) => {
|
59
|
+
if (getCheckboxChecked(value, this.values)) {
|
60
|
+
this.remove(value) || this.remove(true);
|
61
|
+
return true;
|
62
|
+
}
|
63
|
+
return false;
|
64
|
+
};
|
65
|
+
}
|
66
|
+
}
|
67
|
+
const isMultiselect = (node) => node instanceof HTMLSelectElement && node.multiple;
|
68
|
+
const isCheckbox = (node) => node instanceof HTMLInputElement && node.type === "checkbox";
|
69
|
+
const isRadio = (node) => node instanceof HTMLInputElement && node.type === "radio";
|
70
|
+
const setElementValue = (element, values, field) => {
|
71
|
+
if (isMultiselect(element)) {
|
72
|
+
for (const option of element.options) {
|
73
|
+
option.selected = values.bool(option.value);
|
74
|
+
}
|
75
|
+
return;
|
76
|
+
}
|
77
|
+
if (isCheckbox(element)) {
|
78
|
+
element.checked = values.bool(element.value);
|
79
|
+
return;
|
80
|
+
}
|
81
|
+
if (isRadio(element)) {
|
82
|
+
element.checked = values.radio(element.value);
|
83
|
+
return;
|
84
|
+
}
|
85
|
+
const input = element;
|
86
|
+
invariant(input.type !== "hidden", `Cannot set value on hidden input if it is not a controlled field. Field being updated was ${field}.`);
|
87
|
+
input.value = values.str();
|
88
|
+
};
|
89
|
+
const areElementsTheSameType = (nodes) => {
|
90
|
+
const getType = (node) => {
|
91
|
+
if (node instanceof HTMLInputElement)
|
92
|
+
return node.type;
|
93
|
+
if (node instanceof HTMLSelectElement)
|
94
|
+
return node.multiple ? "select" : "multiselect";
|
95
|
+
return null;
|
96
|
+
};
|
97
|
+
const firstElementInstance = nodes[0].constructor;
|
98
|
+
const firstElementType = getType(nodes[0]);
|
99
|
+
return nodes.every((element) => element.constructor === firstElementInstance &&
|
100
|
+
getType(element) === firstElementType);
|
101
|
+
};
|
102
|
+
export const setInputValueInForm = (formElement, name, value) => {
|
103
|
+
const controlElement = formElement.elements.namedItem(name);
|
104
|
+
if (!controlElement)
|
105
|
+
return;
|
106
|
+
if (controlElement instanceof RadioNodeList) {
|
107
|
+
const values = areElementsTheSameType([...controlElement])
|
108
|
+
? new PermissiveValues(value)
|
109
|
+
: new Values(value);
|
110
|
+
for (const element of controlElement) {
|
111
|
+
setElementValue(element, values, name);
|
112
|
+
}
|
113
|
+
values.warnIfLeftovers(name);
|
114
|
+
}
|
115
|
+
else {
|
116
|
+
const values = new PermissiveValues(value);
|
117
|
+
setElementValue(controlElement, values, name);
|
118
|
+
values.warnIfLeftovers(name);
|
119
|
+
}
|
120
|
+
};
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import { InternalFormId } from "./state/atomUtils";
|
2
|
+
declare type ControlledDataItem = Record<string, unknown[]>;
|
3
|
+
declare class ValidatedFormData {
|
4
|
+
#private;
|
5
|
+
constructor(formData: FormData, customData: ControlledDataItem);
|
6
|
+
get: (fieldName: string) => any;
|
7
|
+
getAll: (fieldName: string) => any[];
|
8
|
+
has: (fieldName: string) => boolean;
|
9
|
+
append: (fieldName: string, value: any) => void;
|
10
|
+
delete: (fieldName: string) => void;
|
11
|
+
set: (fieldName: string, value: any) => void;
|
12
|
+
entries: () => IterableIterator<[string, any[]]>;
|
13
|
+
values: () => IterableIterator<any[]>;
|
14
|
+
[Symbol.iterator]: () => IterableIterator<[string, any[]]>;
|
15
|
+
setRepeated: (fieldName: string, value: any[]) => void;
|
16
|
+
changedFields(): Generator<readonly [string, any[]], void, unknown>;
|
17
|
+
}
|
18
|
+
export declare type FormValuesUpdater = (formData: ValidatedFormData) => void;
|
19
|
+
export declare const useSetFormValues: (formId: InternalFormId) => (arg: FormValuesUpdater) => Promise<void>;
|
20
|
+
export {};
|
@@ -0,0 +1,83 @@
|
|
1
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
2
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
3
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
4
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
5
|
+
};
|
6
|
+
var _ValidatedFormData_data, _ValidatedFormData_updates, _a;
|
7
|
+
import { useAtomCallback } from "jotai/utils";
|
8
|
+
import groupBy from "lodash/groupBy";
|
9
|
+
import mapValues from "lodash/mapValues";
|
10
|
+
import invariant from "tiny-invariant";
|
11
|
+
import { setInputValueInForm } from "./logic/setInputValueInForm";
|
12
|
+
import { MultiValueMap } from "./MultiValueMap";
|
13
|
+
import { ATOM_SCOPE, formElementAtom } from "./state";
|
14
|
+
import { controlledFieldsAtom, setControlledFieldValueAtom, } from "./state/controlledFields";
|
15
|
+
class ValidatedFormData {
|
16
|
+
constructor(formData, customData) {
|
17
|
+
_ValidatedFormData_data.set(this, new MultiValueMap());
|
18
|
+
_ValidatedFormData_updates.set(this, new Map());
|
19
|
+
// API to mimic form data
|
20
|
+
this.get = (fieldName) => __classPrivateFieldGet(this, _ValidatedFormData_data, "f").getAll(fieldName)[0];
|
21
|
+
this.getAll = (fieldName) => __classPrivateFieldGet(this, _ValidatedFormData_data, "f").getAll(fieldName);
|
22
|
+
this.has = (fieldName) => __classPrivateFieldGet(this, _ValidatedFormData_data, "f").has(fieldName);
|
23
|
+
this.append = (fieldName, value) => {
|
24
|
+
__classPrivateFieldGet(this, _ValidatedFormData_data, "f").add(fieldName, value);
|
25
|
+
};
|
26
|
+
this.delete = (fieldName) => {
|
27
|
+
__classPrivateFieldGet(this, _ValidatedFormData_data, "f").delete(fieldName);
|
28
|
+
__classPrivateFieldGet(this, _ValidatedFormData_updates, "f").set(fieldName, true);
|
29
|
+
};
|
30
|
+
this.set = (fieldName, value) => {
|
31
|
+
__classPrivateFieldGet(this, _ValidatedFormData_data, "f").delete(fieldName);
|
32
|
+
__classPrivateFieldGet(this, _ValidatedFormData_data, "f").add(fieldName, value);
|
33
|
+
__classPrivateFieldGet(this, _ValidatedFormData_updates, "f").set(fieldName, true);
|
34
|
+
};
|
35
|
+
this.entries = () => __classPrivateFieldGet(this, _ValidatedFormData_data, "f").entries();
|
36
|
+
this.values = () => __classPrivateFieldGet(this, _ValidatedFormData_data, "f").values();
|
37
|
+
this[_a] = () => __classPrivateFieldGet(this, _ValidatedFormData_data, "f").entries();
|
38
|
+
// Custom APIs
|
39
|
+
this.setRepeated = (fieldName, value) => {
|
40
|
+
__classPrivateFieldGet(this, _ValidatedFormData_data, "f").delete(fieldName);
|
41
|
+
value.forEach((val) => __classPrivateFieldGet(this, _ValidatedFormData_data, "f").add(fieldName, val));
|
42
|
+
__classPrivateFieldGet(this, _ValidatedFormData_updates, "f").set(fieldName, true);
|
43
|
+
};
|
44
|
+
for (const [key, value] of formData.entries()) {
|
45
|
+
__classPrivateFieldGet(this, _ValidatedFormData_data, "f").add(key, value);
|
46
|
+
}
|
47
|
+
Object.entries(customData).forEach(([name, values]) => {
|
48
|
+
__classPrivateFieldGet(this, _ValidatedFormData_data, "f").delete(name);
|
49
|
+
values.forEach((value) => __classPrivateFieldGet(this, _ValidatedFormData_data, "f").add(name, value));
|
50
|
+
});
|
51
|
+
}
|
52
|
+
*changedFields() {
|
53
|
+
for (const updatedField of __classPrivateFieldGet(this, _ValidatedFormData_updates, "f").keys()) {
|
54
|
+
const value = this.getAll(updatedField);
|
55
|
+
yield [updatedField, value];
|
56
|
+
}
|
57
|
+
}
|
58
|
+
}
|
59
|
+
_ValidatedFormData_data = new WeakMap(), _ValidatedFormData_updates = new WeakMap(), _a = Symbol.iterator;
|
60
|
+
export const useSetFormValues = (formId) => useAtomCallback(async (get, set, update) => {
|
61
|
+
var _b;
|
62
|
+
const form = get(formElementAtom(formId));
|
63
|
+
invariant(form, "Unable to access form element when setting field value. This is likely a bug in remix-validated-form.");
|
64
|
+
const formData = new FormData(form);
|
65
|
+
const controlledFields = get(controlledFieldsAtom(formId));
|
66
|
+
const controlledData = mapValues(groupBy(controlledFields, (field) => field.name), (val) => val.map((field) => get(field.valueAtom)));
|
67
|
+
const validatedFormData = new ValidatedFormData(formData, controlledData);
|
68
|
+
update(validatedFormData);
|
69
|
+
for (const [field, value] of validatedFormData.changedFields()) {
|
70
|
+
const relevantFields = controlledFields.filter(({ name }) => name === field);
|
71
|
+
if (relevantFields.length === 0) {
|
72
|
+
setInputValueInForm(form, field, value);
|
73
|
+
return;
|
74
|
+
}
|
75
|
+
for (const [index, field] of relevantFields.entries()) {
|
76
|
+
const itemValue = (_b = value[index]) !== null && _b !== void 0 ? _b : "";
|
77
|
+
await set(setControlledFieldValueAtom, {
|
78
|
+
internalFieldId: field.internalId,
|
79
|
+
value: itemValue,
|
80
|
+
});
|
81
|
+
}
|
82
|
+
}
|
83
|
+
}, ATOM_SCOPE);
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import { useAtomCallback } from "jotai/utils";
|
2
|
+
import { useCallback } from "react";
|
3
|
+
import invariant from "tiny-invariant";
|
4
|
+
import { setInputValueInForm } from "./logic/setInputValueInForm";
|
5
|
+
import { ATOM_SCOPE, formElementAtom } from "./state";
|
6
|
+
import { controlledFieldsAtom, setControlledFieldValueAtom, } from "./state/controlledFields";
|
7
|
+
export const useSetFormValues = (formId) => useAtomCallback(useCallback(async (get, set, updatedValues) => {
|
8
|
+
const form = get(formElementAtom(formId));
|
9
|
+
invariant(form, "Unable to access form element when setting field value. This is likely a bug in remix-validated-form.");
|
10
|
+
const controlledFields = get(controlledFieldsAtom(formId));
|
11
|
+
const updatePromises = [];
|
12
|
+
for (const [field, value] of Object.entries(updatedValues)) {
|
13
|
+
const isControlled = !!controlledFields[field];
|
14
|
+
if (isControlled) {
|
15
|
+
updatePromises.push(set(setControlledFieldValueAtom, {
|
16
|
+
field,
|
17
|
+
formId,
|
18
|
+
value,
|
19
|
+
}));
|
20
|
+
}
|
21
|
+
else {
|
22
|
+
setInputValueInForm(form, field, Array.isArray(value) ? value : [value]);
|
23
|
+
}
|
24
|
+
}
|
25
|
+
await Promise.all(updatePromises);
|
26
|
+
}, [formId]), ATOM_SCOPE);
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import { Atom } from "jotai";
|
2
|
+
export declare type InternalFormId = string | symbol;
|
3
|
+
export declare const formAtomFamily: <T>(data: T) => {
|
4
|
+
(param: InternalFormId): Atom<T> & {
|
5
|
+
write: (get: {
|
6
|
+
<Value>(atom: Atom<Value | Promise<Value>>): Value;
|
7
|
+
<Value_1>(atom: Atom<Promise<Value_1>>): Value_1;
|
8
|
+
<Value_2>(atom: Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
|
9
|
+
} & {
|
10
|
+
<Value_3>(atom: Atom<Value_3 | Promise<Value_3>>, options: {
|
11
|
+
unstable_promise: true;
|
12
|
+
}): Value_3 | Promise<Value_3>;
|
13
|
+
<Value_4>(atom: Atom<Promise<Value_4>>, options: {
|
14
|
+
unstable_promise: true;
|
15
|
+
}): Value_4 | Promise<Value_4>;
|
16
|
+
<Value_5>(atom: Atom<Value_5>, options: {
|
17
|
+
unstable_promise: true;
|
18
|
+
}): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
|
19
|
+
}, set: {
|
20
|
+
<Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
|
21
|
+
<Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
|
22
|
+
}, update: T | ((prev: T) => T)) => void;
|
23
|
+
onMount?: (<S extends import("jotai/core/atom").SetAtom<T | ((prev: T) => T), void>>(setAtom: S) => void | (() => void)) | undefined;
|
24
|
+
} & {
|
25
|
+
init: T;
|
26
|
+
};
|
27
|
+
remove(param: InternalFormId): void;
|
28
|
+
setShouldRemove(shouldRemove: ((createdAt: number, param: InternalFormId) => boolean) | null): void;
|
29
|
+
};
|
30
|
+
export declare type FieldAtomKey = {
|
31
|
+
formId: InternalFormId;
|
32
|
+
field: string;
|
33
|
+
};
|
34
|
+
export declare const fieldAtomFamily: <T extends Atom<unknown>>(func: (key: FieldAtomKey) => T) => {
|
35
|
+
(param: FieldAtomKey): T;
|
36
|
+
remove(param: FieldAtomKey): void;
|
37
|
+
setShouldRemove(shouldRemove: ((createdAt: number, param: FieldAtomKey) => boolean) | null): void;
|
38
|
+
};
|