remix-validated-form 4.1.2 → 4.1.3
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 +0 -0
- package/browser/internal/hooks-valtio.js +0 -0
- package/browser/internal/hooks-zustand.d.ts +0 -0
- package/browser/internal/hooks-zustand.js +0 -0
- package/browser/internal/hydratable.d.ts +0 -0
- package/browser/internal/hydratable.js +0 -0
- package/browser/internal/immerMiddleware.d.ts +0 -0
- package/browser/internal/immerMiddleware.js +0 -0
- package/browser/internal/logic/getCheckboxChecked.js +1 -1
- package/browser/internal/logic/setInputValueInForm.d.ts +1 -1
- package/browser/internal/logic/setInputValueInForm.js +97 -17
- 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 +0 -0
- package/browser/internal/state-valtio.js +0 -0
- package/browser/internal/state-zustand.d.ts +0 -0
- package/browser/internal/state-zustand.js +0 -0
- package/build/ValidatedForm.js +9 -4
- package/build/internal/hooks-valtio.d.ts +0 -0
- package/build/internal/hooks-valtio.js +0 -0
- package/build/internal/hooks-zustand.d.ts +0 -0
- package/build/internal/hooks-zustand.js +0 -0
- package/build/internal/hydratable.d.ts +0 -0
- package/build/internal/hydratable.js +0 -0
- package/build/internal/immerMiddleware.d.ts +0 -0
- package/build/internal/immerMiddleware.js +0 -0
- package/build/internal/logic/getCheckboxChecked.js +1 -1
- package/build/internal/logic/setInputValueInForm.d.ts +1 -1
- package/build/internal/logic/setInputValueInForm.js +97 -17
- 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 +0 -0
- package/build/internal/state-valtio.js +0 -0
- package/build/internal/state-zustand.d.ts +0 -0
- package/build/internal/state-zustand.js +0 -0
- package/package.json +5 -5
- package/src/ValidatedForm.tsx +10 -4
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
|
});
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -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")
|
@@ -1 +1 @@
|
|
1
|
-
export declare const setInputValueInForm: (formElement: HTMLFormElement, name: string, value: unknown) => void;
|
1
|
+
export declare const setInputValueInForm: (formElement: HTMLFormElement, name: string, value: unknown[]) => void;
|
@@ -1,40 +1,120 @@
|
|
1
1
|
import invariant from "tiny-invariant";
|
2
2
|
import { getCheckboxChecked } from "./getCheckboxChecked";
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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)) {
|
7
72
|
for (const option of element.options) {
|
8
|
-
option.selected =
|
73
|
+
option.selected = values.bool(option.value);
|
9
74
|
}
|
10
75
|
return;
|
11
76
|
}
|
12
|
-
if (element
|
13
|
-
|
14
|
-
invariant(newChecked !== undefined, `Unable to determine if checkbox should be checked. Provided value was ${value} for checkbox ${name}.`);
|
15
|
-
element.checked = newChecked;
|
77
|
+
if (isCheckbox(element)) {
|
78
|
+
element.checked = values.bool(element.value);
|
16
79
|
return;
|
17
80
|
}
|
18
|
-
if (element
|
19
|
-
|
20
|
-
invariant(newChecked !== undefined, `Unable to determine if radio should be checked. Provided value was ${value} for radio ${name}.`);
|
21
|
-
element.checked = newChecked;
|
81
|
+
if (isRadio(element)) {
|
82
|
+
element.checked = values.radio(element.value);
|
22
83
|
return;
|
23
84
|
}
|
24
|
-
invariant(typeof value === "string", `Invalid value for field "${name}" which is an ${element.constructor.name}. Expected string but received ${typeof value}`);
|
25
85
|
const input = element;
|
26
|
-
input.value
|
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);
|
27
101
|
};
|
28
102
|
export const setInputValueInForm = (formElement, name, value) => {
|
29
103
|
const controlElement = formElement.elements.namedItem(name);
|
30
104
|
if (!controlElement)
|
31
105
|
return;
|
32
106
|
if (controlElement instanceof RadioNodeList) {
|
107
|
+
const values = areElementsTheSameType([...controlElement])
|
108
|
+
? new PermissiveValues(value)
|
109
|
+
: new Values(value);
|
33
110
|
for (const element of controlElement) {
|
34
|
-
setElementValue(element,
|
111
|
+
setElementValue(element, values, name);
|
35
112
|
}
|
113
|
+
values.warnIfLeftovers(name);
|
36
114
|
}
|
37
115
|
else {
|
38
|
-
|
116
|
+
const values = new PermissiveValues(value);
|
117
|
+
setElementValue(controlElement, values, name);
|
118
|
+
values.warnIfLeftovers(name);
|
39
119
|
}
|
40
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
|
+
};
|
@@ -0,0 +1,66 @@
|
|
1
|
+
import { PrimitiveAtom } from "jotai";
|
2
|
+
import { InternalFormId } from "./atomUtils";
|
3
|
+
export declare type ValueSerializer = (val: unknown) => string;
|
4
|
+
export declare const controlledFieldsAtom: {
|
5
|
+
(param: InternalFormId): import("jotai").Atom<Record<string, PrimitiveAtom<unknown>>> & {
|
6
|
+
write: (get: {
|
7
|
+
<Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
|
8
|
+
<Value_1>(atom: import("jotai").Atom<Promise<Value_1>>): Value_1;
|
9
|
+
<Value_2>(atom: import("jotai").Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
|
10
|
+
} & {
|
11
|
+
<Value_3>(atom: import("jotai").Atom<Value_3 | Promise<Value_3>>, options: {
|
12
|
+
unstable_promise: true;
|
13
|
+
}): Value_3 | Promise<Value_3>;
|
14
|
+
<Value_4>(atom: import("jotai").Atom<Promise<Value_4>>, options: {
|
15
|
+
unstable_promise: true;
|
16
|
+
}): Value_4 | Promise<Value_4>;
|
17
|
+
<Value_5>(atom: import("jotai").Atom<Value_5>, options: {
|
18
|
+
unstable_promise: true;
|
19
|
+
}): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
|
20
|
+
}, set: {
|
21
|
+
<Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
|
22
|
+
<Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
|
23
|
+
}, update: Record<string, PrimitiveAtom<unknown>> | ((prev: Record<string, PrimitiveAtom<unknown>>) => Record<string, PrimitiveAtom<unknown>>)) => void;
|
24
|
+
onMount?: (<S extends (update: Record<string, PrimitiveAtom<unknown>> | ((prev: Record<string, PrimitiveAtom<unknown>>) => Record<string, PrimitiveAtom<unknown>>)) => void>(setAtom: S) => void | (() => void)) | undefined;
|
25
|
+
} & {
|
26
|
+
init: Record<string, PrimitiveAtom<unknown>>;
|
27
|
+
};
|
28
|
+
remove(param: InternalFormId): void;
|
29
|
+
setShouldRemove(shouldRemove: ((createdAt: number, param: InternalFormId) => boolean) | null): void;
|
30
|
+
};
|
31
|
+
export declare const setControlledFieldValueAtom: import("jotai").Atom<null> & {
|
32
|
+
write: (get: {
|
33
|
+
<Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
|
34
|
+
<Value_1>(atom: import("jotai").Atom<Promise<Value_1>>): Value_1;
|
35
|
+
<Value_2>(atom: import("jotai").Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
|
36
|
+
} & {
|
37
|
+
<Value_3>(atom: import("jotai").Atom<Value_3 | Promise<Value_3>>, options: {
|
38
|
+
unstable_promise: true;
|
39
|
+
}): Value_3 | Promise<Value_3>;
|
40
|
+
<Value_4>(atom: import("jotai").Atom<Promise<Value_4>>, options: {
|
41
|
+
unstable_promise: true;
|
42
|
+
}): Value_4 | Promise<Value_4>;
|
43
|
+
<Value_5>(atom: import("jotai").Atom<Value_5>, options: {
|
44
|
+
unstable_promise: true;
|
45
|
+
}): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
|
46
|
+
}, set: {
|
47
|
+
<Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
|
48
|
+
<Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
|
49
|
+
}, update: {
|
50
|
+
formId: InternalFormId;
|
51
|
+
field: string;
|
52
|
+
value: unknown;
|
53
|
+
}) => Promise<void>;
|
54
|
+
onMount?: (<S extends (update: {
|
55
|
+
formId: InternalFormId;
|
56
|
+
field: string;
|
57
|
+
value: unknown;
|
58
|
+
}) => Promise<void>>(setAtom: S) => void | (() => void)) | undefined;
|
59
|
+
} & {
|
60
|
+
init: null;
|
61
|
+
};
|
62
|
+
export declare const useAllControlledFields: (formId: InternalFormId) => Record<string, PrimitiveAtom<unknown>>;
|
63
|
+
export declare const useControlledFieldValue: (formId: InternalFormId, field: string) => any;
|
64
|
+
export declare const useControllableValue: (formId: InternalFormId, field: string) => readonly [any, (value: unknown) => Promise<void>];
|
65
|
+
export declare const useFieldSerializer: (formId: InternalFormId, field: string) => ValueSerializer;
|
66
|
+
export declare const useRegisterFieldSerializer: (formId: InternalFormId, field: string, serializer?: ValueSerializer | undefined) => void;
|
@@ -0,0 +1,93 @@
|
|
1
|
+
import { atom } from "jotai";
|
2
|
+
import omit from "lodash/omit";
|
3
|
+
import { useCallback, useEffect } from "react";
|
4
|
+
import { useFieldDefaultValue, useFormAtomValue, useFormAtom, useFormUpdateAtom, } from "../hooks";
|
5
|
+
import { isHydratedAtom } from "../state";
|
6
|
+
import { fieldAtomFamily, formAtomFamily, } from "./atomUtils";
|
7
|
+
export const controlledFieldsAtom = formAtomFamily({});
|
8
|
+
const refCountAtom = fieldAtomFamily(() => atom(0));
|
9
|
+
const fieldValueAtom = fieldAtomFamily(() => atom(undefined));
|
10
|
+
const fieldValueHydratedAtom = fieldAtomFamily(() => atom(false));
|
11
|
+
const pendingValidateAtom = fieldAtomFamily(() => atom(undefined));
|
12
|
+
const valueSerializerAtom = fieldAtomFamily(() => atom({
|
13
|
+
serializer: (value) => {
|
14
|
+
if (value === undefined)
|
15
|
+
return "";
|
16
|
+
return JSON.stringify(value);
|
17
|
+
},
|
18
|
+
}));
|
19
|
+
const registerAtom = atom(null, (get, set, { formId, field }) => {
|
20
|
+
set(refCountAtom({ formId, field }), (prev) => prev + 1);
|
21
|
+
const newRefCount = get(refCountAtom({ formId, field }));
|
22
|
+
// We don't set hydrated here because it gets set when we know
|
23
|
+
// we have the right default values
|
24
|
+
if (newRefCount === 1) {
|
25
|
+
set(controlledFieldsAtom(formId), (prev) => ({
|
26
|
+
...prev,
|
27
|
+
[field]: fieldValueAtom({ formId, field }),
|
28
|
+
}));
|
29
|
+
}
|
30
|
+
});
|
31
|
+
const unregisterAtom = atom(null, (get, set, { formId, field }) => {
|
32
|
+
set(refCountAtom({ formId, field }), (prev) => prev - 1);
|
33
|
+
const newRefCount = get(refCountAtom({ formId, field }));
|
34
|
+
if (newRefCount === 0) {
|
35
|
+
set(controlledFieldsAtom(formId), (prev) => omit(prev, field));
|
36
|
+
fieldValueAtom.remove({ formId, field });
|
37
|
+
pendingValidateAtom.remove({ formId, field });
|
38
|
+
fieldValueHydratedAtom.remove({ formId, field });
|
39
|
+
}
|
40
|
+
});
|
41
|
+
export const setControlledFieldValueAtom = atom(null, async (_get, set, { formId, field, value, }) => {
|
42
|
+
set(fieldValueAtom({ formId, field }), value);
|
43
|
+
const pending = pendingValidateAtom({ formId, field });
|
44
|
+
await new Promise((resolve) => set(pending, resolve));
|
45
|
+
set(pending, undefined);
|
46
|
+
});
|
47
|
+
export const useAllControlledFields = (formId) => useFormAtomValue(controlledFieldsAtom(formId));
|
48
|
+
export const useControlledFieldValue = (formId, field) => {
|
49
|
+
const fieldAtom = fieldValueAtom({ formId, field });
|
50
|
+
const [value, setValue] = useFormAtom(fieldAtom);
|
51
|
+
const defaultValue = useFieldDefaultValue(field, { formId });
|
52
|
+
const isHydrated = useFormAtomValue(isHydratedAtom(formId));
|
53
|
+
const [isFieldHydrated, setIsFieldHydrated] = useFormAtom(fieldValueHydratedAtom({ formId, field }));
|
54
|
+
useEffect(() => {
|
55
|
+
if (isHydrated && !isFieldHydrated) {
|
56
|
+
setValue(defaultValue);
|
57
|
+
setIsFieldHydrated(true);
|
58
|
+
}
|
59
|
+
}, [
|
60
|
+
defaultValue,
|
61
|
+
field,
|
62
|
+
formId,
|
63
|
+
isFieldHydrated,
|
64
|
+
isHydrated,
|
65
|
+
setIsFieldHydrated,
|
66
|
+
setValue,
|
67
|
+
]);
|
68
|
+
return isFieldHydrated ? value : defaultValue;
|
69
|
+
};
|
70
|
+
export const useControllableValue = (formId, field) => {
|
71
|
+
const pending = useFormAtomValue(pendingValidateAtom({ formId, field }));
|
72
|
+
useEffect(() => {
|
73
|
+
pending === null || pending === void 0 ? void 0 : pending();
|
74
|
+
}, [pending]);
|
75
|
+
const register = useFormUpdateAtom(registerAtom);
|
76
|
+
const unregister = useFormUpdateAtom(unregisterAtom);
|
77
|
+
useEffect(() => {
|
78
|
+
register({ formId, field });
|
79
|
+
return () => unregister({ formId, field });
|
80
|
+
}, [field, formId, register, unregister]);
|
81
|
+
const setControlledFieldValue = useFormUpdateAtom(setControlledFieldValueAtom);
|
82
|
+
const setValue = useCallback((value) => setControlledFieldValue({ formId, field, value }), [field, formId, setControlledFieldValue]);
|
83
|
+
const value = useControlledFieldValue(formId, field);
|
84
|
+
return [value, setValue];
|
85
|
+
};
|
86
|
+
export const useFieldSerializer = (formId, field) => useFormAtomValue(valueSerializerAtom({ formId, field })).serializer;
|
87
|
+
export const useRegisterFieldSerializer = (formId, field, serializer) => {
|
88
|
+
const setSerializer = useFormUpdateAtom(valueSerializerAtom({ formId, field }));
|
89
|
+
useEffect(() => {
|
90
|
+
if (serializer)
|
91
|
+
setSerializer({ serializer });
|
92
|
+
}, [serializer, setSerializer]);
|
93
|
+
};
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
"use strict";
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
package/build/ValidatedForm.js
CHANGED
@@ -212,9 +212,9 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
|
|
212
212
|
clickedButtonRef.current = submitButton;
|
213
213
|
}
|
214
214
|
}
|
215
|
-
window.addEventListener("click", handleClick);
|
215
|
+
window.addEventListener("click", handleClick, { capture: true });
|
216
216
|
return () => {
|
217
|
-
window.removeEventListener("click", handleClick);
|
217
|
+
window.removeEventListener("click", handleClick, { capture: true });
|
218
218
|
};
|
219
219
|
}, []);
|
220
220
|
const handleSubmit = async (e) => {
|
@@ -234,10 +234,15 @@ function ValidatedForm({ validator, onSubmit, children, fetcher, action, default
|
|
234
234
|
endSubmit({ formAtom });
|
235
235
|
return;
|
236
236
|
}
|
237
|
+
// We deviate from the remix code here a bit because of our async submit.
|
238
|
+
// In remix's `FormImpl`, they use `event.currentTarget` to get the form,
|
239
|
+
// but we already have the form in `formRef.current` so we can just use that.
|
240
|
+
// If we use `event.currentTarget` here, it will break because `currentTarget`
|
241
|
+
// will have changed since the start of the submission.
|
237
242
|
if (fetcher)
|
238
|
-
fetcher.submit(clickedButtonRef.current ||
|
243
|
+
fetcher.submit(clickedButtonRef.current || formRef.current);
|
239
244
|
else
|
240
|
-
submit(clickedButtonRef.current ||
|
245
|
+
submit(clickedButtonRef.current || formRef.current, {
|
241
246
|
method,
|
242
247
|
replace,
|
243
248
|
});
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getCheckboxChecked = void 0;
|
4
4
|
const getCheckboxChecked = (checkboxValue = "on", newValue) => {
|
5
5
|
if (Array.isArray(newValue))
|
6
|
-
return newValue.
|
6
|
+
return newValue.some((val) => val === true || val === checkboxValue);
|
7
7
|
if (typeof newValue === "boolean")
|
8
8
|
return newValue;
|
9
9
|
if (typeof newValue === "string")
|
@@ -1 +1 @@
|
|
1
|
-
export declare const setInputValueInForm: (formElement: HTMLFormElement, name: string, value: unknown) => void;
|
1
|
+
export declare const setInputValueInForm: (formElement: HTMLFormElement, name: string, value: unknown[]) => void;
|
@@ -6,42 +6,122 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.setInputValueInForm = void 0;
|
7
7
|
const tiny_invariant_1 = __importDefault(require("tiny-invariant"));
|
8
8
|
const getCheckboxChecked_1 = require("./getCheckboxChecked");
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
/**
|
10
|
+
* Helper class to track the values being set on uncontrolled fields.
|
11
|
+
* HTML is super permissive with inputs that all share the same `name`.
|
12
|
+
*
|
13
|
+
* This class is strict in the sense that, if the user provides an array value,
|
14
|
+
* the values inside the array must be in the same order as the elements in the DOM.
|
15
|
+
* Doing this allows us to be flexible with what types of form controls the user is using.
|
16
|
+
*
|
17
|
+
* This is how HTML tracks inputs of the same name as well.
|
18
|
+
* `new FormData(formElement).getAll('myField')` will return values in DOM order.
|
19
|
+
*/
|
20
|
+
class Values {
|
21
|
+
constructor(values) {
|
22
|
+
this.hasSetRadioValue = false;
|
23
|
+
this.bool = (value) => {
|
24
|
+
if ((0, getCheckboxChecked_1.getCheckboxChecked)(value, this.values[0])) {
|
25
|
+
this.values.shift();
|
26
|
+
return true;
|
27
|
+
}
|
28
|
+
return false;
|
29
|
+
};
|
30
|
+
this.radio = (value) => {
|
31
|
+
if (this.hasSetRadioValue)
|
32
|
+
return false;
|
33
|
+
const result = this.bool(value);
|
34
|
+
if (result)
|
35
|
+
this.hasSetRadioValue = true;
|
36
|
+
return result;
|
37
|
+
};
|
38
|
+
this.str = () => { var _a; return String((_a = this.values.pop()) !== null && _a !== void 0 ? _a : ""); };
|
39
|
+
this.allValues = () => this.values;
|
40
|
+
this.warnIfLeftovers = (field) => {
|
41
|
+
if (this.values.length > 0) {
|
42
|
+
console.warn(`Could not determine how to use the value for the field ${field}. ` +
|
43
|
+
`Leftover values were: ${this.values.join(", ")}.`);
|
44
|
+
}
|
45
|
+
};
|
46
|
+
const unknownValues = Array.isArray(values) ? values : [values];
|
47
|
+
this.values = unknownValues;
|
48
|
+
}
|
49
|
+
}
|
50
|
+
/**
|
51
|
+
* This subclass is order-permissive, meaning the user doesn't have to worry about
|
52
|
+
* the order in which the inputs occur in the DOM.
|
53
|
+
* This is useful for multiselects and checkbox groups and provides a better DX than
|
54
|
+
* the order-strict version.
|
55
|
+
*/
|
56
|
+
class PermissiveValues extends Values {
|
57
|
+
constructor() {
|
58
|
+
super(...arguments);
|
59
|
+
this.remove = (value) => {
|
60
|
+
const index = this.values.indexOf(value);
|
61
|
+
const deleted = this.values.splice(index, 1);
|
62
|
+
return deleted.length > 0;
|
63
|
+
};
|
64
|
+
this.bool = (value) => {
|
65
|
+
if ((0, getCheckboxChecked_1.getCheckboxChecked)(value, this.values)) {
|
66
|
+
this.remove(value) || this.remove(true);
|
67
|
+
return true;
|
68
|
+
}
|
69
|
+
return false;
|
70
|
+
};
|
71
|
+
}
|
72
|
+
}
|
73
|
+
const isMultiselect = (node) => node instanceof HTMLSelectElement && node.multiple;
|
74
|
+
const isCheckbox = (node) => node instanceof HTMLInputElement && node.type === "checkbox";
|
75
|
+
const isRadio = (node) => node instanceof HTMLInputElement && node.type === "radio";
|
76
|
+
const setElementValue = (element, values, field) => {
|
77
|
+
if (isMultiselect(element)) {
|
13
78
|
for (const option of element.options) {
|
14
|
-
option.selected =
|
79
|
+
option.selected = values.bool(option.value);
|
15
80
|
}
|
16
81
|
return;
|
17
82
|
}
|
18
|
-
if (element
|
19
|
-
|
20
|
-
(0, tiny_invariant_1.default)(newChecked !== undefined, `Unable to determine if checkbox should be checked. Provided value was ${value} for checkbox ${name}.`);
|
21
|
-
element.checked = newChecked;
|
83
|
+
if (isCheckbox(element)) {
|
84
|
+
element.checked = values.bool(element.value);
|
22
85
|
return;
|
23
86
|
}
|
24
|
-
if (element
|
25
|
-
|
26
|
-
(0, tiny_invariant_1.default)(newChecked !== undefined, `Unable to determine if radio should be checked. Provided value was ${value} for radio ${name}.`);
|
27
|
-
element.checked = newChecked;
|
87
|
+
if (isRadio(element)) {
|
88
|
+
element.checked = values.radio(element.value);
|
28
89
|
return;
|
29
90
|
}
|
30
|
-
(0, tiny_invariant_1.default)(typeof value === "string", `Invalid value for field "${name}" which is an ${element.constructor.name}. Expected string but received ${typeof value}`);
|
31
91
|
const input = element;
|
32
|
-
input.value
|
92
|
+
(0, tiny_invariant_1.default)(input.type !== "hidden", `Cannot set value on hidden input if it is not a controlled field. Field being updated was ${field}.`);
|
93
|
+
input.value = values.str();
|
94
|
+
};
|
95
|
+
const areElementsTheSameType = (nodes) => {
|
96
|
+
const getType = (node) => {
|
97
|
+
if (node instanceof HTMLInputElement)
|
98
|
+
return node.type;
|
99
|
+
if (node instanceof HTMLSelectElement)
|
100
|
+
return node.multiple ? "select" : "multiselect";
|
101
|
+
return null;
|
102
|
+
};
|
103
|
+
const firstElementInstance = nodes[0].constructor;
|
104
|
+
const firstElementType = getType(nodes[0]);
|
105
|
+
return nodes.every((element) => element.constructor === firstElementInstance &&
|
106
|
+
getType(element) === firstElementType);
|
33
107
|
};
|
34
108
|
const setInputValueInForm = (formElement, name, value) => {
|
35
109
|
const controlElement = formElement.elements.namedItem(name);
|
36
110
|
if (!controlElement)
|
37
111
|
return;
|
38
112
|
if (controlElement instanceof RadioNodeList) {
|
113
|
+
const values = areElementsTheSameType([...controlElement])
|
114
|
+
? new PermissiveValues(value)
|
115
|
+
: new Values(value);
|
39
116
|
for (const element of controlElement) {
|
40
|
-
setElementValue(element,
|
117
|
+
setElementValue(element, values, name);
|
41
118
|
}
|
119
|
+
values.warnIfLeftovers(name);
|
42
120
|
}
|
43
121
|
else {
|
44
|
-
|
122
|
+
const values = new PermissiveValues(value);
|
123
|
+
setElementValue(controlElement, values, name);
|
124
|
+
values.warnIfLeftovers(name);
|
45
125
|
}
|
46
126
|
};
|
47
127
|
exports.setInputValueInForm = setInputValueInForm;
|
@@ -0,0 +1,33 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.useSetFormValues = void 0;
|
7
|
+
const utils_1 = require("jotai/utils");
|
8
|
+
const react_1 = require("react");
|
9
|
+
const tiny_invariant_1 = __importDefault(require("tiny-invariant"));
|
10
|
+
const setInputValueInForm_1 = require("./logic/setInputValueInForm");
|
11
|
+
const state_1 = require("./state");
|
12
|
+
const controlledFields_1 = require("./state/controlledFields");
|
13
|
+
const useSetFormValues = (formId) => (0, utils_1.useAtomCallback)((0, react_1.useCallback)(async (get, set, updatedValues) => {
|
14
|
+
const form = get((0, state_1.formElementAtom)(formId));
|
15
|
+
(0, tiny_invariant_1.default)(form, "Unable to access form element when setting field value. This is likely a bug in remix-validated-form.");
|
16
|
+
const controlledFields = get((0, controlledFields_1.controlledFieldsAtom)(formId));
|
17
|
+
const updatePromises = [];
|
18
|
+
for (const [field, value] of Object.entries(updatedValues)) {
|
19
|
+
const isControlled = !!controlledFields[field];
|
20
|
+
if (isControlled) {
|
21
|
+
updatePromises.push(set(controlledFields_1.setControlledFieldValueAtom, {
|
22
|
+
field,
|
23
|
+
formId,
|
24
|
+
value,
|
25
|
+
}));
|
26
|
+
}
|
27
|
+
else {
|
28
|
+
(0, setInputValueInForm_1.setInputValueInForm)(form, field, Array.isArray(value) ? value : [value]);
|
29
|
+
}
|
30
|
+
}
|
31
|
+
await Promise.all(updatePromises);
|
32
|
+
}, [formId]), state_1.ATOM_SCOPE);
|
33
|
+
exports.useSetFormValues = useSetFormValues;
|
@@ -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
|
+
};
|
@@ -0,0 +1,13 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.fieldAtomFamily = exports.formAtomFamily = void 0;
|
7
|
+
const jotai_1 = require("jotai");
|
8
|
+
const utils_1 = require("jotai/utils");
|
9
|
+
const isEqual_1 = __importDefault(require("lodash/isEqual"));
|
10
|
+
const formAtomFamily = (data) => (0, utils_1.atomFamily)((_) => (0, jotai_1.atom)(data));
|
11
|
+
exports.formAtomFamily = formAtomFamily;
|
12
|
+
const fieldAtomFamily = (func) => (0, utils_1.atomFamily)(func, isEqual_1.default);
|
13
|
+
exports.fieldAtomFamily = fieldAtomFamily;
|
@@ -0,0 +1,66 @@
|
|
1
|
+
import { PrimitiveAtom } from "jotai";
|
2
|
+
import { InternalFormId } from "./atomUtils";
|
3
|
+
export declare type ValueSerializer = (val: unknown) => string;
|
4
|
+
export declare const controlledFieldsAtom: {
|
5
|
+
(param: InternalFormId): import("jotai").Atom<Record<string, PrimitiveAtom<unknown>>> & {
|
6
|
+
write: (get: {
|
7
|
+
<Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
|
8
|
+
<Value_1>(atom: import("jotai").Atom<Promise<Value_1>>): Value_1;
|
9
|
+
<Value_2>(atom: import("jotai").Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
|
10
|
+
} & {
|
11
|
+
<Value_3>(atom: import("jotai").Atom<Value_3 | Promise<Value_3>>, options: {
|
12
|
+
unstable_promise: true;
|
13
|
+
}): Value_3 | Promise<Value_3>;
|
14
|
+
<Value_4>(atom: import("jotai").Atom<Promise<Value_4>>, options: {
|
15
|
+
unstable_promise: true;
|
16
|
+
}): Value_4 | Promise<Value_4>;
|
17
|
+
<Value_5>(atom: import("jotai").Atom<Value_5>, options: {
|
18
|
+
unstable_promise: true;
|
19
|
+
}): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
|
20
|
+
}, set: {
|
21
|
+
<Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
|
22
|
+
<Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
|
23
|
+
}, update: Record<string, PrimitiveAtom<unknown>> | ((prev: Record<string, PrimitiveAtom<unknown>>) => Record<string, PrimitiveAtom<unknown>>)) => void;
|
24
|
+
onMount?: (<S extends (update: Record<string, PrimitiveAtom<unknown>> | ((prev: Record<string, PrimitiveAtom<unknown>>) => Record<string, PrimitiveAtom<unknown>>)) => void>(setAtom: S) => void | (() => void)) | undefined;
|
25
|
+
} & {
|
26
|
+
init: Record<string, PrimitiveAtom<unknown>>;
|
27
|
+
};
|
28
|
+
remove(param: InternalFormId): void;
|
29
|
+
setShouldRemove(shouldRemove: ((createdAt: number, param: InternalFormId) => boolean) | null): void;
|
30
|
+
};
|
31
|
+
export declare const setControlledFieldValueAtom: import("jotai").Atom<null> & {
|
32
|
+
write: (get: {
|
33
|
+
<Value>(atom: import("jotai").Atom<Value | Promise<Value>>): Value;
|
34
|
+
<Value_1>(atom: import("jotai").Atom<Promise<Value_1>>): Value_1;
|
35
|
+
<Value_2>(atom: import("jotai").Atom<Value_2>): Value_2 extends Promise<infer V> ? V : Value_2;
|
36
|
+
} & {
|
37
|
+
<Value_3>(atom: import("jotai").Atom<Value_3 | Promise<Value_3>>, options: {
|
38
|
+
unstable_promise: true;
|
39
|
+
}): Value_3 | Promise<Value_3>;
|
40
|
+
<Value_4>(atom: import("jotai").Atom<Promise<Value_4>>, options: {
|
41
|
+
unstable_promise: true;
|
42
|
+
}): Value_4 | Promise<Value_4>;
|
43
|
+
<Value_5>(atom: import("jotai").Atom<Value_5>, options: {
|
44
|
+
unstable_promise: true;
|
45
|
+
}): (Value_5 extends Promise<infer V> ? V : Value_5) | Promise<Value_5 extends Promise<infer V> ? V : Value_5>;
|
46
|
+
}, set: {
|
47
|
+
<Value_6, Result extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_6, undefined, Result>): Result;
|
48
|
+
<Value_7, Update, Result_1 extends void | Promise<void>>(atom: import("jotai").WritableAtom<Value_7, Update, Result_1>, update: Update): Result_1;
|
49
|
+
}, update: {
|
50
|
+
formId: InternalFormId;
|
51
|
+
field: string;
|
52
|
+
value: unknown;
|
53
|
+
}) => Promise<void>;
|
54
|
+
onMount?: (<S extends (update: {
|
55
|
+
formId: InternalFormId;
|
56
|
+
field: string;
|
57
|
+
value: unknown;
|
58
|
+
}) => Promise<void>>(setAtom: S) => void | (() => void)) | undefined;
|
59
|
+
} & {
|
60
|
+
init: null;
|
61
|
+
};
|
62
|
+
export declare const useAllControlledFields: (formId: InternalFormId) => Record<string, PrimitiveAtom<unknown>>;
|
63
|
+
export declare const useControlledFieldValue: (formId: InternalFormId, field: string) => any;
|
64
|
+
export declare const useControllableValue: (formId: InternalFormId, field: string) => readonly [any, (value: unknown) => Promise<void>];
|
65
|
+
export declare const useFieldSerializer: (formId: InternalFormId, field: string) => ValueSerializer;
|
66
|
+
export declare const useRegisterFieldSerializer: (formId: InternalFormId, field: string, serializer?: ValueSerializer | undefined) => void;
|
@@ -0,0 +1,95 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
+
};
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
+
exports.useRegisterFieldSerializer = exports.useFieldSerializer = exports.useControllableValue = exports.useControlledFieldValue = exports.useAllControlledFields = exports.setControlledFieldValueAtom = exports.controlledFieldsAtom = void 0;
|
7
|
+
const jotai_1 = require("jotai");
|
8
|
+
const omit_1 = __importDefault(require("lodash/omit"));
|
9
|
+
const react_1 = require("react");
|
10
|
+
const hooks_1 = require("../hooks");
|
11
|
+
const state_1 = require("../state");
|
12
|
+
const atomUtils_1 = require("./atomUtils");
|
13
|
+
exports.controlledFieldsAtom = (0, atomUtils_1.formAtomFamily)({});
|
14
|
+
const refCountAtom = (0, atomUtils_1.fieldAtomFamily)(() => (0, jotai_1.atom)(0));
|
15
|
+
const fieldValueAtom = (0, atomUtils_1.fieldAtomFamily)(() => (0, jotai_1.atom)(undefined));
|
16
|
+
const fieldValueHydratedAtom = (0, atomUtils_1.fieldAtomFamily)(() => (0, jotai_1.atom)(false));
|
17
|
+
const pendingValidateAtom = (0, atomUtils_1.fieldAtomFamily)(() => (0, jotai_1.atom)(undefined));
|
18
|
+
const valueSerializerAtom = (0, atomUtils_1.fieldAtomFamily)(() => (0, jotai_1.atom)({ serializer: JSON.stringify }));
|
19
|
+
const registerAtom = (0, jotai_1.atom)(null, (get, set, { formId, field }) => {
|
20
|
+
set(refCountAtom({ formId, field }), (prev) => prev + 1);
|
21
|
+
const newRefCount = get(refCountAtom({ formId, field }));
|
22
|
+
if (newRefCount === 1) {
|
23
|
+
set((0, exports.controlledFieldsAtom)(formId), (prev) => ({
|
24
|
+
...prev,
|
25
|
+
[field]: fieldValueAtom({ formId, field }),
|
26
|
+
}));
|
27
|
+
}
|
28
|
+
});
|
29
|
+
const unregisterAtom = (0, jotai_1.atom)(null, (get, set, { formId, field }) => {
|
30
|
+
set(refCountAtom({ formId, field }), (prev) => prev - 1);
|
31
|
+
const newRefCount = get(refCountAtom({ formId, field }));
|
32
|
+
if (newRefCount === 0) {
|
33
|
+
set((0, exports.controlledFieldsAtom)(formId), (prev) => (0, omit_1.default)(prev, field));
|
34
|
+
fieldValueAtom.remove({ formId, field });
|
35
|
+
pendingValidateAtom.remove({ formId, field });
|
36
|
+
}
|
37
|
+
});
|
38
|
+
exports.setControlledFieldValueAtom = (0, jotai_1.atom)(null, async (_get, set, { formId, field, value, }) => {
|
39
|
+
set(fieldValueAtom({ formId, field }), value);
|
40
|
+
const pending = pendingValidateAtom({ formId, field });
|
41
|
+
await new Promise((resolve) => set(pending, resolve));
|
42
|
+
set(pending, undefined);
|
43
|
+
});
|
44
|
+
const useAllControlledFields = (formId) => (0, hooks_1.useFormAtomValue)((0, exports.controlledFieldsAtom)(formId));
|
45
|
+
exports.useAllControlledFields = useAllControlledFields;
|
46
|
+
const useControlledFieldValue = (formId, field) => {
|
47
|
+
const fieldAtom = fieldValueAtom({ formId, field });
|
48
|
+
const [value, setValue] = (0, hooks_1.useFormAtom)(fieldAtom);
|
49
|
+
const defaultValue = (0, hooks_1.useFieldDefaultValue)(field, { formId });
|
50
|
+
const isHydrated = (0, hooks_1.useFormAtomValue)((0, state_1.isHydratedAtom)(formId));
|
51
|
+
const [isFieldHydrated, setIsFieldHydrated] = (0, hooks_1.useFormAtom)(fieldValueHydratedAtom({ formId, field }));
|
52
|
+
(0, react_1.useEffect)(() => {
|
53
|
+
if (isHydrated && !isFieldHydrated) {
|
54
|
+
setValue({ formId, field, value: defaultValue });
|
55
|
+
setIsFieldHydrated(true);
|
56
|
+
}
|
57
|
+
}, [
|
58
|
+
defaultValue,
|
59
|
+
field,
|
60
|
+
formId,
|
61
|
+
isFieldHydrated,
|
62
|
+
isHydrated,
|
63
|
+
setIsFieldHydrated,
|
64
|
+
setValue,
|
65
|
+
]);
|
66
|
+
return isFieldHydrated ? value : defaultValue;
|
67
|
+
};
|
68
|
+
exports.useControlledFieldValue = useControlledFieldValue;
|
69
|
+
const useControllableValue = (formId, field) => {
|
70
|
+
const pending = (0, hooks_1.useFormAtomValue)(pendingValidateAtom({ formId, field }));
|
71
|
+
(0, react_1.useEffect)(() => {
|
72
|
+
pending === null || pending === void 0 ? void 0 : pending();
|
73
|
+
}, [pending]);
|
74
|
+
const register = (0, hooks_1.useFormUpdateAtom)(registerAtom);
|
75
|
+
const unregister = (0, hooks_1.useFormUpdateAtom)(unregisterAtom);
|
76
|
+
(0, react_1.useEffect)(() => {
|
77
|
+
register({ formId, field });
|
78
|
+
return () => unregister({ formId, field });
|
79
|
+
}, [field, formId, register, unregister]);
|
80
|
+
const setControlledFieldValue = (0, hooks_1.useFormUpdateAtom)(exports.setControlledFieldValueAtom);
|
81
|
+
const setValue = (0, react_1.useCallback)((value) => setControlledFieldValue({ formId, field, value }), [field, formId, setControlledFieldValue]);
|
82
|
+
const value = (0, exports.useControlledFieldValue)(formId, field);
|
83
|
+
return [value, setValue];
|
84
|
+
};
|
85
|
+
exports.useControllableValue = useControllableValue;
|
86
|
+
const useFieldSerializer = (formId, field) => (0, hooks_1.useFormAtomValue)(valueSerializerAtom({ formId, field })).serializer;
|
87
|
+
exports.useFieldSerializer = useFieldSerializer;
|
88
|
+
const useRegisterFieldSerializer = (formId, field, serializer) => {
|
89
|
+
const setSerializer = (0, hooks_1.useFormUpdateAtom)(valueSerializerAtom({ formId, field }));
|
90
|
+
(0, react_1.useEffect)(() => {
|
91
|
+
if (serializer)
|
92
|
+
setSerializer({ serializer });
|
93
|
+
}, [serializer, setSerializer]);
|
94
|
+
};
|
95
|
+
exports.useRegisterFieldSerializer = useRegisterFieldSerializer;
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "remix-validated-form",
|
3
|
-
"version": "4.1.
|
3
|
+
"version": "4.1.3",
|
4
4
|
"description": "Form component and utils for easy form validation in remix",
|
5
5
|
"browser": "./browser/index.js",
|
6
6
|
"main": "./build/index.js",
|
@@ -33,13 +33,13 @@
|
|
33
33
|
"validation"
|
34
34
|
],
|
35
35
|
"peerDependencies": {
|
36
|
-
"@remix-run/react": "
|
37
|
-
"@remix-run/server-runtime": "
|
36
|
+
"@remix-run/react": ">1.x",
|
37
|
+
"@remix-run/server-runtime": ">1.x",
|
38
38
|
"react": "^17.0.2"
|
39
39
|
},
|
40
40
|
"devDependencies": {
|
41
|
-
"@remix-run/react": "^1.
|
42
|
-
"@remix-run/server-runtime": "^1.
|
41
|
+
"@remix-run/react": "^1.2.1",
|
42
|
+
"@remix-run/server-runtime": "^1.2.1",
|
43
43
|
"@types/lodash": "^4.14.178",
|
44
44
|
"@types/react": "^17.0.37",
|
45
45
|
"fetch-blob": "^3.1.3",
|
package/src/ValidatedForm.tsx
CHANGED
@@ -327,9 +327,9 @@ export function ValidatedForm<DataType>({
|
|
327
327
|
}
|
328
328
|
}
|
329
329
|
|
330
|
-
window.addEventListener("click", handleClick);
|
330
|
+
window.addEventListener("click", handleClick, { capture: true });
|
331
331
|
return () => {
|
332
|
-
window.removeEventListener("click", handleClick);
|
332
|
+
window.removeEventListener("click", handleClick, { capture: true });
|
333
333
|
};
|
334
334
|
}, []);
|
335
335
|
|
@@ -354,12 +354,18 @@ export function ValidatedForm<DataType>({
|
|
354
354
|
return;
|
355
355
|
}
|
356
356
|
|
357
|
-
|
357
|
+
// We deviate from the remix code here a bit because of our async submit.
|
358
|
+
// In remix's `FormImpl`, they use `event.currentTarget` to get the form,
|
359
|
+
// but we already have the form in `formRef.current` so we can just use that.
|
360
|
+
// If we use `event.currentTarget` here, it will break because `currentTarget`
|
361
|
+
// will have changed since the start of the submission.
|
362
|
+
if (fetcher) fetcher.submit(clickedButtonRef.current || formRef.current);
|
358
363
|
else
|
359
|
-
submit(clickedButtonRef.current ||
|
364
|
+
submit(clickedButtonRef.current || formRef.current, {
|
360
365
|
method,
|
361
366
|
replace,
|
362
367
|
});
|
368
|
+
|
363
369
|
clickedButtonRef.current = null;
|
364
370
|
}
|
365
371
|
};
|