@rebasepro/forms 0.15.0 → 0.16.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/dist/index.es.js CHANGED
@@ -154,6 +154,8 @@ function useCreateFormex({ initialValues, initialModifiedValues, initialErrors,
154
154
  const debugIdRef = useRef(debugId);
155
155
  const [values, setValuesInner] = useState(startValues);
156
156
  const [touchedState, setTouchedState] = useState(initialTouched ?? {});
157
+ const touchedRef = useRef(touchedState);
158
+ touchedRef.current = touchedState;
157
159
  const [errors, setErrors] = useState(initialErrors ?? {});
158
160
  const [dirty, setDirty] = useState(initialDirty ?? !deepEqual(initialValues, startValues));
159
161
  const [submitCount, setSubmitCount] = useState(0);
@@ -171,8 +173,17 @@ function useCreateFormex({ initialValues, initialModifiedValues, initialErrors,
171
173
  }, 300);
172
174
  }
173
175
  }, []);
174
- const historyRef = useRef([startValues]);
176
+ const historyRef = useRef([{ values: startValues }]);
175
177
  const historyIndexRef = useRef(0);
178
+ /**
179
+ * Record a new state, dropping anything that had been undone past it.
180
+ */
181
+ const pushHistory = useCallback((entry) => {
182
+ const newHistory = historyRef.current.slice(0, historyIndexRef.current + 1);
183
+ newHistory.push(entry);
184
+ historyRef.current = newHistory;
185
+ historyIndexRef.current = newHistory.length - 1;
186
+ }, []);
176
187
  useEffect(() => {
177
188
  if (validateOnInitialRender) validate();
178
189
  }, []);
@@ -180,12 +191,9 @@ function useCreateFormex({ initialValues, initialModifiedValues, initialErrors,
180
191
  valuesRef.current = newValues;
181
192
  setValuesInner(newValues);
182
193
  setDirty(!deepEqual(initialValuesRef.current, newValues));
183
- const newHistory = historyRef.current.slice(0, historyIndexRef.current + 1);
184
- newHistory.push(newValues);
185
- historyRef.current = newHistory;
186
- historyIndexRef.current = newHistory.length - 1;
194
+ pushHistory({ values: newValues });
187
195
  callDebouncedOnValuesChange(newValues);
188
- }, [callDebouncedOnValuesChange]);
196
+ }, [callDebouncedOnValuesChange, pushHistory]);
189
197
  const validate = useCallback(async () => {
190
198
  setIsValidating(true);
191
199
  const validationErrors = await validation?.(valuesRef.current);
@@ -199,12 +207,13 @@ function useCreateFormex({ initialValues, initialModifiedValues, initialErrors,
199
207
  setValuesInner(newValues);
200
208
  if (!deepEqual(getIn(initialValuesRef.current, key), value)) setDirty(true);
201
209
  if (shouldValidate) validate();
202
- const newHistory = historyRef.current.slice(0, historyIndexRef.current + 1);
203
- newHistory.push(newValues);
204
- historyRef.current = newHistory;
205
- historyIndexRef.current = newHistory.length - 1;
210
+ pushHistory({ values: newValues });
206
211
  callDebouncedOnValuesChange(newValues);
207
- }, [validate, callDebouncedOnValuesChange]);
212
+ }, [
213
+ validate,
214
+ callDebouncedOnValuesChange,
215
+ pushHistory
216
+ ]);
208
217
  const setFieldError = useCallback((key, error) => {
209
218
  setErrors((prevErrors) => {
210
219
  const newErrors = { ...prevErrors };
@@ -253,18 +262,37 @@ function useCreateFormex({ initialValues, initialModifiedValues, initialErrors,
253
262
  setVersion((prev) => prev + 1);
254
263
  }, [onSubmit, validation]);
255
264
  const resetForm = useCallback((props) => {
256
- const { submitCount: submitCountProp, values: valuesProp, errors: errorsProp, touched: touchedProp } = props ?? {};
257
- valuesRef.current = valuesProp ?? initialValuesRef.current;
258
- initialValuesRef.current = valuesProp ?? initialValuesRef.current;
259
- setValuesInner(valuesProp ?? initialValuesRef.current);
265
+ const { submitCount: submitCountProp, values: valuesProp, errors: errorsProp, touched: touchedProp, undoable } = props ?? {};
266
+ const priorValues = valuesRef.current;
267
+ const priorTouched = touchedRef.current;
268
+ const nextValues = valuesProp ?? initialValuesRef.current;
269
+ const nextTouched = touchedProp ?? initialTouched ?? {};
270
+ valuesRef.current = nextValues;
271
+ initialValuesRef.current = nextValues;
272
+ setValuesInner(nextValues);
260
273
  setErrors(errorsProp ?? {});
261
- setTouchedState(touchedProp ?? initialTouched ?? {});
274
+ setTouchedState(nextTouched);
262
275
  setDirty(false);
263
276
  setSubmitCount(submitCountProp ?? 0);
264
277
  setVersion((prev) => prev + 1);
265
278
  onReset?.(controllerRef.current);
266
- historyRef.current = [valuesProp ?? initialValuesRef.current];
267
- historyIndexRef.current = 0;
279
+ if (undoable) {
280
+ const kept = historyRef.current.slice(0, historyIndexRef.current + 1);
281
+ kept[kept.length - 1] = {
282
+ values: priorValues,
283
+ touched: priorTouched
284
+ };
285
+ kept.push({
286
+ values: nextValues,
287
+ touched: nextTouched,
288
+ boundary: true
289
+ });
290
+ historyRef.current = kept;
291
+ historyIndexRef.current = kept.length - 1;
292
+ } else {
293
+ historyRef.current = [{ values: nextValues }];
294
+ historyIndexRef.current = 0;
295
+ }
268
296
  }, [onReset, initialTouched]);
269
297
  /**
270
298
  * The `initialValues` prop moved: the record this form edits finished
@@ -293,34 +321,30 @@ function useCreateFormex({ initialValues, initialModifiedValues, initialErrors,
293
321
  else {
294
322
  valuesRef.current = initialValues;
295
323
  setValuesInner(initialValues);
296
- historyRef.current = [initialValues];
324
+ historyRef.current = [{ values: initialValues }];
297
325
  historyIndexRef.current = 0;
298
326
  setDirty(false);
299
327
  }
300
328
  setVersion((prev) => prev + 1);
301
329
  }, [initialValues]);
302
- const undo = useCallback(() => {
303
- if (historyIndexRef.current > 0) {
304
- const newIndex = historyIndexRef.current - 1;
305
- const newValues = historyRef.current[newIndex];
306
- setValuesInner(newValues);
307
- valuesRef.current = newValues;
308
- historyIndexRef.current = newIndex;
309
- setDirty(!deepEqual(initialValuesRef.current, newValues));
310
- callDebouncedOnValuesChange(newValues);
311
- }
330
+ const stepHistory = useCallback((newIndex) => {
331
+ const from = historyRef.current[historyIndexRef.current];
332
+ const entry = historyRef.current[newIndex];
333
+ const newValues = entry.values;
334
+ setValuesInner(newValues);
335
+ valuesRef.current = newValues;
336
+ historyIndexRef.current = newIndex;
337
+ setDirty(!deepEqual(initialValuesRef.current, newValues));
338
+ if (entry.touched) setTouchedState(entry.touched);
339
+ if (from?.boundary || entry.boundary) setVersion((prev) => prev + 1);
340
+ callDebouncedOnValuesChange(newValues);
312
341
  }, [callDebouncedOnValuesChange]);
342
+ const undo = useCallback(() => {
343
+ if (historyIndexRef.current > 0) stepHistory(historyIndexRef.current - 1);
344
+ }, [stepHistory]);
313
345
  const redo = useCallback(() => {
314
- if (historyIndexRef.current < historyRef.current.length - 1) {
315
- const newIndex = historyIndexRef.current + 1;
316
- const newValues = historyRef.current[newIndex];
317
- setValuesInner(newValues);
318
- valuesRef.current = newValues;
319
- historyIndexRef.current = newIndex;
320
- setDirty(!deepEqual(initialValuesRef.current, newValues));
321
- callDebouncedOnValuesChange(newValues);
322
- }
323
- }, [callDebouncedOnValuesChange]);
346
+ if (historyIndexRef.current < historyRef.current.length - 1) stepHistory(historyIndexRef.current + 1);
347
+ }, [stepHistory]);
324
348
  const controllerRef = useRef({});
325
349
  const controller = useMemo(() => ({
326
350
  values,
@@ -1 +1 @@
1
- {"version":3,"file":"index.es.js","names":[],"sources":["../src/Formex.tsx","../src/utils.ts","../src/Field.tsx","../src/useCreateFormex.tsx"],"sourcesContent":["import React, { useContext } from \"react\";\nimport { FormexController } from \"./types\";\n\n\nconst FormexContext = React.createContext<FormexController<any> | null>(null);\n\nexport const useFormex = <T = any>() => {\n const ctx = useContext(FormexContext);\n if (!ctx) throw new Error(\"useFormex must be used within a Formex provider\");\n return ctx as FormexController<T>;\n};\n\nexport const Formex = <T = any>({ value, children }: { value: FormexController<T>, children: React.ReactNode }) => {\n return <FormexContext.Provider value={value}>{children}</FormexContext.Provider>;\n};\n","/** @private is the value an empty array? */\nexport const isEmptyArray = (value?: unknown) =>\n Array.isArray(value) && value.length === 0;\n\n/** @private is the given object a Function? */\n\nexport const isFunction = (obj: unknown): obj is Function =>\n typeof obj === \"function\";\n\n/** @private is the given object an Object? */\nexport const isObject = (obj: unknown): obj is Record<string, unknown> =>\n obj !== null && typeof obj === \"object\";\n\n/** @private is the given object an integer? */\nexport const isInteger = (obj: unknown): boolean =>\n String(Math.floor(Number(obj))) === obj;\n\n/** @private is the given object a NaN? */\n\nexport const isNaN = (obj: unknown): boolean => obj !== obj;\n\n/**\n * Deeply get a value from an object via its path.\n */\nexport function getIn(\n obj: unknown,\n key: string | string[],\n def?: unknown,\n p = 0\n): unknown {\n // The read counterpart. `getIn(values, \"constructor.prototype\")` handing\n // back `Object.prototype` is how a polluted value gets read back out, and\n // how a form comes to render one.\n if (pathTraversesPrototype(key)) return def;\n\n const path = toPath(key);\n let current: unknown = obj;\n while (current && p < path.length) {\n current = (current as Record<string, unknown>)[path[p++]];\n }\n\n // check if path is not in the end\n if (p !== path.length && !current) {\n return def;\n }\n\n return current === undefined ? def : current;\n}\n\nexport function setIn(obj: unknown, path: string, value: unknown): unknown {\n // Refused rather than sanitised: there is no legitimate reading of a form\n // field whose path names the prototype chain, and silently rewriting the\n // path would write the value somewhere the caller did not ask for.\n // Returning the original object is what every other no-op in this function\n // does.\n if (pathTraversesPrototype(path)) return obj;\n\n const res = clone(obj) as Record<string, unknown>; // this keeps inheritance when obj is a class\n let resVal: Record<string, unknown> = res;\n let i = 0;\n const pathArray = toPath(path);\n\n for (; i < pathArray.length - 1; i++) {\n const currentPath: string = pathArray[i];\n const currentObj = getIn(obj, pathArray.slice(0, i + 1));\n\n if (currentObj && (isObject(currentObj) || Array.isArray(currentObj))) {\n resVal = resVal[currentPath] = clone(currentObj) as Record<string, unknown>;\n } else {\n const nextPath: string = pathArray[i + 1];\n resVal = resVal[currentPath] =\n (isInteger(nextPath) && Number(nextPath) >= 0 ? [] : {}) as Record<string, unknown>;\n }\n }\n\n // Return original object if new value is the same as current\n if ((i === 0 ? (obj as Record<string, unknown>) : resVal)[pathArray[i]] === value) {\n return obj;\n }\n\n if (value === undefined) {\n delete resVal[pathArray[i]];\n } else {\n resVal[pathArray[i]] = value;\n }\n\n // If the path array has a single element, the loop did not run.\n // Deleting on `resVal` had no effect in this scenario, so we delete on the result instead.\n if (i === 0 && value === undefined) {\n delete res[pathArray[i]];\n }\n\n return res;\n}\n\nexport function clone(value: unknown): unknown {\n if (Array.isArray(value)) {\n return [...value];\n } else if (typeof value === \"object\" && value !== null) {\n // Preserve class instances (EntityReference, GeoPoint, etc.) - don't spread them\n if (Object.getPrototypeOf(value) !== Object.prototype) {\n return value;\n }\n return { ...(value as Record<string, unknown>) };\n } else {\n return value; // This is for primitive types which do not need cloning.\n }\n}\n\n/**\n * Segments that reach the prototype chain rather than a property of the object.\n *\n * `res[\"__proto__\"] = x` is a setter for the object's prototype, not an own\n * property, so a path of `__proto__.polluted` wrote straight onto\n * `Object.prototype` and gave every object in the process a `polluted`\n * property. `constructor.prototype.x` arrived by a second route, and\n * `__proto__.0` did it to arrays.\n *\n * These are paths, and a path here is a property key — which for a map property\n * or a column mapped out of an imported CSV is data, not code.\n */\nconst UNSAFE_PATH_SEGMENTS = new Set([\"__proto__\", \"constructor\", \"prototype\"]);\n\n/** Whether any segment of this path would traverse the prototype chain. */\nexport function pathTraversesPrototype(path: string | string[]): boolean {\n return toPath(path).some(segment => UNSAFE_PATH_SEGMENTS.has(segment));\n}\n\nfunction toPath(value: string | string[]) {\n if (Array.isArray(value)) return value; // Already in path array form.\n // Replace brackets with dots, remove leading/trailing dots, then split by dot.\n return value.replace(/\\[(\\d+)]/g, \".$1\").replace(/^\\./, \"\").replace(/\\.$/, \"\").split(\".\");\n}\n","import * as React from \"react\";\nimport { useFormex } from \"./Formex\";\nimport { getIn, isFunction, isObject } from \"./utils\";\nimport { FormexController } from \"./types\";\n\nexport interface FieldInputProps<Value> {\n /** Value of the field */\n value: Value;\n /** Name of the field */\n name: string;\n /** Multiple select? */\n multiple?: boolean;\n /** Is the field checked? */\n checked?: boolean;\n /** Change event handler */\n onChange: (event: React.SyntheticEvent) => void,\n /** Blur event handler */\n onBlur: (event: React.FocusEvent) => void,\n}\n\nexport interface FormexFieldProps<Value = any, FormValues extends object = object> {\n field: FieldInputProps<Value>;\n form: FormexController<FormValues>;\n}\n\nexport interface FieldConfig<Value, C extends React.ElementType | undefined = undefined> {\n\n /**\n * Component to render. Can either be a string e.g. 'select', 'input', or 'textarea', or a component.\n */\n as?:\n | C\n | string\n | React.ForwardRefExoticComponent<Record<string, unknown>>;\n\n /**\n * Children render function <Field name>{props => ...}</Field>)\n */\n children?: ((props: FormexFieldProps<Value>) => React.ReactNode) | React.ReactNode;\n\n /**\n * Validate a single field value independently\n */\n // validate?: FieldValidator;\n\n /**\n * Used for 'select' and related input types.\n */\n multiple?: boolean;\n\n /**\n * Field name\n */\n name: string;\n\n /** HTML input type */\n type?: string;\n\n /** Field value */\n value?: unknown;\n\n /** Inner ref */\n innerRef?: (instance: unknown) => void;\n\n}\n\nexport type FieldProps<T, C extends React.ElementType | undefined> = {\n as?: C;\n} & (C extends React.ElementType ? (React.ComponentProps<C> & FieldConfig<T, C>) : FieldConfig<T, C>);\n\nexport function Field<T, C extends React.ElementType | undefined = undefined>({\n validate,\n name,\n children,\n as: is, // `as` is reserved in typescript lol\n // component,\n className,\n ...props\n }: FieldProps<T, C>) {\n const formex = useFormex();\n\n const field = getFieldProps({ name,\n...props }, formex);\n\n if (isFunction(children)) {\n return children({ field,\nform: formex });\n }\n\n // if (component) {\n // if (typeof component === \"string\") {\n // const { innerRef, ...rest } = props;\n // return React.createElement(\n // component,\n // { ref: innerRef, ...field, ...rest, className },\n // children\n // );\n // }\n // return React.createElement(\n // component,\n // { field, form: formex, ...props, className },\n // children\n // );\n // }\n\n // default to input here so we can check for both `as` and `children` above\n const asElement = is || \"input\";\n\n if (typeof asElement === \"string\") {\n const { innerRef, ...rest } = props;\n return React.createElement(\n asElement,\n { ref: innerRef,\n...field,\n...rest,\nclassName },\n children\n );\n }\n\n return React.createElement(asElement, { ...field,\n...props,\nclassName }, children);\n}\n\nconst getFieldProps = (nameOrOptions: string | FieldConfig<unknown>, formex: FormexController<object>): FieldInputProps<unknown> => {\n const name: string = typeof nameOrOptions === \"string\"\n ? nameOrOptions\n : nameOrOptions.name;\n const valueState = getIn(formex.values as Record<string, unknown>, name);\n\n const field: FieldInputProps<unknown> = {\n name: name as string,\n value: valueState,\n onChange: formex.handleChange,\n onBlur: formex.handleBlur\n };\n if (typeof nameOrOptions !== \"string\") {\n const {\n type,\n value: valueProp, // value is special for checkboxes\n as: is,\n multiple\n } = nameOrOptions as FieldConfig<unknown>;\n\n if (type === \"checkbox\") {\n if (valueProp === undefined) {\n field.checked = !!valueState;\n } else {\n field.checked = !!(\n Array.isArray(valueState) && ~valueState.indexOf(valueProp)\n );\n field.value = valueProp;\n }\n } else if (type === \"radio\") {\n field.checked = valueState === valueProp;\n field.value = valueProp;\n } else if (is === \"select\" && multiple) {\n field.value = field.value || [];\n field.multiple = true;\n }\n }\n return field;\n};\n","import React, { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { getIn, setIn } from \"./utils\";\nimport { deepEqual as equal } from \"fast-equals\";\n\nimport { FormexController, FormexResetProps } from \"./types\";\n\nexport function useCreateFormex<T = any>({\n initialValues,\n initialModifiedValues,\n initialErrors,\n initialDirty,\n initialTouched,\n validation,\n validateOnChange = false,\n validateOnInitialRender = false,\n onSubmit,\n onReset,\n onValuesChangeDeferred,\n debugId\n}: {\n /**\n * The **baseline**: what the values are stored as. Everything the form\n * calls \"dirty\" is a difference from this, so it has to be the stored\n * record and nothing else. To open a form already carrying an edit, pass\n * that edit as {@link initialModifiedValues} — folding it into\n * `initialValues` instead makes the baseline agree with the edit, and then\n * nothing can tell that the edit is unsaved.\n */\n initialValues: T;\n /**\n * What the form should *show* on its first render, when that is not the\n * baseline — an edit handed over from somewhere else, a draft restored\n * from a cache. Dirty is computed from the difference.\n */\n initialModifiedValues?: T;\n initialErrors?: Record<string, string>;\n /**\n * Force the starting dirty state. Only for callers that know the form\n * opens modified but cannot supply the modified values; prefer\n * {@link initialModifiedValues}, which lets it be derived.\n */\n initialDirty?: boolean;\n initialTouched?: Record<string, boolean>;\n validateOnChange?: boolean;\n validateOnInitialRender?: boolean;\n validation?: (\n values: T\n ) =>\n | Record<string, string>\n | Promise<Record<string, string>>\n | undefined\n | void;\n onValuesChangeDeferred?: (values: T, controller: FormexController<T>) => void;\n onSubmit?: (values: T, controller: FormexController<T>) => void | Promise<void>;\n onReset?: (controller: FormexController<T>) => void | Promise<void>;\n debugId?: string;\n}): FormexController<T> {\n // The baseline and the current values start apart when the form opens\n // already carrying an edit. Keeping them separate is what lets the dirty\n // flag be *derived* rather than asserted, and what lets the baseline be\n // replaced later without touching what the user is looking at.\n const startValues = initialModifiedValues ?? initialValues;\n\n const initialValuesRef = useRef<T>(initialValues);\n const valuesRef = useRef<T>(startValues);\n const debugIdRef = useRef<string | undefined>(debugId);\n\n const [values, setValuesInner] = useState<T>(startValues);\n const [touchedState, setTouchedState] = useState<Record<string, boolean>>(initialTouched ?? {});\n const [errors, setErrors] = useState<Record<string, string>>(initialErrors ?? {});\n const [dirty, setDirty] = useState(initialDirty ?? !equal(initialValues, startValues));\n const [submitCount, setSubmitCount] = useState(0);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [isValidating, setIsValidating] = useState(false);\n const [version, setVersion] = useState(0);\n\n const onValuesChangeRef = useRef(onValuesChangeDeferred);\n onValuesChangeRef.current = onValuesChangeDeferred;\n const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n const callDebouncedOnValuesChange = useCallback((values: T) => {\n if (onValuesChangeRef.current) {\n if (debounceTimeoutRef.current) {\n clearTimeout(debounceTimeoutRef.current);\n }\n debounceTimeoutRef.current = setTimeout(() => {\n onValuesChangeRef.current?.(values, controllerRef.current);\n }, 300);\n }\n }, []);\n\n // Replace state for history with refs\n const historyRef = useRef<T[]>([startValues]);\n const historyIndexRef = useRef<number>(0);\n\n useEffect(() => {\n if (validateOnInitialRender) {\n validate();\n }\n }, []);\n\n const setValues = useCallback((newValues: T) => {\n valuesRef.current = newValues;\n setValuesInner(newValues);\n setDirty(!equal(initialValuesRef.current, newValues));\n // Update history using refs\n const newHistory = historyRef.current.slice(0, historyIndexRef.current + 1);\n newHistory.push(newValues);\n historyRef.current = newHistory;\n historyIndexRef.current = newHistory.length - 1;\n callDebouncedOnValuesChange(newValues);\n }, [callDebouncedOnValuesChange]);\n\n const validate = useCallback(async () => {\n setIsValidating(true);\n const validationErrors = await validation?.(valuesRef.current);\n setErrors(validationErrors ?? {});\n setIsValidating(false);\n return validationErrors;\n }, [validation]);\n\n const setFieldValue = useCallback(\n (key: string, value: unknown, shouldValidate?: boolean) => {\n const newValues = setIn(valuesRef.current as Record<string, unknown>, key, value) as T;\n valuesRef.current = newValues;\n setValuesInner(newValues);\n if (!equal(getIn(initialValuesRef.current as Record<string, unknown>, key), value)) {\n setDirty(true);\n }\n if (shouldValidate) {\n validate();\n }\n // Update history using refs\n const newHistory = historyRef.current.slice(0, historyIndexRef.current + 1);\n newHistory.push(newValues);\n historyRef.current = newHistory;\n historyIndexRef.current = newHistory.length - 1;\n callDebouncedOnValuesChange(newValues);\n },\n [validate, callDebouncedOnValuesChange]\n );\n\n const setFieldError = useCallback((key: string, error: string | undefined) => {\n setErrors((prevErrors: Record<string, string>) => {\n const newErrors = { ...prevErrors };\n if (error) {\n newErrors[key] = error;\n } else {\n delete newErrors[key];\n }\n return newErrors;\n });\n }, []);\n\n const setFieldTouched = useCallback(\n (key: string, touched: boolean, shouldValidate?: boolean) => {\n setTouchedState((prev: Record<string, boolean>) => ({\n ...prev,\n [key]: touched\n }));\n if (shouldValidate) {\n validate();\n }\n },\n [validate]\n );\n\n const handleChange = useCallback(\n (event: React.SyntheticEvent) => {\n const target = event.target as HTMLInputElement;\n let value;\n if (target.type === \"checkbox\") {\n value = target.checked;\n } else if (target.type === \"number\") {\n value = target.valueAsNumber;\n } else {\n value = target.value;\n }\n const name = target.name;\n setFieldValue(name, value, validateOnChange);\n setFieldTouched(name, true);\n },\n [setFieldValue, setFieldTouched, validateOnChange]\n );\n\n const handleBlur = useCallback((event: React.FocusEvent) => {\n const target = event.target as HTMLInputElement;\n const name = target.name;\n setFieldTouched(name, true);\n }, [setFieldTouched]);\n\n const submit = useCallback(\n async (e?: React.FormEvent<HTMLFormElement>) => {\n e?.preventDefault();\n e?.stopPropagation();\n setIsSubmitting(true);\n setSubmitCount((prev: number) => prev + 1);\n const validationErrors = await validation?.(valuesRef.current);\n if (validationErrors && Object.keys(validationErrors).length > 0) {\n setErrors(validationErrors);\n } else {\n setErrors({});\n await onSubmit?.(valuesRef.current, controllerRef.current);\n }\n setIsSubmitting(false);\n setVersion((prev: number) => prev + 1);\n },\n [onSubmit, validation]\n );\n\n const resetForm = useCallback((props?: FormexResetProps<T>) => {\n const {\n submitCount: submitCountProp,\n values: valuesProp,\n errors: errorsProp,\n touched: touchedProp\n } = props ?? {};\n valuesRef.current = valuesProp ?? initialValuesRef.current;\n initialValuesRef.current = valuesProp ?? initialValuesRef.current;\n setValuesInner(valuesProp ?? initialValuesRef.current);\n setErrors(errorsProp ?? {});\n setTouchedState(touchedProp ?? initialTouched ?? {});\n setDirty(false);\n setSubmitCount(submitCountProp ?? 0);\n setVersion((prev: number) => prev + 1);\n onReset?.(controllerRef.current);\n // Reset history with refs\n historyRef.current = [valuesProp ?? initialValuesRef.current];\n historyIndexRef.current = 0;\n }, [onReset, initialTouched]);\n\n /**\n * The `initialValues` prop moved: the record this form edits finished\n * loading, or was replaced. That is a **re-baseline**, not a reset.\n *\n * It used to call `resetForm({ values: initialValues })`, which is a reset\n * in both of the ways that matter, and both were wrong here:\n *\n * - it fired `onReset`, which callers reasonably read as \"the user\n * discarded their changes\". The admin clears the cache that seeds an\n * in-flight edit handed over from the side panel there, so a record's own\n * data arriving deleted the edit the form had just been opened with, and\n * the form then re-seeded itself from the server — an expanded record\n * silently lost whatever had been typed into it.\n * - it overwrote `values`, so anything typed while the record was still\n * loading was thrown away without a word.\n *\n * So: move the baseline, leave the edit alone, and re-judge one against\n * the other. Only an untouched form follows the baseline to its new value.\n */\n useEffect(() => {\n if (equal(initialValuesRef.current, initialValues)) return;\n\n const modified = !equal(initialValuesRef.current, valuesRef.current);\n initialValuesRef.current = initialValues;\n\n if (modified) {\n setDirty(!equal(initialValues, valuesRef.current));\n } else {\n valuesRef.current = initialValues;\n setValuesInner(initialValues);\n historyRef.current = [initialValues];\n historyIndexRef.current = 0;\n setDirty(false);\n }\n // Containers that read `values` off the controller key on `version`;\n // a re-seed changes what they are holding just as a reset does.\n setVersion((prev: number) => prev + 1);\n }, [initialValues]);\n\n const undo = useCallback(() => {\n if (historyIndexRef.current > 0) {\n const newIndex = historyIndexRef.current - 1;\n const newValues = historyRef.current[newIndex];\n setValuesInner(newValues);\n valuesRef.current = newValues;\n historyIndexRef.current = newIndex;\n setDirty(!equal(initialValuesRef.current, newValues));\n callDebouncedOnValuesChange(newValues);\n }\n }, [callDebouncedOnValuesChange]);\n\n const redo = useCallback(() => {\n if (historyIndexRef.current < historyRef.current.length - 1) {\n const newIndex = historyIndexRef.current + 1;\n const newValues = historyRef.current[newIndex];\n setValuesInner(newValues);\n valuesRef.current = newValues;\n historyIndexRef.current = newIndex;\n setDirty(!equal(initialValuesRef.current, newValues));\n callDebouncedOnValuesChange(newValues);\n }\n }, [callDebouncedOnValuesChange]);\n\n const controllerRef = useRef<FormexController<T>>({} as FormexController<T>);\n\n const controller = useMemo<FormexController<T>>(\n () => ({\n values,\n initialValues: initialValuesRef.current,\n handleChange,\n isSubmitting,\n setSubmitting: setIsSubmitting,\n setValues,\n setFieldValue,\n errors,\n setFieldError,\n touched: touchedState,\n setFieldTouched,\n setTouched: setTouchedState,\n dirty,\n setDirty,\n handleSubmit: submit,\n submitCount,\n setSubmitCount,\n handleBlur,\n validate,\n isValidating,\n resetForm,\n version,\n debugId: debugIdRef.current,\n undo,\n redo,\n canUndo: historyIndexRef.current > 0,\n canRedo: historyIndexRef.current < historyRef.current.length - 1\n }),\n [\n values,\n errors,\n touchedState,\n dirty,\n isSubmitting,\n submitCount,\n isValidating,\n version,\n handleChange,\n handleBlur,\n setValues,\n setFieldValue,\n setFieldTouched,\n setTouchedState,\n setFieldError,\n validate,\n submit,\n resetForm,\n undo,\n redo\n ]\n );\n\n useEffect(() => {\n controllerRef.current = controller;\n }, [controller]);\n\n return controller;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.es.js","names":[],"sources":["../src/Formex.tsx","../src/utils.ts","../src/Field.tsx","../src/useCreateFormex.tsx"],"sourcesContent":["import React, { useContext } from \"react\";\nimport { FormexController } from \"./types\";\n\n\nconst FormexContext = React.createContext<FormexController<any> | null>(null);\n\nexport const useFormex = <T = any>() => {\n const ctx = useContext(FormexContext);\n if (!ctx) throw new Error(\"useFormex must be used within a Formex provider\");\n return ctx as FormexController<T>;\n};\n\nexport const Formex = <T = any>({ value, children }: { value: FormexController<T>, children: React.ReactNode }) => {\n return <FormexContext.Provider value={value}>{children}</FormexContext.Provider>;\n};\n","/** @private is the value an empty array? */\nexport const isEmptyArray = (value?: unknown) =>\n Array.isArray(value) && value.length === 0;\n\n/** @private is the given object a Function? */\n\nexport const isFunction = (obj: unknown): obj is Function =>\n typeof obj === \"function\";\n\n/** @private is the given object an Object? */\nexport const isObject = (obj: unknown): obj is Record<string, unknown> =>\n obj !== null && typeof obj === \"object\";\n\n/** @private is the given object an integer? */\nexport const isInteger = (obj: unknown): boolean =>\n String(Math.floor(Number(obj))) === obj;\n\n/** @private is the given object a NaN? */\n\nexport const isNaN = (obj: unknown): boolean => obj !== obj;\n\n/**\n * Deeply get a value from an object via its path.\n */\nexport function getIn(\n obj: unknown,\n key: string | string[],\n def?: unknown,\n p = 0\n): unknown {\n // The read counterpart. `getIn(values, \"constructor.prototype\")` handing\n // back `Object.prototype` is how a polluted value gets read back out, and\n // how a form comes to render one.\n if (pathTraversesPrototype(key)) return def;\n\n const path = toPath(key);\n let current: unknown = obj;\n while (current && p < path.length) {\n current = (current as Record<string, unknown>)[path[p++]];\n }\n\n // check if path is not in the end\n if (p !== path.length && !current) {\n return def;\n }\n\n return current === undefined ? def : current;\n}\n\nexport function setIn(obj: unknown, path: string, value: unknown): unknown {\n // Refused rather than sanitised: there is no legitimate reading of a form\n // field whose path names the prototype chain, and silently rewriting the\n // path would write the value somewhere the caller did not ask for.\n // Returning the original object is what every other no-op in this function\n // does.\n if (pathTraversesPrototype(path)) return obj;\n\n const res = clone(obj) as Record<string, unknown>; // this keeps inheritance when obj is a class\n let resVal: Record<string, unknown> = res;\n let i = 0;\n const pathArray = toPath(path);\n\n for (; i < pathArray.length - 1; i++) {\n const currentPath: string = pathArray[i];\n const currentObj = getIn(obj, pathArray.slice(0, i + 1));\n\n if (currentObj && (isObject(currentObj) || Array.isArray(currentObj))) {\n resVal = resVal[currentPath] = clone(currentObj) as Record<string, unknown>;\n } else {\n const nextPath: string = pathArray[i + 1];\n resVal = resVal[currentPath] =\n (isInteger(nextPath) && Number(nextPath) >= 0 ? [] : {}) as Record<string, unknown>;\n }\n }\n\n // Return original object if new value is the same as current\n if ((i === 0 ? (obj as Record<string, unknown>) : resVal)[pathArray[i]] === value) {\n return obj;\n }\n\n if (value === undefined) {\n delete resVal[pathArray[i]];\n } else {\n resVal[pathArray[i]] = value;\n }\n\n // If the path array has a single element, the loop did not run.\n // Deleting on `resVal` had no effect in this scenario, so we delete on the result instead.\n if (i === 0 && value === undefined) {\n delete res[pathArray[i]];\n }\n\n return res;\n}\n\nexport function clone(value: unknown): unknown {\n if (Array.isArray(value)) {\n return [...value];\n } else if (typeof value === \"object\" && value !== null) {\n // Preserve class instances (EntityReference, GeoPoint, etc.) - don't spread them\n if (Object.getPrototypeOf(value) !== Object.prototype) {\n return value;\n }\n return { ...(value as Record<string, unknown>) };\n } else {\n return value; // This is for primitive types which do not need cloning.\n }\n}\n\n/**\n * Segments that reach the prototype chain rather than a property of the object.\n *\n * `res[\"__proto__\"] = x` is a setter for the object's prototype, not an own\n * property, so a path of `__proto__.polluted` wrote straight onto\n * `Object.prototype` and gave every object in the process a `polluted`\n * property. `constructor.prototype.x` arrived by a second route, and\n * `__proto__.0` did it to arrays.\n *\n * These are paths, and a path here is a property key — which for a map property\n * or a column mapped out of an imported CSV is data, not code.\n */\nconst UNSAFE_PATH_SEGMENTS = new Set([\"__proto__\", \"constructor\", \"prototype\"]);\n\n/** Whether any segment of this path would traverse the prototype chain. */\nexport function pathTraversesPrototype(path: string | string[]): boolean {\n return toPath(path).some(segment => UNSAFE_PATH_SEGMENTS.has(segment));\n}\n\nfunction toPath(value: string | string[]) {\n if (Array.isArray(value)) return value; // Already in path array form.\n // Replace brackets with dots, remove leading/trailing dots, then split by dot.\n return value.replace(/\\[(\\d+)]/g, \".$1\").replace(/^\\./, \"\").replace(/\\.$/, \"\").split(\".\");\n}\n","import * as React from \"react\";\nimport { useFormex } from \"./Formex\";\nimport { getIn, isFunction, isObject } from \"./utils\";\nimport { FormexController } from \"./types\";\n\nexport interface FieldInputProps<Value> {\n /** Value of the field */\n value: Value;\n /** Name of the field */\n name: string;\n /** Multiple select? */\n multiple?: boolean;\n /** Is the field checked? */\n checked?: boolean;\n /** Change event handler */\n onChange: (event: React.SyntheticEvent) => void,\n /** Blur event handler */\n onBlur: (event: React.FocusEvent) => void,\n}\n\nexport interface FormexFieldProps<Value = any, FormValues extends object = object> {\n field: FieldInputProps<Value>;\n form: FormexController<FormValues>;\n}\n\nexport interface FieldConfig<Value, C extends React.ElementType | undefined = undefined> {\n\n /**\n * Component to render. Can either be a string e.g. 'select', 'input', or 'textarea', or a component.\n */\n as?:\n | C\n | string\n | React.ForwardRefExoticComponent<Record<string, unknown>>;\n\n /**\n * Children render function <Field name>{props => ...}</Field>)\n */\n children?: ((props: FormexFieldProps<Value>) => React.ReactNode) | React.ReactNode;\n\n /**\n * Validate a single field value independently\n */\n // validate?: FieldValidator;\n\n /**\n * Used for 'select' and related input types.\n */\n multiple?: boolean;\n\n /**\n * Field name\n */\n name: string;\n\n /** HTML input type */\n type?: string;\n\n /** Field value */\n value?: unknown;\n\n /** Inner ref */\n innerRef?: (instance: unknown) => void;\n\n}\n\nexport type FieldProps<T, C extends React.ElementType | undefined> = {\n as?: C;\n} & (C extends React.ElementType ? (React.ComponentProps<C> & FieldConfig<T, C>) : FieldConfig<T, C>);\n\nexport function Field<T, C extends React.ElementType | undefined = undefined>({\n validate,\n name,\n children,\n as: is, // `as` is reserved in typescript lol\n // component,\n className,\n ...props\n }: FieldProps<T, C>) {\n const formex = useFormex();\n\n const field = getFieldProps({ name,\n...props }, formex);\n\n if (isFunction(children)) {\n return children({ field,\nform: formex });\n }\n\n // if (component) {\n // if (typeof component === \"string\") {\n // const { innerRef, ...rest } = props;\n // return React.createElement(\n // component,\n // { ref: innerRef, ...field, ...rest, className },\n // children\n // );\n // }\n // return React.createElement(\n // component,\n // { field, form: formex, ...props, className },\n // children\n // );\n // }\n\n // default to input here so we can check for both `as` and `children` above\n const asElement = is || \"input\";\n\n if (typeof asElement === \"string\") {\n const { innerRef, ...rest } = props;\n return React.createElement(\n asElement,\n { ref: innerRef,\n...field,\n...rest,\nclassName },\n children\n );\n }\n\n return React.createElement(asElement, { ...field,\n...props,\nclassName }, children);\n}\n\nconst getFieldProps = (nameOrOptions: string | FieldConfig<unknown>, formex: FormexController<object>): FieldInputProps<unknown> => {\n const name: string = typeof nameOrOptions === \"string\"\n ? nameOrOptions\n : nameOrOptions.name;\n const valueState = getIn(formex.values as Record<string, unknown>, name);\n\n const field: FieldInputProps<unknown> = {\n name: name as string,\n value: valueState,\n onChange: formex.handleChange,\n onBlur: formex.handleBlur\n };\n if (typeof nameOrOptions !== \"string\") {\n const {\n type,\n value: valueProp, // value is special for checkboxes\n as: is,\n multiple\n } = nameOrOptions as FieldConfig<unknown>;\n\n if (type === \"checkbox\") {\n if (valueProp === undefined) {\n field.checked = !!valueState;\n } else {\n field.checked = !!(\n Array.isArray(valueState) && ~valueState.indexOf(valueProp)\n );\n field.value = valueProp;\n }\n } else if (type === \"radio\") {\n field.checked = valueState === valueProp;\n field.value = valueProp;\n } else if (is === \"select\" && multiple) {\n field.value = field.value || [];\n field.multiple = true;\n }\n }\n return field;\n};\n","import React, { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { getIn, setIn } from \"./utils\";\nimport { deepEqual as equal } from \"fast-equals\";\n\nimport { FormexController, FormexResetProps } from \"./types\";\n\n/**\n * One step of the form's undo history.\n *\n * `touched` and `boundary` are set only on the pair of entries an undoable\n * reset writes — the state it replaced and the state it produced. A reset\n * clears the touched map and bumps `version`, so a step across that pair has to\n * put both back. Ordinary edits leave both unset on purpose: undoing a\n * keystroke should not re-seed every field in the form.\n */\ntype FormexHistoryEntry<T> = {\n values: T;\n touched?: Record<string, boolean>;\n boundary?: boolean;\n};\n\nexport function useCreateFormex<T = any>({\n initialValues,\n initialModifiedValues,\n initialErrors,\n initialDirty,\n initialTouched,\n validation,\n validateOnChange = false,\n validateOnInitialRender = false,\n onSubmit,\n onReset,\n onValuesChangeDeferred,\n debugId\n}: {\n /**\n * The **baseline**: what the values are stored as. Everything the form\n * calls \"dirty\" is a difference from this, so it has to be the stored\n * record and nothing else. To open a form already carrying an edit, pass\n * that edit as {@link initialModifiedValues} — folding it into\n * `initialValues` instead makes the baseline agree with the edit, and then\n * nothing can tell that the edit is unsaved.\n */\n initialValues: T;\n /**\n * What the form should *show* on its first render, when that is not the\n * baseline — an edit handed over from somewhere else, a draft restored\n * from a cache. Dirty is computed from the difference.\n */\n initialModifiedValues?: T;\n initialErrors?: Record<string, string>;\n /**\n * Force the starting dirty state. Only for callers that know the form\n * opens modified but cannot supply the modified values; prefer\n * {@link initialModifiedValues}, which lets it be derived.\n */\n initialDirty?: boolean;\n initialTouched?: Record<string, boolean>;\n validateOnChange?: boolean;\n validateOnInitialRender?: boolean;\n validation?: (\n values: T\n ) =>\n | Record<string, string>\n | Promise<Record<string, string>>\n | undefined\n | void;\n onValuesChangeDeferred?: (values: T, controller: FormexController<T>) => void;\n onSubmit?: (values: T, controller: FormexController<T>) => void | Promise<void>;\n onReset?: (controller: FormexController<T>) => void | Promise<void>;\n debugId?: string;\n}): FormexController<T> {\n // The baseline and the current values start apart when the form opens\n // already carrying an edit. Keeping them separate is what lets the dirty\n // flag be *derived* rather than asserted, and what lets the baseline be\n // replaced later without touching what the user is looking at.\n const startValues = initialModifiedValues ?? initialValues;\n\n const initialValuesRef = useRef<T>(initialValues);\n const valuesRef = useRef<T>(startValues);\n const debugIdRef = useRef<string | undefined>(debugId);\n\n const [values, setValuesInner] = useState<T>(startValues);\n const [touchedState, setTouchedState] = useState<Record<string, boolean>>(initialTouched ?? {});\n // Read by `resetForm`, which needs the map as it stands at the moment it is\n // about to clear it. Assigned during render rather than from an effect, so\n // an event handler can never read one render's worth of stale state.\n const touchedRef = useRef<Record<string, boolean>>(touchedState);\n touchedRef.current = touchedState;\n const [errors, setErrors] = useState<Record<string, string>>(initialErrors ?? {});\n const [dirty, setDirty] = useState(initialDirty ?? !equal(initialValues, startValues));\n const [submitCount, setSubmitCount] = useState(0);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [isValidating, setIsValidating] = useState(false);\n const [version, setVersion] = useState(0);\n\n const onValuesChangeRef = useRef(onValuesChangeDeferred);\n onValuesChangeRef.current = onValuesChangeDeferred;\n const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n const callDebouncedOnValuesChange = useCallback((values: T) => {\n if (onValuesChangeRef.current) {\n if (debounceTimeoutRef.current) {\n clearTimeout(debounceTimeoutRef.current);\n }\n debounceTimeoutRef.current = setTimeout(() => {\n onValuesChangeRef.current?.(values, controllerRef.current);\n }, 300);\n }\n }, []);\n\n // Replace state for history with refs\n const historyRef = useRef<FormexHistoryEntry<T>[]>([{ values: startValues }]);\n const historyIndexRef = useRef<number>(0);\n\n /**\n * Record a new state, dropping anything that had been undone past it.\n */\n const pushHistory = useCallback((entry: FormexHistoryEntry<T>) => {\n const newHistory = historyRef.current.slice(0, historyIndexRef.current + 1);\n newHistory.push(entry);\n historyRef.current = newHistory;\n historyIndexRef.current = newHistory.length - 1;\n }, []);\n\n useEffect(() => {\n if (validateOnInitialRender) {\n validate();\n }\n }, []);\n\n const setValues = useCallback((newValues: T) => {\n valuesRef.current = newValues;\n setValuesInner(newValues);\n setDirty(!equal(initialValuesRef.current, newValues));\n pushHistory({ values: newValues });\n callDebouncedOnValuesChange(newValues);\n }, [callDebouncedOnValuesChange, pushHistory]);\n\n const validate = useCallback(async () => {\n setIsValidating(true);\n const validationErrors = await validation?.(valuesRef.current);\n setErrors(validationErrors ?? {});\n setIsValidating(false);\n return validationErrors;\n }, [validation]);\n\n const setFieldValue = useCallback(\n (key: string, value: unknown, shouldValidate?: boolean) => {\n const newValues = setIn(valuesRef.current as Record<string, unknown>, key, value) as T;\n valuesRef.current = newValues;\n setValuesInner(newValues);\n if (!equal(getIn(initialValuesRef.current as Record<string, unknown>, key), value)) {\n setDirty(true);\n }\n if (shouldValidate) {\n validate();\n }\n pushHistory({ values: newValues });\n callDebouncedOnValuesChange(newValues);\n },\n [validate, callDebouncedOnValuesChange, pushHistory]\n );\n\n const setFieldError = useCallback((key: string, error: string | undefined) => {\n setErrors((prevErrors: Record<string, string>) => {\n const newErrors = { ...prevErrors };\n if (error) {\n newErrors[key] = error;\n } else {\n delete newErrors[key];\n }\n return newErrors;\n });\n }, []);\n\n const setFieldTouched = useCallback(\n (key: string, touched: boolean, shouldValidate?: boolean) => {\n setTouchedState((prev: Record<string, boolean>) => ({\n ...prev,\n [key]: touched\n }));\n if (shouldValidate) {\n validate();\n }\n },\n [validate]\n );\n\n const handleChange = useCallback(\n (event: React.SyntheticEvent) => {\n const target = event.target as HTMLInputElement;\n let value;\n if (target.type === \"checkbox\") {\n value = target.checked;\n } else if (target.type === \"number\") {\n value = target.valueAsNumber;\n } else {\n value = target.value;\n }\n const name = target.name;\n setFieldValue(name, value, validateOnChange);\n setFieldTouched(name, true);\n },\n [setFieldValue, setFieldTouched, validateOnChange]\n );\n\n const handleBlur = useCallback((event: React.FocusEvent) => {\n const target = event.target as HTMLInputElement;\n const name = target.name;\n setFieldTouched(name, true);\n }, [setFieldTouched]);\n\n const submit = useCallback(\n async (e?: React.FormEvent<HTMLFormElement>) => {\n e?.preventDefault();\n e?.stopPropagation();\n setIsSubmitting(true);\n setSubmitCount((prev: number) => prev + 1);\n const validationErrors = await validation?.(valuesRef.current);\n if (validationErrors && Object.keys(validationErrors).length > 0) {\n setErrors(validationErrors);\n } else {\n setErrors({});\n await onSubmit?.(valuesRef.current, controllerRef.current);\n }\n setIsSubmitting(false);\n setVersion((prev: number) => prev + 1);\n },\n [onSubmit, validation]\n );\n\n const resetForm = useCallback((props?: FormexResetProps<T>) => {\n const {\n submitCount: submitCountProp,\n values: valuesProp,\n errors: errorsProp,\n touched: touchedProp,\n undoable\n } = props ?? {};\n const priorValues = valuesRef.current;\n const priorTouched = touchedRef.current;\n const nextValues = valuesProp ?? initialValuesRef.current;\n const nextTouched = touchedProp ?? initialTouched ?? {};\n valuesRef.current = nextValues;\n initialValuesRef.current = nextValues;\n setValuesInner(nextValues);\n setErrors(errorsProp ?? {});\n setTouchedState(nextTouched);\n setDirty(false);\n setSubmitCount(submitCountProp ?? 0);\n setVersion((prev: number) => prev + 1);\n onReset?.(controllerRef.current);\n if (undoable) {\n // Keep what the user typed one step behind them. The entry stepped\n // back into carries the touched map as well as the values: without\n // it the values return but every field reads untouched, and a draft\n // backup — which is extracted *through* the touched map — would come\n // back empty.\n const kept = historyRef.current.slice(0, historyIndexRef.current + 1);\n kept[kept.length - 1] = { values: priorValues, touched: priorTouched };\n kept.push({ values: nextValues, touched: nextTouched, boundary: true });\n historyRef.current = kept;\n historyIndexRef.current = kept.length - 1;\n } else {\n historyRef.current = [{ values: nextValues }];\n historyIndexRef.current = 0;\n }\n }, [onReset, initialTouched]);\n\n /**\n * The `initialValues` prop moved: the record this form edits finished\n * loading, or was replaced. That is a **re-baseline**, not a reset.\n *\n * It used to call `resetForm({ values: initialValues })`, which is a reset\n * in both of the ways that matter, and both were wrong here:\n *\n * - it fired `onReset`, which callers reasonably read as \"the user\n * discarded their changes\". The admin clears the cache that seeds an\n * in-flight edit handed over from the side panel there, so a record's own\n * data arriving deleted the edit the form had just been opened with, and\n * the form then re-seeded itself from the server — an expanded record\n * silently lost whatever had been typed into it.\n * - it overwrote `values`, so anything typed while the record was still\n * loading was thrown away without a word.\n *\n * So: move the baseline, leave the edit alone, and re-judge one against\n * the other. Only an untouched form follows the baseline to its new value.\n */\n useEffect(() => {\n if (equal(initialValuesRef.current, initialValues)) return;\n\n const modified = !equal(initialValuesRef.current, valuesRef.current);\n initialValuesRef.current = initialValues;\n\n if (modified) {\n setDirty(!equal(initialValues, valuesRef.current));\n } else {\n valuesRef.current = initialValues;\n setValuesInner(initialValues);\n historyRef.current = [{ values: initialValues }];\n historyIndexRef.current = 0;\n setDirty(false);\n }\n // Containers that read `values` off the controller key on `version`;\n // a re-seed changes what they are holding just as a reset does.\n setVersion((prev: number) => prev + 1);\n }, [initialValues]);\n\n const stepHistory = useCallback((newIndex: number) => {\n const from = historyRef.current[historyIndexRef.current];\n const entry = historyRef.current[newIndex];\n const newValues = entry.values;\n setValuesInner(newValues);\n valuesRef.current = newValues;\n historyIndexRef.current = newIndex;\n setDirty(!equal(initialValuesRef.current, newValues));\n if (entry.touched) {\n setTouchedState(entry.touched);\n }\n // Stepping across a reset. The reset told everything reading `values`\n // off the controller to re-read itself, so the way back has to say so\n // too — otherwise a cleared markdown editor stays cleared while the\n // value behind it is already back. Ordinary steps skip this.\n if (from?.boundary || entry.boundary) {\n setVersion((prev: number) => prev + 1);\n }\n callDebouncedOnValuesChange(newValues);\n }, [callDebouncedOnValuesChange]);\n\n const undo = useCallback(() => {\n if (historyIndexRef.current > 0) {\n stepHistory(historyIndexRef.current - 1);\n }\n }, [stepHistory]);\n\n const redo = useCallback(() => {\n if (historyIndexRef.current < historyRef.current.length - 1) {\n stepHistory(historyIndexRef.current + 1);\n }\n }, [stepHistory]);\n\n const controllerRef = useRef<FormexController<T>>({} as FormexController<T>);\n\n const controller = useMemo<FormexController<T>>(\n () => ({\n values,\n initialValues: initialValuesRef.current,\n handleChange,\n isSubmitting,\n setSubmitting: setIsSubmitting,\n setValues,\n setFieldValue,\n errors,\n setFieldError,\n touched: touchedState,\n setFieldTouched,\n setTouched: setTouchedState,\n dirty,\n setDirty,\n handleSubmit: submit,\n submitCount,\n setSubmitCount,\n handleBlur,\n validate,\n isValidating,\n resetForm,\n version,\n debugId: debugIdRef.current,\n undo,\n redo,\n canUndo: historyIndexRef.current > 0,\n canRedo: historyIndexRef.current < historyRef.current.length - 1\n }),\n [\n values,\n errors,\n touchedState,\n dirty,\n isSubmitting,\n submitCount,\n isValidating,\n version,\n handleChange,\n handleBlur,\n setValues,\n setFieldValue,\n setFieldTouched,\n setTouchedState,\n setFieldError,\n validate,\n submit,\n resetForm,\n undo,\n redo\n ]\n );\n\n useEffect(() => {\n controllerRef.current = controller;\n }, [controller]);\n\n return controller;\n}\n"],"mappings":";;;;;AAIA,IAAM,gBAAgB,MAAM,cAA4C,IAAI;AAE5E,IAAa,kBAA2B;CACpC,MAAM,MAAM,WAAW,aAAa;CACpC,IAAI,CAAC,KAAK,MAAM,IAAI,MAAM,iDAAiD;CAC3E,OAAO;AACX;AAEA,IAAa,UAAmB,EAAE,OAAO,eAA0E;CAC/G,OAAO,oBAAC,cAAc,UAAf;EAA+B;EAAQ;CAAiC,CAAA;AACnF;;;;ACbA,IAAa,gBAAgB,UACzB,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW;;AAI7C,IAAa,cAAc,QACvB,OAAO,QAAQ;;AAGnB,IAAa,YAAY,QACrB,QAAQ,QAAQ,OAAO,QAAQ;;AAGnC,IAAa,aAAa,QACtB,OAAO,KAAK,MAAM,OAAO,GAAG,CAAC,CAAC,MAAM;;AAIxC,IAAa,SAAS,QAA0B,QAAQ;;;;AAKxD,SAAgB,MACZ,KACA,KACA,KACA,IAAI,GACG;CAIP,IAAI,uBAAuB,GAAG,GAAG,OAAO;CAExC,MAAM,OAAO,OAAO,GAAG;CACvB,IAAI,UAAmB;CACvB,OAAO,WAAW,IAAI,KAAK,QACvB,UAAW,QAAoC,KAAK;CAIxD,IAAI,MAAM,KAAK,UAAU,CAAC,SACtB,OAAO;CAGX,OAAO,YAAY,KAAA,IAAY,MAAM;AACzC;AAEA,SAAgB,MAAM,KAAc,MAAc,OAAyB;CAMvE,IAAI,uBAAuB,IAAI,GAAG,OAAO;CAEzC,MAAM,MAAM,MAAM,GAAG;CACrB,IAAI,SAAkC;CACtC,IAAI,IAAI;CACR,MAAM,YAAY,OAAO,IAAI;CAE7B,OAAO,IAAI,UAAU,SAAS,GAAG,KAAK;EAClC,MAAM,cAAsB,UAAU;EACtC,MAAM,aAAa,MAAM,KAAK,UAAU,MAAM,GAAG,IAAI,CAAC,CAAC;EAEvD,IAAI,eAAe,SAAS,UAAU,KAAK,MAAM,QAAQ,UAAU,IAC/D,SAAS,OAAO,eAAe,MAAM,UAAU;OAC5C;GACH,MAAM,WAAmB,UAAU,IAAI;GACvC,SAAS,OAAO,eACX,UAAU,QAAQ,KAAK,OAAO,QAAQ,KAAK,IAAI,CAAC,IAAI,CAAC;EAC9D;CACJ;CAGA,KAAK,MAAM,IAAK,MAAkC,OAAA,CAAQ,UAAU,QAAQ,OACxE,OAAO;CAGX,IAAI,UAAU,KAAA,GACV,OAAO,OAAO,UAAU;MAExB,OAAO,UAAU,MAAM;CAK3B,IAAI,MAAM,KAAK,UAAU,KAAA,GACrB,OAAO,IAAI,UAAU;CAGzB,OAAO;AACX;AAEA,SAAgB,MAAM,OAAyB;CAC3C,IAAI,MAAM,QAAQ,KAAK,GACnB,OAAO,CAAC,GAAG,KAAK;MACb,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM;EAEpD,IAAI,OAAO,eAAe,KAAK,MAAM,OAAO,WACxC,OAAO;EAEX,OAAO,EAAE,GAAI,MAAkC;CACnD,OACI,OAAO;AAEf;;;;;;;;;;;;;AAcA,IAAM,uCAAuB,IAAI,IAAI;CAAC;CAAa;CAAe;AAAW,CAAC;;AAG9E,SAAgB,uBAAuB,MAAkC;CACrE,OAAO,OAAO,IAAI,CAAC,CAAC,MAAK,YAAW,qBAAqB,IAAI,OAAO,CAAC;AACzE;AAEA,SAAS,OAAO,OAA0B;CACtC,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO;CAEjC,OAAO,MAAM,QAAQ,aAAa,KAAK,CAAC,CAAC,QAAQ,OAAO,EAAE,CAAC,CAAC,QAAQ,OAAO,EAAE,CAAC,CAAC,MAAM,GAAG;AAC5F;;;AC9DA,SAAgB,MAA8D,EACI,UACA,MACA,UACA,IAAI,IAEJ,WACA,GAAG,SACc;CAC/F,MAAM,SAAS,UAAU;CAEzB,MAAM,QAAQ,cAAc;EAAE;EAClC,GAAG;CAAM,GAAG,MAAM;CAEd,IAAI,WAAW,QAAQ,GACnB,OAAO,SAAS;EAAE;EAC1B,MAAM;CAAO,CAAC;CAoBV,MAAM,YAAY,MAAM;CAExB,IAAI,OAAO,cAAc,UAAU;EAC/B,MAAM,EAAE,UAAU,GAAG,SAAS;EAC9B,OAAO,QAAM,cACT,WACA;GAAE,KAAK;GACnB,GAAG;GACH,GAAG;GACH;EAAU,GACE,QACJ;CACJ;CAEA,OAAO,QAAM,cAAc,WAAW;EAAE,GAAG;EAC/C,GAAG;EACH;CAAU,GAAG,QAAQ;AACrB;AAEA,IAAM,iBAAiB,eAA8C,WAA+D;CAChI,MAAM,OAAe,OAAO,kBAAkB,WACxC,gBACA,cAAc;CACpB,MAAM,aAAa,MAAM,OAAO,QAAmC,IAAI;CAEvE,MAAM,QAAkC;EAC9B;EACN,OAAO;EACP,UAAU,OAAO;EACjB,QAAQ,OAAO;CACnB;CACA,IAAI,OAAO,kBAAkB,UAAU;EACnC,MAAM,EACF,MACA,OAAO,WACP,IAAI,IACJ,aACA;EAEJ,IAAI,SAAS,YACT,IAAI,cAAc,KAAA,GACd,MAAM,UAAU,CAAC,CAAC;OACf;GACH,MAAM,UAAU,CAAC,EACb,MAAM,QAAQ,UAAU,KAAK,CAAC,WAAW,QAAQ,SAAS;GAE9D,MAAM,QAAQ;EAClB;OACG,IAAI,SAAS,SAAS;GACzB,MAAM,UAAU,eAAe;GAC/B,MAAM,QAAQ;EAClB,OAAO,IAAI,OAAO,YAAY,UAAU;GACpC,MAAM,QAAQ,MAAM,SAAS,CAAC;GAC9B,MAAM,WAAW;EACrB;CACJ;CACA,OAAO;AACX;;;AC9IA,SAAgB,gBAAyB,EACrC,eACA,uBACA,eACA,cACA,gBACA,YACA,mBAAmB,OACnB,0BAA0B,OAC1B,UACA,SACA,wBACA,WAsCoB;CAKpB,MAAM,cAAc,yBAAyB;CAE7C,MAAM,mBAAmB,OAAU,aAAa;CAChD,MAAM,YAAY,OAAU,WAAW;CACvC,MAAM,aAAa,OAA2B,OAAO;CAErD,MAAM,CAAC,QAAQ,kBAAkB,SAAY,WAAW;CACxD,MAAM,CAAC,cAAc,mBAAmB,SAAkC,kBAAkB,CAAC,CAAC;CAI9F,MAAM,aAAa,OAAgC,YAAY;CAC/D,WAAW,UAAU;CACrB,MAAM,CAAC,QAAQ,aAAa,SAAiC,iBAAiB,CAAC,CAAC;CAChF,MAAM,CAAC,OAAO,YAAY,SAAS,gBAAgB,CAAC,UAAM,eAAe,WAAW,CAAC;CACrF,MAAM,CAAC,aAAa,kBAAkB,SAAS,CAAC;CAChD,MAAM,CAAC,cAAc,mBAAmB,SAAS,KAAK;CACtD,MAAM,CAAC,cAAc,mBAAmB,SAAS,KAAK;CACtD,MAAM,CAAC,SAAS,cAAc,SAAS,CAAC;CAExC,MAAM,oBAAoB,OAAO,sBAAsB;CACvD,kBAAkB,UAAU;CAC5B,MAAM,qBAAqB,OAAkD,KAAA,CAAS;CAEtF,MAAM,8BAA8B,aAAa,WAAc;EAC3D,IAAI,kBAAkB,SAAS;GAC3B,IAAI,mBAAmB,SACnB,aAAa,mBAAmB,OAAO;GAE3C,mBAAmB,UAAU,iBAAiB;IAC1C,kBAAkB,UAAU,QAAQ,cAAc,OAAO;GAC7D,GAAG,GAAG;EACV;CACJ,GAAG,CAAC,CAAC;CAGL,MAAM,aAAa,OAAgC,CAAC,EAAE,QAAQ,YAAY,CAAC,CAAC;CAC5E,MAAM,kBAAkB,OAAe,CAAC;;;;CAKxC,MAAM,cAAc,aAAa,UAAiC;EAC9D,MAAM,aAAa,WAAW,QAAQ,MAAM,GAAG,gBAAgB,UAAU,CAAC;EAC1E,WAAW,KAAK,KAAK;EACrB,WAAW,UAAU;EACrB,gBAAgB,UAAU,WAAW,SAAS;CAClD,GAAG,CAAC,CAAC;CAEL,gBAAgB;EACZ,IAAI,yBACA,SAAS;CAEjB,GAAG,CAAC,CAAC;CAEL,MAAM,YAAY,aAAa,cAAiB;EAC5C,UAAU,UAAU;EACpB,eAAe,SAAS;EACxB,SAAS,CAAC,UAAM,iBAAiB,SAAS,SAAS,CAAC;EACpD,YAAY,EAAE,QAAQ,UAAU,CAAC;EACjC,4BAA4B,SAAS;CACzC,GAAG,CAAC,6BAA6B,WAAW,CAAC;CAE7C,MAAM,WAAW,YAAY,YAAY;EACrC,gBAAgB,IAAI;EACpB,MAAM,mBAAmB,MAAM,aAAa,UAAU,OAAO;EAC7D,UAAU,oBAAoB,CAAC,CAAC;EAChC,gBAAgB,KAAK;EACrB,OAAO;CACX,GAAG,CAAC,UAAU,CAAC;CAEf,MAAM,gBAAgB,aACjB,KAAa,OAAgB,mBAA6B;EACvD,MAAM,YAAY,MAAM,UAAU,SAAoC,KAAK,KAAK;EAChF,UAAU,UAAU;EACpB,eAAe,SAAS;EACxB,IAAI,CAAC,UAAM,MAAM,iBAAiB,SAAoC,GAAG,GAAG,KAAK,GAC7E,SAAS,IAAI;EAEjB,IAAI,gBACA,SAAS;EAEb,YAAY,EAAE,QAAQ,UAAU,CAAC;EACjC,4BAA4B,SAAS;CACzC,GACA;EAAC;EAAU;EAA6B;CAAW,CACvD;CAEA,MAAM,gBAAgB,aAAa,KAAa,UAA8B;EAC1E,WAAW,eAAuC;GAC9C,MAAM,YAAY,EAAE,GAAG,WAAW;GAClC,IAAI,OACA,UAAU,OAAO;QAEjB,OAAO,UAAU;GAErB,OAAO;EACX,CAAC;CACL,GAAG,CAAC,CAAC;CAEL,MAAM,kBAAkB,aACnB,KAAa,SAAkB,mBAA6B;EACzD,iBAAiB,UAAmC;GAChD,GAAG;IACF,MAAM;EACX,EAAE;EACF,IAAI,gBACA,SAAS;CAEjB,GACA,CAAC,QAAQ,CACb;CAEA,MAAM,eAAe,aAChB,UAAgC;EAC7B,MAAM,SAAS,MAAM;EACrB,IAAI;EACJ,IAAI,OAAO,SAAS,YAChB,QAAQ,OAAO;OACZ,IAAI,OAAO,SAAS,UACvB,QAAQ,OAAO;OAEf,QAAQ,OAAO;EAEnB,MAAM,OAAO,OAAO;EACpB,cAAc,MAAM,OAAO,gBAAgB;EAC3C,gBAAgB,MAAM,IAAI;CAC9B,GACA;EAAC;EAAe;EAAiB;CAAgB,CACrD;CAEA,MAAM,aAAa,aAAa,UAA4B;EAExD,MAAM,OADS,MAAM,OACD;EACpB,gBAAgB,MAAM,IAAI;CAC9B,GAAG,CAAC,eAAe,CAAC;CAEpB,MAAM,SAAS,YACX,OAAO,MAAyC;EAC5C,GAAG,eAAe;EAClB,GAAG,gBAAgB;EACnB,gBAAgB,IAAI;EACpB,gBAAgB,SAAiB,OAAO,CAAC;EACzC,MAAM,mBAAmB,MAAM,aAAa,UAAU,OAAO;EAC7D,IAAI,oBAAoB,OAAO,KAAK,gBAAgB,CAAC,CAAC,SAAS,GAC3D,UAAU,gBAAgB;OACvB;GACH,UAAU,CAAC,CAAC;GACZ,MAAM,WAAW,UAAU,SAAS,cAAc,OAAO;EAC7D;EACA,gBAAgB,KAAK;EACrB,YAAY,SAAiB,OAAO,CAAC;CACzC,GACA,CAAC,UAAU,UAAU,CACzB;CAEA,MAAM,YAAY,aAAa,UAAgC;EAC3D,MAAM,EACF,aAAa,iBACb,QAAQ,YACR,QAAQ,YACR,SAAS,aACT,aACA,SAAS,CAAC;EACd,MAAM,cAAc,UAAU;EAC9B,MAAM,eAAe,WAAW;EAChC,MAAM,aAAa,cAAc,iBAAiB;EAClD,MAAM,cAAc,eAAe,kBAAkB,CAAC;EACtD,UAAU,UAAU;EACpB,iBAAiB,UAAU;EAC3B,eAAe,UAAU;EACzB,UAAU,cAAc,CAAC,CAAC;EAC1B,gBAAgB,WAAW;EAC3B,SAAS,KAAK;EACd,eAAe,mBAAmB,CAAC;EACnC,YAAY,SAAiB,OAAO,CAAC;EACrC,UAAU,cAAc,OAAO;EAC/B,IAAI,UAAU;GAMV,MAAM,OAAO,WAAW,QAAQ,MAAM,GAAG,gBAAgB,UAAU,CAAC;GACpE,KAAK,KAAK,SAAS,KAAK;IAAE,QAAQ;IAAa,SAAS;GAAa;GACrE,KAAK,KAAK;IAAE,QAAQ;IAAY,SAAS;IAAa,UAAU;GAAK,CAAC;GACtE,WAAW,UAAU;GACrB,gBAAgB,UAAU,KAAK,SAAS;EAC5C,OAAO;GACH,WAAW,UAAU,CAAC,EAAE,QAAQ,WAAW,CAAC;GAC5C,gBAAgB,UAAU;EAC9B;CACJ,GAAG,CAAC,SAAS,cAAc,CAAC;;;;;;;;;;;;;;;;;;;;CAqB5B,gBAAgB;EACZ,IAAI,UAAM,iBAAiB,SAAS,aAAa,GAAG;EAEpD,MAAM,WAAW,CAAC,UAAM,iBAAiB,SAAS,UAAU,OAAO;EACnE,iBAAiB,UAAU;EAE3B,IAAI,UACA,SAAS,CAAC,UAAM,eAAe,UAAU,OAAO,CAAC;OAC9C;GACH,UAAU,UAAU;GACpB,eAAe,aAAa;GAC5B,WAAW,UAAU,CAAC,EAAE,QAAQ,cAAc,CAAC;GAC/C,gBAAgB,UAAU;GAC1B,SAAS,KAAK;EAClB;EAGA,YAAY,SAAiB,OAAO,CAAC;CACzC,GAAG,CAAC,aAAa,CAAC;CAElB,MAAM,cAAc,aAAa,aAAqB;EAClD,MAAM,OAAO,WAAW,QAAQ,gBAAgB;EAChD,MAAM,QAAQ,WAAW,QAAQ;EACjC,MAAM,YAAY,MAAM;EACxB,eAAe,SAAS;EACxB,UAAU,UAAU;EACpB,gBAAgB,UAAU;EAC1B,SAAS,CAAC,UAAM,iBAAiB,SAAS,SAAS,CAAC;EACpD,IAAI,MAAM,SACN,gBAAgB,MAAM,OAAO;EAMjC,IAAI,MAAM,YAAY,MAAM,UACxB,YAAY,SAAiB,OAAO,CAAC;EAEzC,4BAA4B,SAAS;CACzC,GAAG,CAAC,2BAA2B,CAAC;CAEhC,MAAM,OAAO,kBAAkB;EAC3B,IAAI,gBAAgB,UAAU,GAC1B,YAAY,gBAAgB,UAAU,CAAC;CAE/C,GAAG,CAAC,WAAW,CAAC;CAEhB,MAAM,OAAO,kBAAkB;EAC3B,IAAI,gBAAgB,UAAU,WAAW,QAAQ,SAAS,GACtD,YAAY,gBAAgB,UAAU,CAAC;CAE/C,GAAG,CAAC,WAAW,CAAC;CAEhB,MAAM,gBAAgB,OAA4B,CAAC,CAAwB;CAE3E,MAAM,aAAa,eACR;EACH;EACA,eAAe,iBAAiB;EAChC;EACA;EACA,eAAe;EACf;EACA;EACA;EACA;EACA,SAAS;EACT;EACA,YAAY;EACZ;EACA;EACA,cAAc;EACd;EACA;EACA;EACA;EACA;EACA;EACA;EACA,SAAS,WAAW;EACpB;EACA;EACA,SAAS,gBAAgB,UAAU;EACnC,SAAS,gBAAgB,UAAU,WAAW,QAAQ,SAAS;CACnE,IACA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACJ,CACJ;CAEA,gBAAgB;EACZ,cAAc,UAAU;CAC5B,GAAG,CAAC,UAAU,CAAC;CAEf,OAAO;AACX"}
package/dist/types.d.ts CHANGED
@@ -39,4 +39,15 @@ export type FormexResetProps<T = any> = {
39
39
  submitCount?: number;
40
40
  errors?: Record<string, string>;
41
41
  touched?: Record<string, boolean>;
42
+ /**
43
+ * Leave what the reset replaced one step behind in the undo history, so
44
+ * {@link FormexController.undo} brings it back.
45
+ *
46
+ * For a reset the *user* asked for — a "Discard changes" or "Clear form"
47
+ * button — where the thing being thrown away is everything they typed, and
48
+ * a misclick is otherwise unrecoverable. The default clears the history,
49
+ * which is right for a reset the form performs on its own (a save, a new
50
+ * record): there is nothing there worth stepping back into.
51
+ */
52
+ undoable?: boolean;
42
53
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/forms",
3
3
  "type": "module",
4
- "version": "0.15.0",
4
+ "version": "0.16.0",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
7
7
  "access": "public"
package/src/types.ts CHANGED
@@ -44,4 +44,15 @@ export type FormexResetProps<T = any> = {
44
44
  submitCount?: number;
45
45
  errors?: Record<string, string>;
46
46
  touched?: Record<string, boolean>;
47
+ /**
48
+ * Leave what the reset replaced one step behind in the undo history, so
49
+ * {@link FormexController.undo} brings it back.
50
+ *
51
+ * For a reset the *user* asked for — a "Discard changes" or "Clear form"
52
+ * button — where the thing being thrown away is everything they typed, and
53
+ * a misclick is otherwise unrecoverable. The default clears the history,
54
+ * which is right for a reset the form performs on its own (a save, a new
55
+ * record): there is nothing there worth stepping back into.
56
+ */
57
+ undoable?: boolean;
47
58
  };
@@ -4,6 +4,21 @@ import { deepEqual as equal } from "fast-equals";
4
4
 
5
5
  import { FormexController, FormexResetProps } from "./types";
6
6
 
7
+ /**
8
+ * One step of the form's undo history.
9
+ *
10
+ * `touched` and `boundary` are set only on the pair of entries an undoable
11
+ * reset writes — the state it replaced and the state it produced. A reset
12
+ * clears the touched map and bumps `version`, so a step across that pair has to
13
+ * put both back. Ordinary edits leave both unset on purpose: undoing a
14
+ * keystroke should not re-seed every field in the form.
15
+ */
16
+ type FormexHistoryEntry<T> = {
17
+ values: T;
18
+ touched?: Record<string, boolean>;
19
+ boundary?: boolean;
20
+ };
21
+
7
22
  export function useCreateFormex<T = any>({
8
23
  initialValues,
9
24
  initialModifiedValues,
@@ -67,6 +82,11 @@ export function useCreateFormex<T = any>({
67
82
 
68
83
  const [values, setValuesInner] = useState<T>(startValues);
69
84
  const [touchedState, setTouchedState] = useState<Record<string, boolean>>(initialTouched ?? {});
85
+ // Read by `resetForm`, which needs the map as it stands at the moment it is
86
+ // about to clear it. Assigned during render rather than from an effect, so
87
+ // an event handler can never read one render's worth of stale state.
88
+ const touchedRef = useRef<Record<string, boolean>>(touchedState);
89
+ touchedRef.current = touchedState;
70
90
  const [errors, setErrors] = useState<Record<string, string>>(initialErrors ?? {});
71
91
  const [dirty, setDirty] = useState(initialDirty ?? !equal(initialValues, startValues));
72
92
  const [submitCount, setSubmitCount] = useState(0);
@@ -90,9 +110,19 @@ export function useCreateFormex<T = any>({
90
110
  }, []);
91
111
 
92
112
  // Replace state for history with refs
93
- const historyRef = useRef<T[]>([startValues]);
113
+ const historyRef = useRef<FormexHistoryEntry<T>[]>([{ values: startValues }]);
94
114
  const historyIndexRef = useRef<number>(0);
95
115
 
116
+ /**
117
+ * Record a new state, dropping anything that had been undone past it.
118
+ */
119
+ const pushHistory = useCallback((entry: FormexHistoryEntry<T>) => {
120
+ const newHistory = historyRef.current.slice(0, historyIndexRef.current + 1);
121
+ newHistory.push(entry);
122
+ historyRef.current = newHistory;
123
+ historyIndexRef.current = newHistory.length - 1;
124
+ }, []);
125
+
96
126
  useEffect(() => {
97
127
  if (validateOnInitialRender) {
98
128
  validate();
@@ -103,13 +133,9 @@ export function useCreateFormex<T = any>({
103
133
  valuesRef.current = newValues;
104
134
  setValuesInner(newValues);
105
135
  setDirty(!equal(initialValuesRef.current, newValues));
106
- // Update history using refs
107
- const newHistory = historyRef.current.slice(0, historyIndexRef.current + 1);
108
- newHistory.push(newValues);
109
- historyRef.current = newHistory;
110
- historyIndexRef.current = newHistory.length - 1;
136
+ pushHistory({ values: newValues });
111
137
  callDebouncedOnValuesChange(newValues);
112
- }, [callDebouncedOnValuesChange]);
138
+ }, [callDebouncedOnValuesChange, pushHistory]);
113
139
 
114
140
  const validate = useCallback(async () => {
115
141
  setIsValidating(true);
@@ -130,14 +156,10 @@ export function useCreateFormex<T = any>({
130
156
  if (shouldValidate) {
131
157
  validate();
132
158
  }
133
- // Update history using refs
134
- const newHistory = historyRef.current.slice(0, historyIndexRef.current + 1);
135
- newHistory.push(newValues);
136
- historyRef.current = newHistory;
137
- historyIndexRef.current = newHistory.length - 1;
159
+ pushHistory({ values: newValues });
138
160
  callDebouncedOnValuesChange(newValues);
139
161
  },
140
- [validate, callDebouncedOnValuesChange]
162
+ [validate, callDebouncedOnValuesChange, pushHistory]
141
163
  );
142
164
 
143
165
  const setFieldError = useCallback((key: string, error: string | undefined) => {
@@ -213,20 +235,37 @@ export function useCreateFormex<T = any>({
213
235
  submitCount: submitCountProp,
214
236
  values: valuesProp,
215
237
  errors: errorsProp,
216
- touched: touchedProp
238
+ touched: touchedProp,
239
+ undoable
217
240
  } = props ?? {};
218
- valuesRef.current = valuesProp ?? initialValuesRef.current;
219
- initialValuesRef.current = valuesProp ?? initialValuesRef.current;
220
- setValuesInner(valuesProp ?? initialValuesRef.current);
241
+ const priorValues = valuesRef.current;
242
+ const priorTouched = touchedRef.current;
243
+ const nextValues = valuesProp ?? initialValuesRef.current;
244
+ const nextTouched = touchedProp ?? initialTouched ?? {};
245
+ valuesRef.current = nextValues;
246
+ initialValuesRef.current = nextValues;
247
+ setValuesInner(nextValues);
221
248
  setErrors(errorsProp ?? {});
222
- setTouchedState(touchedProp ?? initialTouched ?? {});
249
+ setTouchedState(nextTouched);
223
250
  setDirty(false);
224
251
  setSubmitCount(submitCountProp ?? 0);
225
252
  setVersion((prev: number) => prev + 1);
226
253
  onReset?.(controllerRef.current);
227
- // Reset history with refs
228
- historyRef.current = [valuesProp ?? initialValuesRef.current];
229
- historyIndexRef.current = 0;
254
+ if (undoable) {
255
+ // Keep what the user typed one step behind them. The entry stepped
256
+ // back into carries the touched map as well as the values: without
257
+ // it the values return but every field reads untouched, and a draft
258
+ // backup — which is extracted *through* the touched map — would come
259
+ // back empty.
260
+ const kept = historyRef.current.slice(0, historyIndexRef.current + 1);
261
+ kept[kept.length - 1] = { values: priorValues, touched: priorTouched };
262
+ kept.push({ values: nextValues, touched: nextTouched, boundary: true });
263
+ historyRef.current = kept;
264
+ historyIndexRef.current = kept.length - 1;
265
+ } else {
266
+ historyRef.current = [{ values: nextValues }];
267
+ historyIndexRef.current = 0;
268
+ }
230
269
  }, [onReset, initialTouched]);
231
270
 
232
271
  /**
@@ -259,7 +298,7 @@ export function useCreateFormex<T = any>({
259
298
  } else {
260
299
  valuesRef.current = initialValues;
261
300
  setValuesInner(initialValues);
262
- historyRef.current = [initialValues];
301
+ historyRef.current = [{ values: initialValues }];
263
302
  historyIndexRef.current = 0;
264
303
  setDirty(false);
265
304
  }
@@ -268,29 +307,38 @@ export function useCreateFormex<T = any>({
268
307
  setVersion((prev: number) => prev + 1);
269
308
  }, [initialValues]);
270
309
 
310
+ const stepHistory = useCallback((newIndex: number) => {
311
+ const from = historyRef.current[historyIndexRef.current];
312
+ const entry = historyRef.current[newIndex];
313
+ const newValues = entry.values;
314
+ setValuesInner(newValues);
315
+ valuesRef.current = newValues;
316
+ historyIndexRef.current = newIndex;
317
+ setDirty(!equal(initialValuesRef.current, newValues));
318
+ if (entry.touched) {
319
+ setTouchedState(entry.touched);
320
+ }
321
+ // Stepping across a reset. The reset told everything reading `values`
322
+ // off the controller to re-read itself, so the way back has to say so
323
+ // too — otherwise a cleared markdown editor stays cleared while the
324
+ // value behind it is already back. Ordinary steps skip this.
325
+ if (from?.boundary || entry.boundary) {
326
+ setVersion((prev: number) => prev + 1);
327
+ }
328
+ callDebouncedOnValuesChange(newValues);
329
+ }, [callDebouncedOnValuesChange]);
330
+
271
331
  const undo = useCallback(() => {
272
332
  if (historyIndexRef.current > 0) {
273
- const newIndex = historyIndexRef.current - 1;
274
- const newValues = historyRef.current[newIndex];
275
- setValuesInner(newValues);
276
- valuesRef.current = newValues;
277
- historyIndexRef.current = newIndex;
278
- setDirty(!equal(initialValuesRef.current, newValues));
279
- callDebouncedOnValuesChange(newValues);
333
+ stepHistory(historyIndexRef.current - 1);
280
334
  }
281
- }, [callDebouncedOnValuesChange]);
335
+ }, [stepHistory]);
282
336
 
283
337
  const redo = useCallback(() => {
284
338
  if (historyIndexRef.current < historyRef.current.length - 1) {
285
- const newIndex = historyIndexRef.current + 1;
286
- const newValues = historyRef.current[newIndex];
287
- setValuesInner(newValues);
288
- valuesRef.current = newValues;
289
- historyIndexRef.current = newIndex;
290
- setDirty(!equal(initialValuesRef.current, newValues));
291
- callDebouncedOnValuesChange(newValues);
339
+ stepHistory(historyIndexRef.current + 1);
292
340
  }
293
- }, [callDebouncedOnValuesChange]);
341
+ }, [stepHistory]);
294
342
 
295
343
  const controllerRef = useRef<FormexController<T>>({} as FormexController<T>);
296
344