@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.
@@ -1,150 +1,243 @@
1
- import React, { FormEvent, useEffect, useState } from "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>({ initialValues, initialErrors, validation, validateOnChange = false, onSubmit, validateOnInitialRender = false }: {
8
- initialValues: T,
9
- initialErrors?: Record<string, string>,
10
- validateOnChange?: boolean,
11
- validateOnInitialRender?: boolean,
12
- validation?: (values: T) => Record<string, string> | Promise<Record<string, string>> | undefined | void,
13
- onSubmit?: (values: T, controller: FormexController<T>) => void | Promise<void>
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
- const setValues = (newValues: T) => {
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
- setDirty(equal(initialValuesRef.current, newValues));
37
- }
60
+ // Adjust dirty flag by comparing with the initial values
61
+ setDirty(!equal(initialValuesRef.current, newValues));
62
+ }, []);
38
63
 
39
- const validate = async () => {
64
+ // Memoized validate function
65
+ const validate = React.useCallback(async () => {
40
66
  setIsValidating(true);
41
- const values = valuesRef.current;
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
- const setFieldValue = (key: string, value: any, shouldValidate?: boolean) => {
49
- const newValues = setIn(valuesRef.current, key, value);
50
- valuesRef.current = newValues;
51
- setValuesInner(newValues);
52
- if (!equal(getIn(initialValuesRef.current, key), value)) {
53
- setDirty(true);
54
- }
55
- if (shouldValidate) {
56
- validate();
57
- }
58
- }
59
-
60
- const setFieldError = (key: string, error: string | undefined) => {
61
- const newErrors = { ...errors };
62
- if (error) {
63
- newErrors[key] = error;
64
- } else {
65
- delete newErrors[key];
66
- }
67
- setErrors(newErrors);
68
- }
69
-
70
- const setFieldTouched = (key: string, touched: boolean, shouldValidate?: boolean | undefined) => {
71
- const newTouched = { ...touchedState };
72
- newTouched[key] = touched;
73
- setTouchedState(newTouched);
74
- if (shouldValidate) {
75
- validate();
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
- const handleBlur = (event: React.FocusEvent) => {
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
- const submit = async (e?: FormEvent<HTMLFormElement>) => {
94
- e?.preventDefault();
95
- e?.stopPropagation();
96
- setIsSubmitting(true);
97
- setSubmitCount(submitCount + 1);
98
- const validationErrors = await validation?.(valuesRef.current);
99
- if (validationErrors && Object.keys(validationErrors).length > 0) {
100
- setErrors(validationErrors);
101
- } else {
102
- setErrors({});
103
- await onSubmit?.(valuesRef.current, controllerRef.current);
104
- }
105
- setIsSubmitting(false);
106
- }
107
-
108
- const resetForm = (props?: FormexResetProps<T>) => {
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
- } = props ?? {};
115
- initialValuesRef.current = valuesProp ?? initialValues;
116
- valuesRef.current = valuesProp ?? initialValues;
117
- setValuesInner(valuesProp ?? initialValues);
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
- const controller: FormexController<T> = {
125
- values,
126
- initialValues: initialValuesRef.current,
127
- handleChange,
128
- isSubmitting,
129
- setSubmitting: setIsSubmitting,
130
- setValues,
131
- setFieldValue,
132
- errors,
133
- setFieldError,
134
- touched: touchedState,
135
- setFieldTouched,
136
- dirty,
137
- setDirty,
138
- handleSubmit: submit,
139
- submitCount,
140
- setSubmitCount,
141
- handleBlur,
142
- validate,
143
- isValidating,
144
- resetForm
145
- };
146
-
147
- const controllerRef = React.useRef<FormexController<T>>(controller);
148
- controllerRef.current = controller;
149
- return controller
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) {