@firecms/formex 3.0.0-canary.25 → 3.0.0-canary.250

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, { useCallback, useEffect, useMemo, useRef, useState } 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,211 @@ 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
- }
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
+ );
107
163
 
108
- const resetForm = (props?: FormexResetProps<T>) => {
164
+ const resetForm = useCallback((props?: FormexResetProps<T>) => {
109
165
  const {
110
166
  submitCount: submitCountProp,
111
167
  values: valuesProp,
112
168
  errors: errorsProp,
113
169
  touched: touchedProp
114
170
  } = props ?? {};
115
- initialValuesRef.current = valuesProp ?? initialValues;
116
- valuesRef.current = valuesProp ?? initialValues;
117
- setValuesInner(valuesProp ?? initialValues);
171
+ valuesRef.current = valuesProp ?? initialValuesRef.current;
172
+ initialValuesRef.current = valuesProp ?? initialValuesRef.current;
173
+ setValuesInner(valuesProp ?? initialValuesRef.current);
118
174
  setErrors(errorsProp ?? {});
119
175
  setTouchedState(touchedProp ?? {});
120
176
  setDirty(false);
121
177
  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
178
+ setVersion((prev) => prev + 1);
179
+ onReset?.(controllerRef.current);
180
+ // Reset history with refs
181
+ historyRef.current = [valuesProp ?? initialValuesRef.current];
182
+ historyIndexRef.current = 0;
183
+ }, [onReset]);
184
+
185
+ const undo = useCallback(() => {
186
+ if (historyIndexRef.current > 0) {
187
+ const newIndex = historyIndexRef.current - 1;
188
+ const newValues = historyRef.current[newIndex];
189
+ setValuesInner(newValues);
190
+ valuesRef.current = newValues;
191
+ historyIndexRef.current = newIndex;
192
+ }
193
+ }, []);
194
+
195
+ const redo = useCallback(() => {
196
+ if (historyIndexRef.current < historyRef.current.length - 1) {
197
+ const newIndex = historyIndexRef.current + 1;
198
+ const newValues = historyRef.current[newIndex];
199
+ setValuesInner(newValues);
200
+ valuesRef.current = newValues;
201
+ historyIndexRef.current = newIndex;
202
+ }
203
+ }, []);
204
+
205
+ const controllerRef = useRef<FormexController<T>>({} as FormexController<T>);
206
+
207
+ const controller = useMemo<FormexController<T>>(
208
+ () => ({
209
+ values,
210
+ initialValues: initialValuesRef.current,
211
+ handleChange,
212
+ isSubmitting,
213
+ setSubmitting: setIsSubmitting,
214
+ setValues,
215
+ setFieldValue,
216
+ errors,
217
+ setFieldError,
218
+ touched: touchedState,
219
+ setFieldTouched,
220
+ dirty,
221
+ setDirty,
222
+ handleSubmit: submit,
223
+ submitCount,
224
+ setSubmitCount,
225
+ handleBlur,
226
+ validate,
227
+ isValidating,
228
+ resetForm,
229
+ version,
230
+ debugId: debugIdRef.current,
231
+ undo,
232
+ redo,
233
+ canUndo: historyIndexRef.current > 0,
234
+ canRedo: historyIndexRef.current < historyRef.current.length - 1,
235
+ }),
236
+ [
237
+ values,
238
+ errors,
239
+ touchedState,
240
+ dirty,
241
+ isSubmitting,
242
+ submitCount,
243
+ isValidating,
244
+ version,
245
+ handleChange,
246
+ handleBlur,
247
+ setValues,
248
+ setFieldValue,
249
+ setFieldTouched,
250
+ setFieldError,
251
+ validate,
252
+ submit,
253
+ resetForm,
254
+ undo,
255
+ redo,
256
+ ]
257
+ );
258
+
259
+ useEffect(() => {
260
+ controllerRef.current = controller;
261
+ }, [controller]);
262
+
263
+ return controller;
150
264
  }
package/src/utils.ts CHANGED
@@ -1,5 +1,3 @@
1
- import * as React from "react";
2
-
3
1
  /** @private is the value an empty array? */
4
2
  export const isEmptyArray = (value?: any) =>
5
3
  Array.isArray(value) && value.length === 0;
@@ -16,49 +14,10 @@ export const isObject = (obj: any): obj is Object =>
16
14
  export const isInteger = (obj: any): boolean =>
17
15
  String(Math.floor(Number(obj))) === obj;
18
16
 
19
- /** @private is the given object a string? */
20
- export const isString = (obj: any): obj is string =>
21
- Object.prototype.toString.call(obj) === "[object String]";
22
-
23
17
  /** @private is the given object a NaN? */
24
18
  // eslint-disable-next-line no-self-compare
25
19
  export const isNaN = (obj: any): boolean => obj !== obj;
26
20
 
27
- /** @private Does a React component have exactly 0 children? */
28
- export const isEmptyChildren = (children: any): boolean =>
29
- React.Children.count(children) === 0;
30
-
31
- /** @private is the given object/value a promise? */
32
- export const isPromise = (value: any): value is PromiseLike<any> =>
33
- isObject(value) && isFunction(value.then);
34
-
35
- /** @private is the given object/value a type of synthetic event? */
36
- export const isInputEvent = (value: any): value is React.SyntheticEvent<any> =>
37
- value && isObject(value) && isObject(value.target);
38
-
39
- /**
40
- * Same as document.activeElement but wraps in a try-catch block. In IE it is
41
- * not safe to call document.activeElement if there is nothing focused.
42
- *
43
- * The activeElement will be null only if the document or document body is not
44
- * yet defined.
45
- *
46
- * @param {?Document} doc Defaults to current document.
47
- * @return {Element | null}
48
- * @see https://github.com/facebook/fbjs/blob/master/packages/fbjs/src/core/dom/getActiveElement.js
49
- */
50
- export function getActiveElement(doc?: Document): Element | null {
51
- doc = doc || (typeof document !== "undefined" ? document : undefined);
52
- if (typeof doc === "undefined") {
53
- return null;
54
- }
55
- try {
56
- return doc.activeElement || doc.body;
57
- } catch (e) {
58
- return doc.body;
59
- }
60
- }
61
-
62
21
  /**
63
22
  * Deeply get a value from an object via its path.
64
23
  */
@@ -120,39 +79,7 @@ export function setIn(obj: any, path: string, value: any): any {
120
79
  return res;
121
80
  }
122
81
 
123
- /**
124
- * Recursively a set the same value for all keys and arrays nested object, cloning
125
- * @param object
126
- * @param value
127
- * @param visited
128
- * @param response
129
- */
130
- export function setNestedObjectValues<T>(
131
- object: any,
132
- value: any,
133
- visited: any = new WeakMap(),
134
- response: any = {}
135
- ): T {
136
- for (const k of Object.keys(object)) {
137
- const val = object[k];
138
- if (isObject(val)) {
139
- if (!visited.get(val)) {
140
- visited.set(val, true);
141
- // In order to keep array values consistent for both dot path and
142
- // bracket syntax, we need to check if this is an array so that
143
- // this will output { friends: [true] } and not { friends: { "0": true } }
144
- response[k] = Array.isArray(val) ? [] : {};
145
- setNestedObjectValues(val, value, visited, response[k]);
146
- }
147
- } else {
148
- response[k] = value;
149
- }
150
- }
151
-
152
- return response;
153
- }
154
-
155
- function clone(value: any) {
82
+ export function clone(value: any) {
156
83
  if (Array.isArray(value)) {
157
84
  return [...value];
158
85
  } else if (typeof value === "object" && value !== null) {