@firecms/formex 3.0.0-canary.20 → 3.0.0-canary.201
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/README.md +2 -2
- package/dist/Field.d.ts +0 -1
- package/dist/index.es.js +427 -148
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +467 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/useCreateFormex.d.ts +4 -1
- package/dist/utils.d.ts +2 -1
- package/package.json +29 -24
- package/src/Field.tsx +0 -4
- package/src/types.ts +7 -0
- package/src/useCreateFormex.tsx +200 -107
- package/src/utils.ts +1 -1
package/src/useCreateFormex.tsx
CHANGED
@@ -1,150 +1,243 @@
|
|
1
|
-
import React, {
|
1
|
+
import React, { useEffect, useState } from "react";
|
2
2
|
import { getIn, setIn } from "./utils";
|
3
3
|
import equal from "react-fast-compare"
|
4
4
|
|
5
5
|
import { FormexController, FormexResetProps } from "./types";
|
6
6
|
|
7
|
-
export function useCreateFormex<T extends object>({
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
7
|
+
export function useCreateFormex<T extends object>({
|
8
|
+
initialValues,
|
9
|
+
initialErrors,
|
10
|
+
initialDirty,
|
11
|
+
validation,
|
12
|
+
validateOnChange = false,
|
13
|
+
validateOnInitialRender = false,
|
14
|
+
onSubmit,
|
15
|
+
onReset,
|
16
|
+
debugId,
|
17
|
+
}: {
|
18
|
+
initialValues: T;
|
19
|
+
initialErrors?: Record<string, string>;
|
20
|
+
initialDirty?: boolean;
|
21
|
+
validateOnChange?: boolean;
|
22
|
+
validateOnInitialRender?: boolean;
|
23
|
+
validation?: (
|
24
|
+
values: T
|
25
|
+
) =>
|
26
|
+
| Record<string, string>
|
27
|
+
| Promise<Record<string, string>>
|
28
|
+
| undefined
|
29
|
+
| void;
|
30
|
+
onSubmit?: (values: T, controller: FormexController<T>) => void | Promise<void>;
|
31
|
+
onReset?: (controller: FormexController<T>) => void | Promise<void>;
|
32
|
+
debugId?: string;
|
14
33
|
}): FormexController<T> {
|
15
|
-
|
34
|
+
// Refs (for current state which shouldn’t trigger re – renders)
|
16
35
|
const initialValuesRef = React.useRef<T>(initialValues);
|
17
36
|
const valuesRef = React.useRef<T>(initialValues);
|
37
|
+
const debugIdRef = React.useRef<string | undefined>(debugId);
|
18
38
|
|
39
|
+
// State
|
19
40
|
const [values, setValuesInner] = useState<T>(initialValues);
|
20
41
|
const [touchedState, setTouchedState] = useState<Record<string, boolean>>({});
|
21
42
|
const [errors, setErrors] = useState<Record<string, string>>(initialErrors ?? {});
|
22
|
-
const [dirty, setDirty] = useState(false);
|
43
|
+
const [dirty, setDirty] = useState(initialDirty ?? false);
|
23
44
|
const [submitCount, setSubmitCount] = useState(0);
|
24
45
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
25
46
|
const [isValidating, setIsValidating] = useState(false);
|
47
|
+
const [version, setVersion] = useState(0);
|
26
48
|
|
49
|
+
// Run initial validation if required
|
27
50
|
useEffect(() => {
|
28
51
|
if (validateOnInitialRender) {
|
29
52
|
validate();
|
30
53
|
}
|
31
54
|
}, []);
|
32
55
|
|
33
|
-
|
56
|
+
// Memoize setValues so that it doesn’t change unless nothing inside changes.
|
57
|
+
const setValues = React.useCallback((newValues: T) => {
|
34
58
|
valuesRef.current = newValues;
|
35
59
|
setValuesInner(newValues);
|
36
|
-
|
37
|
-
|
60
|
+
// Adjust dirty flag by comparing with the initial values
|
61
|
+
setDirty(!equal(initialValuesRef.current, newValues));
|
62
|
+
}, []);
|
38
63
|
|
39
|
-
|
64
|
+
// Memoized validate function
|
65
|
+
const validate = React.useCallback(async () => {
|
40
66
|
setIsValidating(true);
|
41
|
-
const
|
42
|
-
const validationErrors = await validation?.(values);
|
67
|
+
const validationErrors = await validation?.(valuesRef.current);
|
43
68
|
setErrors(validationErrors ?? {});
|
44
69
|
setIsValidating(false);
|
45
70
|
return validationErrors;
|
46
|
-
}
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
setErrors(
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
}
|
78
|
-
|
79
|
-
const handleChange = (event: React.SyntheticEvent) => {
|
80
|
-
const target = event.target as HTMLInputElement;
|
81
|
-
const value = target.type === "checkbox" ? target.checked : target.value;
|
82
|
-
const name = target.name;
|
83
|
-
setFieldValue(name, value, validateOnChange);
|
84
|
-
setFieldTouched(name, true);
|
85
|
-
}
|
71
|
+
}, [validation]);
|
72
|
+
|
73
|
+
// setFieldValue updates a single field and optionally triggers validation
|
74
|
+
const setFieldValue = React.useCallback(
|
75
|
+
(key: string, value: any, shouldValidate?: boolean) => {
|
76
|
+
const newValues = setIn(valuesRef.current, key, value);
|
77
|
+
valuesRef.current = newValues;
|
78
|
+
setValuesInner(newValues);
|
79
|
+
// Compare with initial value using getIn
|
80
|
+
if (!equal(getIn(initialValuesRef.current, key), value)) {
|
81
|
+
setDirty(true);
|
82
|
+
}
|
83
|
+
if (shouldValidate) {
|
84
|
+
validate();
|
85
|
+
}
|
86
|
+
},
|
87
|
+
[validate]
|
88
|
+
);
|
89
|
+
|
90
|
+
// setFieldError uses functional updates to ensure we’re working off the current error state.
|
91
|
+
const setFieldError = React.useCallback((key: string, error: string | undefined) => {
|
92
|
+
setErrors((prevErrors) => {
|
93
|
+
const newErrors = { ...prevErrors };
|
94
|
+
if (error) {
|
95
|
+
newErrors[key] = error;
|
96
|
+
} else {
|
97
|
+
delete newErrors[key];
|
98
|
+
}
|
99
|
+
return newErrors;
|
100
|
+
});
|
101
|
+
}, []);
|
86
102
|
|
87
|
-
|
103
|
+
// setFieldTouched updates touched state and can optionally trigger validation.
|
104
|
+
const setFieldTouched = React.useCallback(
|
105
|
+
(key: string, touched: boolean, shouldValidate?: boolean) => {
|
106
|
+
setTouchedState((prev) => {
|
107
|
+
const newTouched = {
|
108
|
+
...prev,
|
109
|
+
[key]: touched
|
110
|
+
};
|
111
|
+
return newTouched;
|
112
|
+
});
|
113
|
+
if (shouldValidate) {
|
114
|
+
validate();
|
115
|
+
}
|
116
|
+
},
|
117
|
+
[validate]
|
118
|
+
);
|
119
|
+
|
120
|
+
// handleChange reads the event, determines the proper value,
|
121
|
+
// and then delegates to setFieldValue and setFieldTouched.
|
122
|
+
const handleChange = React.useCallback(
|
123
|
+
(event: React.SyntheticEvent) => {
|
124
|
+
const target = event.target as HTMLInputElement;
|
125
|
+
let value;
|
126
|
+
if (target.type === "checkbox") {
|
127
|
+
value = target.checked;
|
128
|
+
} else if (target.type === "number") {
|
129
|
+
value = target.valueAsNumber;
|
130
|
+
} else {
|
131
|
+
value = target.value;
|
132
|
+
}
|
133
|
+
const name = target.name;
|
134
|
+
setFieldValue(name, value, validateOnChange);
|
135
|
+
setFieldTouched(name, true);
|
136
|
+
},
|
137
|
+
[setFieldValue, setFieldTouched, validateOnChange]
|
138
|
+
);
|
139
|
+
|
140
|
+
// handleBlur simply marks the field as touched.
|
141
|
+
const handleBlur = React.useCallback((event: React.FocusEvent) => {
|
88
142
|
const target = event.target as HTMLInputElement;
|
89
143
|
const name = target.name;
|
90
144
|
setFieldTouched(name, true);
|
91
|
-
}
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
e
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
145
|
+
}, [setFieldTouched]);
|
146
|
+
|
147
|
+
// submit uses functional updates on submitCount and version.
|
148
|
+
const submit = React.useCallback(
|
149
|
+
async (e?: React.FormEvent<HTMLFormElement>) => {
|
150
|
+
e?.preventDefault();
|
151
|
+
e?.stopPropagation();
|
152
|
+
setIsSubmitting(true);
|
153
|
+
setSubmitCount((prev) => prev + 1);
|
154
|
+
const validationErrors = await validation?.(valuesRef.current);
|
155
|
+
if (validationErrors && Object.keys(validationErrors).length > 0) {
|
156
|
+
setErrors(validationErrors);
|
157
|
+
} else {
|
158
|
+
setErrors({});
|
159
|
+
await onSubmit?.(valuesRef.current, controllerRef.current);
|
160
|
+
}
|
161
|
+
setIsSubmitting(false);
|
162
|
+
setVersion((prev) => prev + 1);
|
163
|
+
},
|
164
|
+
[onSubmit, validation]
|
165
|
+
);
|
166
|
+
|
167
|
+
// resetForm resets to the passed props (or initial configuration).
|
168
|
+
const resetForm = React.useCallback((props?: FormexResetProps<T>) => {
|
109
169
|
const {
|
110
170
|
submitCount: submitCountProp,
|
111
171
|
values: valuesProp,
|
112
172
|
errors: errorsProp,
|
113
173
|
touched: touchedProp
|
114
|
-
} =
|
115
|
-
|
116
|
-
valuesRef.current = valuesProp ??
|
117
|
-
|
174
|
+
} =
|
175
|
+
props ?? {};
|
176
|
+
valuesRef.current = valuesProp ?? initialValuesRef.current;
|
177
|
+
initialValuesRef.current = valuesProp ?? initialValuesRef.current;
|
178
|
+
setValuesInner(valuesProp ?? initialValuesRef.current);
|
118
179
|
setErrors(errorsProp ?? {});
|
119
180
|
setTouchedState(touchedProp ?? {});
|
120
181
|
setDirty(false);
|
121
182
|
setSubmitCount(submitCountProp ?? 0);
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
183
|
+
setVersion((prev) => prev + 1);
|
184
|
+
onReset?.(controllerRef.current);
|
185
|
+
}, [onReset]);
|
186
|
+
|
187
|
+
// Create a ref for the controller so that it remains stable over time.
|
188
|
+
const controllerRef = React.useRef<FormexController<T>>({} as FormexController<T>);
|
189
|
+
|
190
|
+
// Memoize the controller object so that consumers don’t see new references on every render.
|
191
|
+
const controller = React.useMemo<FormexController<T>>(
|
192
|
+
() => ({
|
193
|
+
values,
|
194
|
+
initialValues: initialValuesRef.current,
|
195
|
+
handleChange,
|
196
|
+
isSubmitting,
|
197
|
+
setSubmitting: setIsSubmitting,
|
198
|
+
setValues,
|
199
|
+
setFieldValue,
|
200
|
+
errors,
|
201
|
+
setFieldError,
|
202
|
+
touched: touchedState,
|
203
|
+
setFieldTouched,
|
204
|
+
dirty,
|
205
|
+
setDirty, // setter from useState is stable
|
206
|
+
handleSubmit: submit,
|
207
|
+
submitCount,
|
208
|
+
setSubmitCount, // setter from useState is stable
|
209
|
+
handleBlur,
|
210
|
+
validate,
|
211
|
+
isValidating,
|
212
|
+
resetForm,
|
213
|
+
version,
|
214
|
+
debugId: debugIdRef.current,
|
215
|
+
}),
|
216
|
+
[
|
217
|
+
values,
|
218
|
+
errors,
|
219
|
+
touchedState,
|
220
|
+
dirty,
|
221
|
+
isSubmitting,
|
222
|
+
submitCount,
|
223
|
+
isValidating,
|
224
|
+
version,
|
225
|
+
handleChange,
|
226
|
+
handleBlur,
|
227
|
+
setValues,
|
228
|
+
setFieldValue,
|
229
|
+
setFieldTouched,
|
230
|
+
setFieldError,
|
231
|
+
validate,
|
232
|
+
submit,
|
233
|
+
resetForm,
|
234
|
+
]
|
235
|
+
);
|
236
|
+
|
237
|
+
// Keep the ref updated with the latest controller
|
238
|
+
React.useEffect(() => {
|
239
|
+
controllerRef.current = controller;
|
240
|
+
}, [controller]);
|
241
|
+
|
242
|
+
return controller;
|
150
243
|
}
|
package/src/utils.ts
CHANGED
@@ -152,7 +152,7 @@ export function setNestedObjectValues<T>(
|
|
152
152
|
return response;
|
153
153
|
}
|
154
154
|
|
155
|
-
function clone(value: any) {
|
155
|
+
export function clone(value: any) {
|
156
156
|
if (Array.isArray(value)) {
|
157
157
|
return [...value];
|
158
158
|
} else if (typeof value === "object" && value !== null) {
|