remix-validated-form 4.2.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +15 -9
- package/README.md +1 -0
- package/browser/ValidatedForm.js +16 -26
- package/browser/hooks.d.ts +2 -0
- package/browser/hooks.js +20 -9
- package/browser/internal/MultiValueMap.d.ts +2 -0
- package/browser/internal/MultiValueMap.js +4 -0
- package/browser/internal/getInputProps.js +2 -1
- package/browser/internal/hooks.d.ts +20 -9
- package/browser/internal/hooks.js +32 -23
- package/browser/internal/logic/getRadioChecked.js +10 -0
- package/browser/internal/reset.d.ts +28 -0
- package/browser/internal/reset.js +13 -0
- package/browser/internal/state/cleanup.d.ts +2 -0
- package/browser/internal/state/cleanup.js +6 -0
- package/browser/internal/state/controlledFieldStore.d.ts +24 -0
- package/browser/internal/state/controlledFieldStore.js +57 -0
- package/browser/internal/state/controlledFields.d.ts +6 -62
- package/browser/internal/state/controlledFields.js +36 -63
- package/browser/internal/state/createFormStore.d.ts +40 -0
- package/browser/internal/state/createFormStore.js +83 -0
- package/browser/internal/state/storeFamily.d.ts +9 -0
- package/browser/internal/state/storeFamily.js +18 -0
- package/browser/internal/state/storeHooks.d.ts +5 -0
- package/browser/internal/state/storeHooks.js +10 -0
- package/browser/internal/state.d.ts +0 -27
- package/browser/internal/state.js +0 -5
- package/browser/unreleased/formStateHooks.d.ts +15 -0
- package/browser/unreleased/formStateHooks.js +23 -14
- package/browser/userFacingFormContext.d.ts +8 -0
- package/browser/userFacingFormContext.js +5 -4
- package/dist/remix-validated-form.cjs.js +17 -0
- package/dist/remix-validated-form.es.js +2844 -0
- package/dist/remix-validated-form.umd.js +17 -0
- package/{build → dist/types}/ValidatedForm.d.ts +0 -0
- package/{build → dist/types}/hooks.d.ts +2 -0
- package/{build → dist/types}/index.d.ts +0 -0
- package/{build → dist/types}/internal/MultiValueMap.d.ts +2 -0
- package/{build → dist/types}/internal/constants.d.ts +0 -0
- package/{build → dist/types}/internal/flatten.d.ts +0 -0
- package/{build → dist/types}/internal/formContext.d.ts +0 -0
- package/{build → dist/types}/internal/getInputProps.d.ts +0 -0
- package/dist/types/internal/hooks.d.ts +32 -0
- package/{build → dist/types}/internal/hydratable.d.ts +0 -0
- package/{build → dist/types}/internal/logic/getCheckboxChecked.d.ts +0 -0
- package/{build → dist/types}/internal/logic/getRadioChecked.d.ts +0 -0
- package/dist/types/internal/state/cleanup.d.ts +2 -0
- package/dist/types/internal/state/controlledFieldStore.d.ts +24 -0
- package/dist/types/internal/state/controlledFields.d.ts +6 -0
- package/dist/types/internal/state/createFormStore.d.ts +40 -0
- package/dist/types/internal/state/storeFamily.d.ts +9 -0
- package/dist/types/internal/state/storeHooks.d.ts +5 -0
- package/{build → dist/types}/internal/submissionCallbacks.d.ts +0 -0
- package/{build → dist/types}/internal/util.d.ts +0 -0
- package/{build → dist/types}/server.d.ts +0 -0
- package/{build → dist/types}/unreleased/formStateHooks.d.ts +15 -0
- package/{build → dist/types}/userFacingFormContext.d.ts +8 -0
- package/{build → dist/types}/validation/createValidator.d.ts +0 -0
- package/{build → dist/types}/validation/types.d.ts +0 -0
- package/package.json +11 -9
- package/src/ValidatedForm.tsx +25 -43
- package/src/hooks.ts +29 -17
- package/src/internal/MultiValueMap.ts +6 -0
- package/src/internal/getInputProps.test.ts +251 -0
- package/src/internal/getInputProps.ts +2 -1
- package/src/internal/hooks.ts +69 -45
- package/src/internal/logic/getRadioChecked.ts +11 -0
- package/src/internal/state/cleanup.ts +8 -0
- package/src/internal/state/controlledFieldStore.ts +91 -0
- package/src/internal/state/controlledFields.ts +78 -0
- package/src/internal/state/createFormStore.ts +152 -0
- package/src/internal/state/storeFamily.ts +24 -0
- package/src/internal/state/storeHooks.ts +22 -0
- package/src/unreleased/formStateHooks.ts +50 -27
- package/src/userFacingFormContext.ts +17 -5
- package/src/validation/validation.test.ts +304 -0
- package/tsconfig.json +4 -1
- package/vite.config.ts +7 -0
- package/.turbo/turbo-test.log +0 -11
- package/browser/components.d.ts +0 -7
- package/browser/components.js +0 -10
- package/browser/internal/SingleTypeMultiValueMap.d.ts +0 -9
- package/browser/internal/SingleTypeMultiValueMap.js +0 -41
- package/browser/internal/customState.d.ts +0 -105
- package/browser/internal/customState.js +0 -46
- package/browser/internal/hooks-valtio.d.ts +0 -18
- package/browser/internal/hooks-valtio.js +0 -110
- package/browser/internal/hooks-zustand.d.ts +0 -16
- package/browser/internal/hooks-zustand.js +0 -100
- package/browser/internal/immerMiddleware.d.ts +0 -6
- package/browser/internal/immerMiddleware.js +0 -7
- package/browser/internal/logic/elementUtils.d.ts +0 -3
- package/browser/internal/logic/elementUtils.js +0 -3
- package/browser/internal/logic/getCheckboxChecked copy.d.ts +0 -1
- package/browser/internal/logic/getCheckboxChecked copy.js +0 -9
- package/browser/internal/logic/setFieldValue.d.ts +0 -1
- package/browser/internal/logic/setFieldValue.js +0 -40
- package/browser/internal/logic/setInputValueInForm.d.ts +0 -1
- package/browser/internal/logic/setInputValueInForm.js +0 -77
- package/browser/internal/setFieldValue.d.ts +0 -20
- package/browser/internal/setFieldValue.js +0 -83
- package/browser/internal/setFormValues.d.ts +0 -2
- package/browser/internal/setFormValues.js +0 -26
- package/browser/internal/state/setFieldValue.d.ts +0 -0
- package/browser/internal/state/setFieldValue.js +0 -1
- package/browser/internal/state-valtio.d.ts +0 -62
- package/browser/internal/state-valtio.js +0 -69
- package/browser/internal/state-zustand.d.ts +0 -47
- package/browser/internal/state-zustand.js +0 -85
- package/browser/internal/test.d.ts +0 -0
- package/browser/internal/test.js +0 -15
- package/browser/internal/useMultiValueMap.d.ts +0 -1
- package/browser/internal/useMultiValueMap.js +0 -11
- package/browser/internal/watch.d.ts +0 -18
- package/browser/internal/watch.js +0 -122
- package/browser/lowLevelHooks.d.ts +0 -0
- package/browser/lowLevelHooks.js +0 -1
- package/browser/test-data/testFormData.d.ts +0 -15
- package/browser/test-data/testFormData.js +0 -46
- package/browser/types.d.ts +0 -1
- package/browser/types.js +0 -1
- package/browser/validation/validation.test.d.ts +0 -1
- package/browser/validation/validation.test.js +0 -274
- package/browser/validation/withYup.d.ts +0 -6
- package/browser/validation/withYup.js +0 -40
- package/browser/validation/withZod.d.ts +0 -6
- package/browser/validation/withZod.js +0 -50
- package/build/ValidatedForm.js +0 -257
- package/build/hooks.js +0 -79
- package/build/index.js +0 -18
- package/build/internal/MultiValueMap.js +0 -44
- package/build/internal/SingleTypeMultiValueMap.d.ts +0 -8
- package/build/internal/SingleTypeMultiValueMap.js +0 -45
- package/build/internal/constants.js +0 -7
- package/build/internal/flatten.js +0 -14
- package/build/internal/formContext.js +0 -5
- package/build/internal/getInputProps.js +0 -57
- package/build/internal/hooks-valtio.d.ts +0 -18
- package/build/internal/hooks-valtio.js +0 -128
- package/build/internal/hooks-zustand.d.ts +0 -16
- package/build/internal/hooks-zustand.js +0 -117
- package/build/internal/hooks.d.ts +0 -21
- package/build/internal/hooks.js +0 -128
- package/build/internal/hydratable.js +0 -17
- package/build/internal/immerMiddleware.d.ts +0 -6
- package/build/internal/immerMiddleware.js +0 -14
- package/build/internal/logic/elementUtils.d.ts +0 -3
- package/build/internal/logic/elementUtils.js +0 -9
- package/build/internal/logic/getCheckboxChecked.js +0 -13
- package/build/internal/logic/getRadioChecked.js +0 -9
- package/build/internal/logic/setFieldValue.d.ts +0 -1
- package/build/internal/logic/setFieldValue.js +0 -47
- package/build/internal/logic/setInputValueInForm.d.ts +0 -1
- package/build/internal/logic/setInputValueInForm.js +0 -84
- package/build/internal/setFormValues.d.ts +0 -2
- package/build/internal/setFormValues.js +0 -33
- package/build/internal/state/atomUtils.d.ts +0 -38
- package/build/internal/state/atomUtils.js +0 -13
- package/build/internal/state/controlledFields.d.ts +0 -62
- package/build/internal/state/controlledFields.js +0 -85
- package/build/internal/state-valtio.d.ts +0 -62
- package/build/internal/state-valtio.js +0 -83
- package/build/internal/state-zustand.d.ts +0 -47
- package/build/internal/state-zustand.js +0 -91
- package/build/internal/state.d.ts +0 -370
- package/build/internal/state.js +0 -76
- package/build/internal/submissionCallbacks.js +0 -17
- package/build/internal/test.d.ts +0 -1
- package/build/internal/test.js +0 -12
- package/build/internal/util.js +0 -41
- package/build/internal/watch.d.ts +0 -20
- package/build/internal/watch.js +0 -126
- package/build/server.js +0 -32
- package/build/types.d.ts +0 -1
- package/build/types.js +0 -2
- package/build/unreleased/formStateHooks.js +0 -59
- package/build/userFacingFormContext.js +0 -30
- package/build/validation/createValidator.js +0 -45
- package/build/validation/types.js +0 -2
- package/src/internal/state/atomUtils.ts +0 -13
- package/src/internal/state.ts +0 -132
@@ -0,0 +1,251 @@
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
2
|
+
import {
|
3
|
+
createGetInputProps,
|
4
|
+
CreateGetInputPropsOptions,
|
5
|
+
} from "./getInputProps";
|
6
|
+
|
7
|
+
const fakeEvent = { fake: "event" } as any;
|
8
|
+
|
9
|
+
describe("getInputProps", () => {
|
10
|
+
describe("initial", () => {
|
11
|
+
it("should validate on blur by default", () => {
|
12
|
+
const options: CreateGetInputPropsOptions = {
|
13
|
+
name: "some-field",
|
14
|
+
defaultValue: "test default value",
|
15
|
+
touched: false,
|
16
|
+
hasBeenSubmitted: false,
|
17
|
+
setTouched: vi.fn(),
|
18
|
+
clearError: vi.fn(),
|
19
|
+
validate: vi.fn(),
|
20
|
+
};
|
21
|
+
const getInputProps = createGetInputProps(options);
|
22
|
+
|
23
|
+
const provided = {
|
24
|
+
onBlur: vi.fn(),
|
25
|
+
onChange: vi.fn(),
|
26
|
+
};
|
27
|
+
const { onChange, onBlur } = getInputProps(provided);
|
28
|
+
|
29
|
+
onChange!(fakeEvent);
|
30
|
+
expect(provided.onChange).toBeCalledTimes(1);
|
31
|
+
expect(provided.onChange).toBeCalledWith(fakeEvent);
|
32
|
+
expect(options.setTouched).not.toBeCalled();
|
33
|
+
expect(options.validate).not.toBeCalled();
|
34
|
+
|
35
|
+
onBlur!(fakeEvent);
|
36
|
+
expect(provided.onBlur).toBeCalledTimes(1);
|
37
|
+
expect(provided.onBlur).toBeCalledWith(fakeEvent);
|
38
|
+
expect(options.setTouched).toBeCalledTimes(1);
|
39
|
+
expect(options.setTouched).toBeCalledWith(true);
|
40
|
+
expect(options.validate).toBeCalledTimes(1);
|
41
|
+
});
|
42
|
+
|
43
|
+
it("should respect provided validation behavior", () => {
|
44
|
+
const options: CreateGetInputPropsOptions = {
|
45
|
+
name: "some-field",
|
46
|
+
defaultValue: "test default value",
|
47
|
+
touched: false,
|
48
|
+
hasBeenSubmitted: false,
|
49
|
+
setTouched: vi.fn(),
|
50
|
+
clearError: vi.fn(),
|
51
|
+
validate: vi.fn(),
|
52
|
+
validationBehavior: {
|
53
|
+
initial: "onChange",
|
54
|
+
},
|
55
|
+
};
|
56
|
+
const getInputProps = createGetInputProps(options);
|
57
|
+
|
58
|
+
const provided = {
|
59
|
+
onBlur: vi.fn(),
|
60
|
+
onChange: vi.fn(),
|
61
|
+
};
|
62
|
+
const { onChange, onBlur } = getInputProps(provided);
|
63
|
+
|
64
|
+
onChange!(fakeEvent);
|
65
|
+
expect(provided.onChange).toBeCalledTimes(1);
|
66
|
+
expect(provided.onChange).toBeCalledWith(fakeEvent);
|
67
|
+
expect(options.setTouched).not.toBeCalled();
|
68
|
+
expect(options.validate).toBeCalledTimes(1);
|
69
|
+
|
70
|
+
onBlur!(fakeEvent);
|
71
|
+
expect(provided.onBlur).toBeCalledTimes(1);
|
72
|
+
expect(provided.onBlur).toBeCalledWith(fakeEvent);
|
73
|
+
expect(options.setTouched).toBeCalledTimes(1);
|
74
|
+
expect(options.setTouched).toBeCalledWith(true);
|
75
|
+
expect(options.validate).toBeCalledTimes(1);
|
76
|
+
});
|
77
|
+
|
78
|
+
it("should not validate when behavior is onSubmit", () => {
|
79
|
+
const options: CreateGetInputPropsOptions = {
|
80
|
+
name: "some-field",
|
81
|
+
defaultValue: "test default value",
|
82
|
+
touched: false,
|
83
|
+
hasBeenSubmitted: false,
|
84
|
+
setTouched: vi.fn(),
|
85
|
+
clearError: vi.fn(),
|
86
|
+
validate: vi.fn(),
|
87
|
+
validationBehavior: {
|
88
|
+
initial: "onSubmit",
|
89
|
+
},
|
90
|
+
};
|
91
|
+
const getInputProps = createGetInputProps(options);
|
92
|
+
|
93
|
+
const provided = {
|
94
|
+
onBlur: vi.fn(),
|
95
|
+
onChange: vi.fn(),
|
96
|
+
};
|
97
|
+
const { onChange, onBlur } = getInputProps(provided);
|
98
|
+
|
99
|
+
onChange!(fakeEvent);
|
100
|
+
expect(provided.onChange).toBeCalledTimes(1);
|
101
|
+
expect(provided.onChange).toBeCalledWith(fakeEvent);
|
102
|
+
expect(options.setTouched).not.toBeCalled();
|
103
|
+
expect(options.validate).not.toBeCalled();
|
104
|
+
|
105
|
+
onBlur!(fakeEvent);
|
106
|
+
expect(provided.onBlur).toBeCalledTimes(1);
|
107
|
+
expect(provided.onBlur).toBeCalledWith(fakeEvent);
|
108
|
+
expect(options.setTouched).toBeCalledTimes(1);
|
109
|
+
expect(options.setTouched).toBeCalledWith(true);
|
110
|
+
expect(options.validate).not.toBeCalled();
|
111
|
+
});
|
112
|
+
});
|
113
|
+
|
114
|
+
describe("whenTouched", () => {
|
115
|
+
it("should validate on change by default", () => {
|
116
|
+
const options: CreateGetInputPropsOptions = {
|
117
|
+
name: "some-field",
|
118
|
+
defaultValue: "test default value",
|
119
|
+
touched: true,
|
120
|
+
hasBeenSubmitted: false,
|
121
|
+
setTouched: vi.fn(),
|
122
|
+
clearError: vi.fn(),
|
123
|
+
validate: vi.fn(),
|
124
|
+
};
|
125
|
+
const getInputProps = createGetInputProps(options);
|
126
|
+
|
127
|
+
const provided = {
|
128
|
+
onBlur: vi.fn(),
|
129
|
+
onChange: vi.fn(),
|
130
|
+
};
|
131
|
+
const { onChange, onBlur } = getInputProps(provided);
|
132
|
+
|
133
|
+
onChange!(fakeEvent);
|
134
|
+
expect(provided.onChange).toBeCalledTimes(1);
|
135
|
+
expect(provided.onChange).toBeCalledWith(fakeEvent);
|
136
|
+
expect(options.setTouched).not.toBeCalled();
|
137
|
+
expect(options.validate).toBeCalledTimes(1);
|
138
|
+
|
139
|
+
onBlur!(fakeEvent);
|
140
|
+
expect(provided.onBlur).toBeCalledTimes(1);
|
141
|
+
expect(provided.onBlur).toBeCalledWith(fakeEvent);
|
142
|
+
expect(options.setTouched).toBeCalledTimes(1);
|
143
|
+
expect(options.setTouched).toBeCalledWith(true);
|
144
|
+
expect(options.validate).toBeCalledTimes(1);
|
145
|
+
});
|
146
|
+
|
147
|
+
it("should respect provided validation behavior", () => {
|
148
|
+
const options: CreateGetInputPropsOptions = {
|
149
|
+
name: "some-field",
|
150
|
+
defaultValue: "test default value",
|
151
|
+
touched: true,
|
152
|
+
hasBeenSubmitted: false,
|
153
|
+
setTouched: vi.fn(),
|
154
|
+
clearError: vi.fn(),
|
155
|
+
validate: vi.fn(),
|
156
|
+
validationBehavior: {
|
157
|
+
whenTouched: "onBlur",
|
158
|
+
},
|
159
|
+
};
|
160
|
+
const getInputProps = createGetInputProps(options);
|
161
|
+
|
162
|
+
const provided = {
|
163
|
+
onBlur: vi.fn(),
|
164
|
+
onChange: vi.fn(),
|
165
|
+
};
|
166
|
+
const { onChange, onBlur } = getInputProps(provided);
|
167
|
+
|
168
|
+
onChange!(fakeEvent);
|
169
|
+
expect(provided.onChange).toBeCalledTimes(1);
|
170
|
+
expect(provided.onChange).toBeCalledWith(fakeEvent);
|
171
|
+
expect(options.setTouched).not.toBeCalled();
|
172
|
+
expect(options.validate).not.toBeCalled();
|
173
|
+
|
174
|
+
onBlur!(fakeEvent);
|
175
|
+
expect(provided.onBlur).toBeCalledTimes(1);
|
176
|
+
expect(provided.onBlur).toBeCalledWith(fakeEvent);
|
177
|
+
expect(options.setTouched).toBeCalledTimes(1);
|
178
|
+
expect(options.setTouched).toBeCalledWith(true);
|
179
|
+
expect(options.validate).toBeCalledTimes(1);
|
180
|
+
});
|
181
|
+
});
|
182
|
+
|
183
|
+
describe("whenSubmitted", () => {
|
184
|
+
it("should validate on change by default", () => {
|
185
|
+
const options: CreateGetInputPropsOptions = {
|
186
|
+
name: "some-field",
|
187
|
+
defaultValue: "test default value",
|
188
|
+
touched: true,
|
189
|
+
hasBeenSubmitted: true,
|
190
|
+
setTouched: vi.fn(),
|
191
|
+
clearError: vi.fn(),
|
192
|
+
validate: vi.fn(),
|
193
|
+
};
|
194
|
+
const getInputProps = createGetInputProps(options);
|
195
|
+
|
196
|
+
const provided = {
|
197
|
+
onBlur: vi.fn(),
|
198
|
+
onChange: vi.fn(),
|
199
|
+
};
|
200
|
+
const { onChange, onBlur } = getInputProps(provided);
|
201
|
+
|
202
|
+
onChange!(fakeEvent);
|
203
|
+
expect(provided.onChange).toBeCalledTimes(1);
|
204
|
+
expect(provided.onChange).toBeCalledWith(fakeEvent);
|
205
|
+
expect(options.setTouched).not.toBeCalled();
|
206
|
+
expect(options.validate).toBeCalledTimes(1);
|
207
|
+
|
208
|
+
onBlur!(fakeEvent);
|
209
|
+
expect(provided.onBlur).toBeCalledTimes(1);
|
210
|
+
expect(provided.onBlur).toBeCalledWith(fakeEvent);
|
211
|
+
expect(options.setTouched).toBeCalledTimes(1);
|
212
|
+
expect(options.setTouched).toBeCalledWith(true);
|
213
|
+
expect(options.validate).toBeCalledTimes(1);
|
214
|
+
});
|
215
|
+
|
216
|
+
it("should respect provided validation behavior", () => {
|
217
|
+
const options: CreateGetInputPropsOptions = {
|
218
|
+
name: "some-field",
|
219
|
+
defaultValue: "test default value",
|
220
|
+
touched: true,
|
221
|
+
hasBeenSubmitted: true,
|
222
|
+
setTouched: vi.fn(),
|
223
|
+
clearError: vi.fn(),
|
224
|
+
validate: vi.fn(),
|
225
|
+
validationBehavior: {
|
226
|
+
whenSubmitted: "onBlur",
|
227
|
+
},
|
228
|
+
};
|
229
|
+
const getInputProps = createGetInputProps(options);
|
230
|
+
|
231
|
+
const provided = {
|
232
|
+
onBlur: vi.fn(),
|
233
|
+
onChange: vi.fn(),
|
234
|
+
};
|
235
|
+
const { onChange, onBlur } = getInputProps(provided);
|
236
|
+
|
237
|
+
onChange!(fakeEvent);
|
238
|
+
expect(provided.onChange).toBeCalledTimes(1);
|
239
|
+
expect(provided.onChange).toBeCalledWith(fakeEvent);
|
240
|
+
expect(options.setTouched).not.toBeCalled();
|
241
|
+
expect(options.validate).not.toBeCalled();
|
242
|
+
|
243
|
+
onBlur!(fakeEvent);
|
244
|
+
expect(provided.onBlur).toBeCalledTimes(1);
|
245
|
+
expect(provided.onBlur).toBeCalledWith(fakeEvent);
|
246
|
+
expect(options.setTouched).toBeCalledTimes(1);
|
247
|
+
expect(options.setTouched).toBeCalledWith(true);
|
248
|
+
expect(options.validate).toBeCalledTimes(1);
|
249
|
+
});
|
250
|
+
});
|
251
|
+
});
|
@@ -84,7 +84,8 @@ export const createGetInputProps = ({
|
|
84
84
|
inputProps.defaultChecked = getCheckboxChecked(props.value, defaultValue);
|
85
85
|
} else if (props.type === "radio") {
|
86
86
|
inputProps.defaultChecked = getRadioChecked(props.value, defaultValue);
|
87
|
-
} else {
|
87
|
+
} else if (props.value === undefined) {
|
88
|
+
// We should only set the defaultValue if the input is uncontrolled.
|
88
89
|
inputProps.defaultValue = defaultValue;
|
89
90
|
}
|
90
91
|
|
package/src/internal/hooks.ts
CHANGED
@@ -1,6 +1,4 @@
|
|
1
1
|
import { useActionData, useMatches, useTransition } from "@remix-run/react";
|
2
|
-
import { Atom, useAtom, WritableAtom } from "jotai";
|
3
|
-
import { useAtomValue, useUpdateAtom } from "jotai/utils";
|
4
2
|
import lodashGet from "lodash/get";
|
5
3
|
import { useCallback, useContext } from "react";
|
6
4
|
import invariant from "tiny-invariant";
|
@@ -8,25 +6,8 @@ import { FieldErrors, ValidationErrorResponseData } from "..";
|
|
8
6
|
import { formDefaultValuesKey } from "./constants";
|
9
7
|
import { InternalFormContext, InternalFormContextValue } from "./formContext";
|
10
8
|
import { Hydratable, hydratable } from "./hydratable";
|
11
|
-
import {
|
12
|
-
|
13
|
-
fieldErrorAtom,
|
14
|
-
fieldTouchedAtom,
|
15
|
-
formPropsAtom,
|
16
|
-
isHydratedAtom,
|
17
|
-
setFieldErrorAtom,
|
18
|
-
setTouchedAtom,
|
19
|
-
} from "./state";
|
20
|
-
|
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);
|
9
|
+
import { InternalFormId } from "./state/storeFamily";
|
10
|
+
import { useFormStore } from "./state/storeHooks";
|
30
11
|
|
31
12
|
export const useInternalFormContext = (
|
32
13
|
formId?: string | symbol,
|
@@ -72,7 +53,7 @@ export const useFieldErrorsForForm = (
|
|
72
53
|
context: InternalFormContextValue
|
73
54
|
): Hydratable<FieldErrors | undefined> => {
|
74
55
|
const response = useErrorResponseForForm(context);
|
75
|
-
const hydrated =
|
56
|
+
const hydrated = useFormStore(context.formId, (state) => state.isHydrated);
|
76
57
|
return hydratable.from(response?.fieldErrors, hydrated);
|
77
58
|
};
|
78
59
|
|
@@ -97,7 +78,7 @@ export const useDefaultValuesForForm = (
|
|
97
78
|
context: InternalFormContextValue
|
98
79
|
): Hydratable<{ [fieldName: string]: any }> => {
|
99
80
|
const { formId, defaultValuesProp } = context;
|
100
|
-
const hydrated =
|
81
|
+
const hydrated = useFormStore(formId, (state) => state.isHydrated);
|
101
82
|
const errorResponse = useErrorResponseForForm(context);
|
102
83
|
const defaultValuesFromLoader = useDefaultValuesFromLoader(context);
|
103
84
|
|
@@ -133,20 +114,31 @@ export const useHasActiveFormSubmit = ({
|
|
133
114
|
export const useFieldTouched = (
|
134
115
|
field: string,
|
135
116
|
{ formId }: InternalFormContextValue
|
136
|
-
) =>
|
117
|
+
) => {
|
118
|
+
const touched = useFormStore(formId, (state) => state.touchedFields[field]);
|
119
|
+
const setFieldTouched = useFormStore(formId, (state) => state.setTouched);
|
120
|
+
const setTouched = useCallback(
|
121
|
+
(touched: boolean) => setFieldTouched(field, touched),
|
122
|
+
[field, setFieldTouched]
|
123
|
+
);
|
124
|
+
return [touched, setTouched] as const;
|
125
|
+
};
|
137
126
|
|
138
127
|
export const useFieldError = (
|
139
128
|
name: string,
|
140
129
|
context: InternalFormContextValue
|
141
130
|
) => {
|
142
131
|
const fieldErrors = useFieldErrorsForForm(context);
|
143
|
-
const
|
144
|
-
|
132
|
+
const state = useFormStore(
|
133
|
+
context.formId,
|
134
|
+
(state) => state.fieldErrors[name]
|
145
135
|
);
|
146
|
-
return [
|
147
|
-
|
148
|
-
|
149
|
-
|
136
|
+
return fieldErrors.map((fieldErrors) => fieldErrors?.[name]).hydrateTo(state);
|
137
|
+
};
|
138
|
+
|
139
|
+
export const useClearError = (context: InternalFormContextValue) => {
|
140
|
+
const { formId } = context;
|
141
|
+
return useFormStore(formId, (state) => state.clearFieldError);
|
150
142
|
};
|
151
143
|
|
152
144
|
export const useFieldDefaultValue = (
|
@@ -154,26 +146,58 @@ export const useFieldDefaultValue = (
|
|
154
146
|
context: InternalFormContextValue
|
155
147
|
) => {
|
156
148
|
const defaultValues = useDefaultValuesForForm(context);
|
157
|
-
const
|
158
|
-
formPropsAtom(context.formId)
|
159
|
-
);
|
149
|
+
const state = useSyncedDefaultValues(context.formId);
|
160
150
|
return defaultValues
|
161
151
|
.map((val) => lodashGet(val, name))
|
162
152
|
.hydrateTo(lodashGet(state, name));
|
163
153
|
};
|
164
154
|
|
165
|
-
export const
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
155
|
+
export const useInternalIsSubmitting = (formId: InternalFormId) =>
|
156
|
+
useFormStore(formId, (state) => state.isSubmitting);
|
157
|
+
|
158
|
+
export const useInternalIsValid = (formId: InternalFormId) =>
|
159
|
+
useFormStore(formId, (state) => state.isValid());
|
160
|
+
|
161
|
+
export const useInternalHasBeenSubmitted = (formId: InternalFormId) =>
|
162
|
+
useFormStore(formId, (state) => state.hasBeenSubmitted);
|
163
|
+
|
164
|
+
export const useValidateField = (formId: InternalFormId) =>
|
165
|
+
useFormStore(formId, (state) => state.validateField);
|
166
|
+
|
167
|
+
export const useValidate = (formId: InternalFormId) =>
|
168
|
+
useFormStore(formId, (state) => state.validate);
|
169
|
+
|
170
|
+
const noOpReceiver = () => () => {};
|
171
|
+
export const useRegisterReceiveFocus = (formId: InternalFormId) =>
|
172
|
+
useFormStore(
|
173
|
+
formId,
|
174
|
+
(state) => state.formProps?.registerReceiveFocus ?? noOpReceiver
|
170
175
|
);
|
171
|
-
};
|
172
176
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
177
|
+
const defaultDefaultValues = {};
|
178
|
+
export const useSyncedDefaultValues = (formId: InternalFormId) =>
|
179
|
+
useFormStore(
|
180
|
+
formId,
|
181
|
+
(state) => state.formProps?.defaultValues ?? defaultDefaultValues
|
178
182
|
);
|
179
|
-
|
183
|
+
|
184
|
+
export const useSetTouched = ({ formId }: InternalFormContextValue) =>
|
185
|
+
useFormStore(formId, (state) => state.setTouched);
|
186
|
+
|
187
|
+
export const useTouchedFields = (formId: InternalFormId) =>
|
188
|
+
useFormStore(formId, (state) => state.touchedFields);
|
189
|
+
|
190
|
+
export const useFieldErrors = (formId: InternalFormId) =>
|
191
|
+
useFormStore(formId, (state) => state.fieldErrors);
|
192
|
+
|
193
|
+
export const useSetFieldErrors = (formId: InternalFormId) =>
|
194
|
+
useFormStore(formId, (state) => state.setFieldErrors);
|
195
|
+
|
196
|
+
export const useResetFormElement = (formId: InternalFormId) =>
|
197
|
+
useFormStore(formId, (state) => state.resetFormElement);
|
198
|
+
|
199
|
+
export const useFormActionProp = (formId: InternalFormId) =>
|
200
|
+
useFormStore(formId, (state) => state.formProps?.action);
|
201
|
+
|
202
|
+
export const useFormSubactionProp = (formId: InternalFormId) =>
|
203
|
+
useFormStore(formId, (state) => state.formProps?.subaction);
|
@@ -5,3 +5,14 @@ export const getRadioChecked = (
|
|
5
5
|
if (typeof newValue === "string") return newValue === radioValue;
|
6
6
|
return undefined;
|
7
7
|
};
|
8
|
+
|
9
|
+
if (import.meta.vitest) {
|
10
|
+
const { it, expect } = import.meta.vitest;
|
11
|
+
it("getRadioChecked", () => {
|
12
|
+
expect(getRadioChecked("on", "on")).toBe(true);
|
13
|
+
expect(getRadioChecked("on", undefined)).toBe(undefined);
|
14
|
+
expect(getRadioChecked("trueValue", undefined)).toBe(undefined);
|
15
|
+
expect(getRadioChecked("trueValue", "bob")).toBe(false);
|
16
|
+
expect(getRadioChecked("trueValue", "trueValue")).toBe(true);
|
17
|
+
});
|
18
|
+
}
|
@@ -0,0 +1,8 @@
|
|
1
|
+
import { controlledFieldStore } from "./controlledFieldStore";
|
2
|
+
import { formStore } from "./createFormStore";
|
3
|
+
import { InternalFormId } from "./storeFamily";
|
4
|
+
|
5
|
+
export const cleanupFormState = (formId: InternalFormId) => {
|
6
|
+
formStore.remove(formId);
|
7
|
+
controlledFieldStore.remove(formId);
|
8
|
+
};
|
@@ -0,0 +1,91 @@
|
|
1
|
+
import invariant from "tiny-invariant";
|
2
|
+
import create from "zustand";
|
3
|
+
import { immer } from "zustand/middleware/immer";
|
4
|
+
import { storeFamily } from "./storeFamily";
|
5
|
+
|
6
|
+
export type ControlledFieldState = {
|
7
|
+
fields: {
|
8
|
+
[fieldName: string]:
|
9
|
+
| {
|
10
|
+
refCount: number;
|
11
|
+
value: unknown;
|
12
|
+
defaultValue?: unknown;
|
13
|
+
hydrated: boolean;
|
14
|
+
valueUpdatePromise: Promise<void> | undefined;
|
15
|
+
resolveValueUpdate: (() => void) | undefined;
|
16
|
+
}
|
17
|
+
| undefined;
|
18
|
+
};
|
19
|
+
register: (fieldName: string) => void;
|
20
|
+
unregister: (fieldName: string) => void;
|
21
|
+
setValue: (fieldName: string, value: unknown) => void;
|
22
|
+
hydrateWithDefault: (fieldName: string, defaultValue: unknown) => void;
|
23
|
+
awaitValueUpdate: (fieldName: string) => Promise<void>;
|
24
|
+
reset: () => void;
|
25
|
+
};
|
26
|
+
|
27
|
+
export const controlledFieldStore = storeFamily(() =>
|
28
|
+
create<ControlledFieldState>()(
|
29
|
+
immer((set, get, api) => ({
|
30
|
+
fields: {},
|
31
|
+
|
32
|
+
register: (field) =>
|
33
|
+
set((state) => {
|
34
|
+
if (state.fields[field]) {
|
35
|
+
state.fields[field]!.refCount++;
|
36
|
+
} else {
|
37
|
+
state.fields[field] = {
|
38
|
+
refCount: 1,
|
39
|
+
value: undefined,
|
40
|
+
hydrated: false,
|
41
|
+
valueUpdatePromise: undefined,
|
42
|
+
resolveValueUpdate: undefined,
|
43
|
+
};
|
44
|
+
}
|
45
|
+
}),
|
46
|
+
|
47
|
+
unregister: (field) =>
|
48
|
+
set((state) => {
|
49
|
+
const fieldState = state.fields[field];
|
50
|
+
if (!fieldState) return;
|
51
|
+
|
52
|
+
fieldState.refCount--;
|
53
|
+
if (fieldState.refCount === 0) delete state.fields[field];
|
54
|
+
}),
|
55
|
+
|
56
|
+
setValue: (field, value) =>
|
57
|
+
set((state) => {
|
58
|
+
const fieldState = state.fields[field];
|
59
|
+
if (!fieldState) return;
|
60
|
+
|
61
|
+
fieldState.value = value;
|
62
|
+
const promise = new Promise<void>((resolve) => {
|
63
|
+
fieldState.resolveValueUpdate = resolve;
|
64
|
+
});
|
65
|
+
fieldState.valueUpdatePromise = promise;
|
66
|
+
}),
|
67
|
+
|
68
|
+
hydrateWithDefault: (field, defaultValue) =>
|
69
|
+
set((state) => {
|
70
|
+
const fieldState = state.fields[field];
|
71
|
+
if (!fieldState) return;
|
72
|
+
|
73
|
+
fieldState.value = defaultValue;
|
74
|
+
fieldState.defaultValue = defaultValue;
|
75
|
+
fieldState.hydrated = true;
|
76
|
+
}),
|
77
|
+
|
78
|
+
awaitValueUpdate: async (field) => {
|
79
|
+
await get().fields[field]?.valueUpdatePromise;
|
80
|
+
},
|
81
|
+
|
82
|
+
reset: () =>
|
83
|
+
set((state) => {
|
84
|
+
Object.values(state.fields).forEach((field) => {
|
85
|
+
if (!field) return;
|
86
|
+
field.value = field.defaultValue;
|
87
|
+
});
|
88
|
+
}),
|
89
|
+
}))
|
90
|
+
)
|
91
|
+
);
|
@@ -0,0 +1,78 @@
|
|
1
|
+
import { useCallback, useEffect } from "react";
|
2
|
+
import { InternalFormContextValue } from "../formContext";
|
3
|
+
import { useFieldDefaultValue } from "../hooks";
|
4
|
+
import { controlledFieldStore } from "./controlledFieldStore";
|
5
|
+
import { formStore } from "./createFormStore";
|
6
|
+
import { InternalFormId } from "./storeFamily";
|
7
|
+
|
8
|
+
export const useControlledFieldValue = (
|
9
|
+
context: InternalFormContextValue,
|
10
|
+
field: string
|
11
|
+
) => {
|
12
|
+
const useValueStore = controlledFieldStore(context.formId);
|
13
|
+
const value = useValueStore((state) => state.fields[field]?.value);
|
14
|
+
|
15
|
+
const useFormStore = formStore(context.formId);
|
16
|
+
const isFormHydrated = useFormStore((state) => state.isHydrated);
|
17
|
+
const defaultValue = useFieldDefaultValue(field, context);
|
18
|
+
|
19
|
+
const isFieldHydrated = useValueStore(
|
20
|
+
(state) => state.fields[field]?.hydrated ?? false
|
21
|
+
);
|
22
|
+
const hydrateWithDefault = useValueStore((state) => state.hydrateWithDefault);
|
23
|
+
|
24
|
+
useEffect(() => {
|
25
|
+
if (isFormHydrated && !isFieldHydrated) {
|
26
|
+
hydrateWithDefault(field, defaultValue);
|
27
|
+
}
|
28
|
+
}, [
|
29
|
+
defaultValue,
|
30
|
+
field,
|
31
|
+
hydrateWithDefault,
|
32
|
+
isFieldHydrated,
|
33
|
+
isFormHydrated,
|
34
|
+
]);
|
35
|
+
|
36
|
+
return isFieldHydrated ? value : defaultValue;
|
37
|
+
};
|
38
|
+
|
39
|
+
export const useControllableValue = (
|
40
|
+
context: InternalFormContextValue,
|
41
|
+
field: string
|
42
|
+
) => {
|
43
|
+
const useValueStore = controlledFieldStore(context.formId);
|
44
|
+
|
45
|
+
const resolveUpdate = useValueStore(
|
46
|
+
(state) => state.fields[field]?.resolveValueUpdate
|
47
|
+
);
|
48
|
+
useEffect(() => {
|
49
|
+
resolveUpdate?.();
|
50
|
+
}, [resolveUpdate]);
|
51
|
+
|
52
|
+
const register = useValueStore((state) => state.register);
|
53
|
+
const unregister = useValueStore((state) => state.unregister);
|
54
|
+
useEffect(() => {
|
55
|
+
register(field);
|
56
|
+
return () => unregister(field);
|
57
|
+
}, [context.formId, field, register, unregister]);
|
58
|
+
|
59
|
+
const setControlledFieldValue = useValueStore((state) => state.setValue);
|
60
|
+
const setValue = useCallback(
|
61
|
+
(value: unknown) => setControlledFieldValue(field, value),
|
62
|
+
[field, setControlledFieldValue]
|
63
|
+
);
|
64
|
+
|
65
|
+
const value = useControlledFieldValue(context, field);
|
66
|
+
|
67
|
+
return [value, setValue] as const;
|
68
|
+
};
|
69
|
+
|
70
|
+
export const useUpdateControllableValue = (formId: InternalFormId) => {
|
71
|
+
const useValueStore = controlledFieldStore(formId);
|
72
|
+
return useValueStore((state) => state.setValue);
|
73
|
+
};
|
74
|
+
|
75
|
+
export const useAwaitValue = (formId: InternalFormId) => {
|
76
|
+
const useValueStore = controlledFieldStore(formId);
|
77
|
+
return useValueStore((state) => state.awaitValueUpdate);
|
78
|
+
};
|