@firecms/formex 3.0.0-canary.21 → 3.0.0-canary.210

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,28 +1,52 @@
1
- import React, { FormEvent, useEffect, useState } from "react";
1
+ import React, { useEffect, useState, useCallback, useMemo, useRef } from "react";
2
2
  import { getIn, setIn } from "./utils";
3
- import equal from "react-fast-compare"
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
-
16
- const initialValuesRef = React.useRef<T>(initialValues);
17
- const valuesRef = React.useRef<T>(initialValues);
34
+ const initialValuesRef = useRef<T>(initialValues);
35
+ const valuesRef = useRef<T>(initialValues);
36
+ const debugIdRef = useRef<string | undefined>(debugId);
18
37
 
19
38
  const [values, setValuesInner] = useState<T>(initialValues);
20
39
  const [touchedState, setTouchedState] = useState<Record<string, boolean>>({});
21
40
  const [errors, setErrors] = useState<Record<string, string>>(initialErrors ?? {});
22
- const [dirty, setDirty] = useState(false);
41
+ const [dirty, setDirty] = useState(initialDirty ?? false);
23
42
  const [submitCount, setSubmitCount] = useState(0);
24
43
  const [isSubmitting, setIsSubmitting] = useState(false);
25
44
  const [isValidating, setIsValidating] = useState(false);
45
+ const [version, setVersion] = useState(0);
46
+
47
+ // Replace state for history with refs
48
+ const historyRef = useRef<T[]>([initialValues]);
49
+ const historyIndexRef = useRef<number>(0);
26
50
 
27
51
  useEffect(() => {
28
52
  if (validateOnInitialRender) {
@@ -30,121 +54,205 @@ export function useCreateFormex<T extends object>({ initialValues, initialErrors
30
54
  }
31
55
  }, []);
32
56
 
33
- const setValues = (newValues: T) => {
57
+ const setValues = useCallback((newValues: T) => {
34
58
  valuesRef.current = newValues;
35
59
  setValuesInner(newValues);
36
- setDirty(equal(initialValuesRef.current, newValues));
37
- }
60
+ setDirty(!equal(initialValuesRef.current, newValues));
61
+ // Update history using refs
62
+ const newHistory = historyRef.current.slice(0, historyIndexRef.current + 1);
63
+ newHistory.push(newValues);
64
+ historyRef.current = newHistory;
65
+ historyIndexRef.current = newHistory.length - 1;
66
+ }, []);
38
67
 
39
- const validate = async () => {
68
+ const validate = useCallback(async () => {
40
69
  setIsValidating(true);
41
- const values = valuesRef.current;
42
- const validationErrors = await validation?.(values);
70
+ const validationErrors = await validation?.(valuesRef.current);
43
71
  setErrors(validationErrors ?? {});
44
72
  setIsValidating(false);
45
73
  return validationErrors;
46
- }
74
+ }, [validation]);
47
75
 
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
- }
76
+ const setFieldValue = useCallback(
77
+ (key: string, value: any, shouldValidate?: boolean) => {
78
+ const newValues = setIn(valuesRef.current, key, value);
79
+ valuesRef.current = newValues;
80
+ setValuesInner(newValues);
81
+ if (!equal(getIn(initialValuesRef.current, key), value)) {
82
+ setDirty(true);
83
+ }
84
+ if (shouldValidate) {
85
+ validate();
86
+ }
87
+ // Update history using refs
88
+ const newHistory = historyRef.current.slice(0, historyIndexRef.current + 1);
89
+ newHistory.push(newValues);
90
+ historyRef.current = newHistory;
91
+ historyIndexRef.current = newHistory.length - 1;
92
+ },
93
+ [validate]
94
+ );
78
95
 
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
- }
96
+ const setFieldError = useCallback((key: string, error: string | undefined) => {
97
+ setErrors((prevErrors) => {
98
+ const newErrors = { ...prevErrors };
99
+ if (error) {
100
+ newErrors[key] = error;
101
+ } else {
102
+ delete newErrors[key];
103
+ }
104
+ return newErrors;
105
+ });
106
+ }, []);
86
107
 
87
- const handleBlur = (event: React.FocusEvent) => {
108
+ const setFieldTouched = useCallback(
109
+ (key: string, touched: boolean, shouldValidate?: boolean) => {
110
+ setTouchedState((prev) => ({
111
+ ...prev,
112
+ [key]: touched,
113
+ }));
114
+ if (shouldValidate) {
115
+ validate();
116
+ }
117
+ },
118
+ [validate]
119
+ );
120
+
121
+ const handleChange = useCallback(
122
+ (event: React.SyntheticEvent) => {
123
+ const target = event.target as HTMLInputElement;
124
+ let value;
125
+ if (target.type === "checkbox") {
126
+ value = target.checked;
127
+ } else if (target.type === "number") {
128
+ value = target.valueAsNumber;
129
+ } else {
130
+ value = target.value;
131
+ }
132
+ const name = target.name;
133
+ setFieldValue(name, value, validateOnChange);
134
+ setFieldTouched(name, true);
135
+ },
136
+ [setFieldValue, setFieldTouched, validateOnChange]
137
+ );
138
+
139
+ const handleBlur = useCallback((event: React.FocusEvent) => {
88
140
  const target = event.target as HTMLInputElement;
89
141
  const name = target.name;
90
142
  setFieldTouched(name, true);
91
- }
143
+ }, [setFieldTouched]);
92
144
 
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>) => {
109
- const {
110
- submitCount: submitCountProp,
111
- values: valuesProp,
112
- errors: errorsProp,
113
- touched: touchedProp
114
- } = props ?? {};
115
- initialValuesRef.current = valuesProp ?? initialValues;
116
- valuesRef.current = valuesProp ?? initialValues;
117
- setValuesInner(valuesProp ?? initialValues);
145
+ const submit = useCallback(
146
+ async (e?: React.FormEvent<HTMLFormElement>) => {
147
+ e?.preventDefault();
148
+ e?.stopPropagation();
149
+ setIsSubmitting(true);
150
+ setSubmitCount((prev) => prev + 1);
151
+ const validationErrors = await validation?.(valuesRef.current);
152
+ if (validationErrors && Object.keys(validationErrors).length > 0) {
153
+ setErrors(validationErrors);
154
+ } else {
155
+ setErrors({});
156
+ await onSubmit?.(valuesRef.current, controllerRef.current);
157
+ }
158
+ setIsSubmitting(false);
159
+ setVersion((prev) => prev + 1);
160
+ },
161
+ [onSubmit, validation]
162
+ );
163
+
164
+ const resetForm = useCallback((props?: FormexResetProps<T>) => {
165
+ const { submitCount: submitCountProp, values: valuesProp, errors: errorsProp, touched: touchedProp } = props ?? {};
166
+ valuesRef.current = valuesProp ?? initialValuesRef.current;
167
+ setValuesInner(valuesProp ?? initialValuesRef.current);
118
168
  setErrors(errorsProp ?? {});
119
169
  setTouchedState(touchedProp ?? {});
120
170
  setDirty(false);
121
171
  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
172
+ setVersion((prev) => prev + 1);
173
+ onReset?.(controllerRef.current);
174
+ // Reset history with refs
175
+ historyRef.current = [valuesProp ?? initialValuesRef.current];
176
+ historyIndexRef.current = 0;
177
+ }, [onReset]);
178
+
179
+ const undo = useCallback(() => {
180
+ if (historyIndexRef.current > 0) {
181
+ const newIndex = historyIndexRef.current - 1;
182
+ const newValues = historyRef.current[newIndex];
183
+ setValuesInner(newValues);
184
+ valuesRef.current = newValues;
185
+ historyIndexRef.current = newIndex;
186
+ }
187
+ }, []);
188
+
189
+ const redo = useCallback(() => {
190
+ if (historyIndexRef.current < historyRef.current.length - 1) {
191
+ const newIndex = historyIndexRef.current + 1;
192
+ const newValues = historyRef.current[newIndex];
193
+ setValuesInner(newValues);
194
+ valuesRef.current = newValues;
195
+ historyIndexRef.current = newIndex;
196
+ }
197
+ }, []);
198
+
199
+ const controllerRef = useRef<FormexController<T>>({} as FormexController<T>);
200
+
201
+ const controller = useMemo<FormexController<T>>(
202
+ () => ({
203
+ values,
204
+ initialValues: initialValuesRef.current,
205
+ handleChange,
206
+ isSubmitting,
207
+ setSubmitting: setIsSubmitting,
208
+ setValues,
209
+ setFieldValue,
210
+ errors,
211
+ setFieldError,
212
+ touched: touchedState,
213
+ setFieldTouched,
214
+ dirty,
215
+ setDirty,
216
+ handleSubmit: submit,
217
+ submitCount,
218
+ setSubmitCount,
219
+ handleBlur,
220
+ validate,
221
+ isValidating,
222
+ resetForm,
223
+ version,
224
+ debugId: debugIdRef.current,
225
+ undo,
226
+ redo,
227
+ canUndo: historyIndexRef.current > 0,
228
+ canRedo: historyIndexRef.current < historyRef.current.length - 1,
229
+ }),
230
+ [
231
+ values,
232
+ errors,
233
+ touchedState,
234
+ dirty,
235
+ isSubmitting,
236
+ submitCount,
237
+ isValidating,
238
+ version,
239
+ handleChange,
240
+ handleBlur,
241
+ setValues,
242
+ setFieldValue,
243
+ setFieldTouched,
244
+ setFieldError,
245
+ validate,
246
+ submit,
247
+ resetForm,
248
+ undo,
249
+ redo,
250
+ ]
251
+ );
252
+
253
+ useEffect(() => {
254
+ controllerRef.current = controller;
255
+ }, [controller]);
256
+
257
+ return controller;
150
258
  }
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) {