@rebasepro/forms 0.13.0 → 0.13.1-canary.g18cfeb7

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
@@ -31,6 +31,7 @@ var isNaN = (obj) => obj !== obj;
31
31
  * Deeply get a value from an object via its path.
32
32
  */
33
33
  function getIn(obj, key, def, p = 0) {
34
+ if (pathTraversesPrototype(key)) return def;
34
35
  const path = toPath(key);
35
36
  let current = obj;
36
37
  while (current && p < path.length) current = current[path[p++]];
@@ -38,6 +39,7 @@ function getIn(obj, key, def, p = 0) {
38
39
  return current === void 0 ? def : current;
39
40
  }
40
41
  function setIn(obj, path, value) {
42
+ if (pathTraversesPrototype(path)) return obj;
41
43
  const res = clone(obj);
42
44
  let resVal = res;
43
45
  let i = 0;
@@ -64,6 +66,27 @@ function clone(value) {
64
66
  return { ...value };
65
67
  } else return value;
66
68
  }
69
+ /**
70
+ * Segments that reach the prototype chain rather than a property of the object.
71
+ *
72
+ * `res["__proto__"] = x` is a setter for the object's prototype, not an own
73
+ * property, so a path of `__proto__.polluted` wrote straight onto
74
+ * `Object.prototype` and gave every object in the process a `polluted`
75
+ * property. `constructor.prototype.x` arrived by a second route, and
76
+ * `__proto__.0` did it to arrays.
77
+ *
78
+ * These are paths, and a path here is a property key — which for a map property
79
+ * or a column mapped out of an imported CSV is data, not code.
80
+ */
81
+ var UNSAFE_PATH_SEGMENTS = /* @__PURE__ */ new Set([
82
+ "__proto__",
83
+ "constructor",
84
+ "prototype"
85
+ ]);
86
+ /** Whether any segment of this path would traverse the prototype chain. */
87
+ function pathTraversesPrototype(path) {
88
+ return toPath(path).some((segment) => UNSAFE_PATH_SEGMENTS.has(segment));
89
+ }
67
90
  function toPath(value) {
68
91
  if (Array.isArray(value)) return value;
69
92
  return value.replace(/\[(\d+)]/g, ".$1").replace(/^\./, "").replace(/\.$/, "").split(".");
@@ -124,14 +147,15 @@ var getFieldProps = (nameOrOptions, formex) => {
124
147
  };
125
148
  //#endregion
126
149
  //#region src/useCreateFormex.tsx
127
- function useCreateFormex({ initialValues, initialErrors, initialDirty, initialTouched, validation, validateOnChange = false, validateOnInitialRender = false, onSubmit, onReset, onValuesChangeDeferred, debugId }) {
150
+ function useCreateFormex({ initialValues, initialModifiedValues, initialErrors, initialDirty, initialTouched, validation, validateOnChange = false, validateOnInitialRender = false, onSubmit, onReset, onValuesChangeDeferred, debugId }) {
151
+ const startValues = initialModifiedValues ?? initialValues;
128
152
  const initialValuesRef = useRef(initialValues);
129
- const valuesRef = useRef(initialValues);
153
+ const valuesRef = useRef(startValues);
130
154
  const debugIdRef = useRef(debugId);
131
- const [values, setValuesInner] = useState(initialValues);
155
+ const [values, setValuesInner] = useState(startValues);
132
156
  const [touchedState, setTouchedState] = useState(initialTouched ?? {});
133
157
  const [errors, setErrors] = useState(initialErrors ?? {});
134
- const [dirty, setDirty] = useState(initialDirty ?? false);
158
+ const [dirty, setDirty] = useState(initialDirty ?? !deepEqual(initialValues, startValues));
135
159
  const [submitCount, setSubmitCount] = useState(0);
136
160
  const [isSubmitting, setIsSubmitting] = useState(false);
137
161
  const [isValidating, setIsValidating] = useState(false);
@@ -147,7 +171,7 @@ function useCreateFormex({ initialValues, initialErrors, initialDirty, initialTo
147
171
  }, 300);
148
172
  }
149
173
  }, []);
150
- const historyRef = useRef([initialValues]);
174
+ const historyRef = useRef([startValues]);
151
175
  const historyIndexRef = useRef(0);
152
176
  useEffect(() => {
153
177
  if (validateOnInitialRender) validate();
@@ -242,9 +266,39 @@ function useCreateFormex({ initialValues, initialErrors, initialDirty, initialTo
242
266
  historyRef.current = [valuesProp ?? initialValuesRef.current];
243
267
  historyIndexRef.current = 0;
244
268
  }, [onReset, initialTouched]);
269
+ /**
270
+ * The `initialValues` prop moved: the record this form edits finished
271
+ * loading, or was replaced. That is a **re-baseline**, not a reset.
272
+ *
273
+ * It used to call `resetForm({ values: initialValues })`, which is a reset
274
+ * in both of the ways that matter, and both were wrong here:
275
+ *
276
+ * - it fired `onReset`, which callers reasonably read as "the user
277
+ * discarded their changes". The admin clears the cache that seeds an
278
+ * in-flight edit handed over from the side panel there, so a record's own
279
+ * data arriving deleted the edit the form had just been opened with, and
280
+ * the form then re-seeded itself from the server — an expanded record
281
+ * silently lost whatever had been typed into it.
282
+ * - it overwrote `values`, so anything typed while the record was still
283
+ * loading was thrown away without a word.
284
+ *
285
+ * So: move the baseline, leave the edit alone, and re-judge one against
286
+ * the other. Only an untouched form follows the baseline to its new value.
287
+ */
245
288
  useEffect(() => {
246
- if (!deepEqual(initialValuesRef.current, initialValues)) resetForm({ values: initialValues });
247
- }, [initialValues, resetForm]);
289
+ if (deepEqual(initialValuesRef.current, initialValues)) return;
290
+ const modified = !deepEqual(initialValuesRef.current, valuesRef.current);
291
+ initialValuesRef.current = initialValues;
292
+ if (modified) setDirty(!deepEqual(initialValues, valuesRef.current));
293
+ else {
294
+ valuesRef.current = initialValues;
295
+ setValuesInner(initialValues);
296
+ historyRef.current = [initialValues];
297
+ historyIndexRef.current = 0;
298
+ setDirty(false);
299
+ }
300
+ setVersion((prev) => prev + 1);
301
+ }, [initialValues]);
248
302
  const undo = useCallback(() => {
249
303
  if (historyIndexRef.current > 0) {
250
304
  const newIndex = historyIndexRef.current - 1;
@@ -324,6 +378,6 @@ function useCreateFormex({ initialValues, initialErrors, initialDirty, initialTo
324
378
  return controller;
325
379
  }
326
380
  //#endregion
327
- export { Field, Formex, clone, getIn, isEmptyArray, isFunction, isInteger, isNaN, isObject, setIn, useCreateFormex, useFormex };
381
+ export { Field, Formex, clone, getIn, isEmptyArray, isFunction, isInteger, isNaN, isObject, pathTraversesPrototype, setIn, useCreateFormex, useFormex };
328
382
 
329
383
  //# sourceMappingURL=index.es.js.map
@@ -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 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 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\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 initialErrors,\n initialDirty,\n initialTouched,\n validation,\n validateOnChange = false,\n validateOnInitialRender = false,\n onSubmit,\n onReset,\n onValuesChangeDeferred,\n debugId\n}: {\n initialValues: T;\n initialErrors?: Record<string, string>;\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 const initialValuesRef = useRef<T>(initialValues);\n const valuesRef = useRef<T>(initialValues);\n const debugIdRef = useRef<string | undefined>(debugId);\n\n const [values, setValuesInner] = useState<T>(initialValues);\n const [touchedState, setTouchedState] = useState<Record<string, boolean>>(initialTouched ?? {});\n const [errors, setErrors] = useState<Record<string, string>>(initialErrors ?? {});\n const [dirty, setDirty] = useState(initialDirty ?? false);\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[]>([initialValues]);\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 useEffect(() => {\n if (!equal(initialValuesRef.current, initialValues)) {\n resetForm({ values: initialValues });\n }\n }, [initialValues, resetForm]);\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\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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"}
package/dist/types.d.ts CHANGED
@@ -23,7 +23,9 @@ export type FormexController<T = any> = {
23
23
  isValidating: boolean;
24
24
  /**
25
25
  * The version of the form. This is incremented every time the form is reset
26
- * or the form is submitted.
26
+ * or the form is submitted, and whenever the `initialValues` it was created
27
+ * with are replaced — a container reading `values` off the controller needs
28
+ * to hear about a re-seed the same way it hears about a reset.
27
29
  */
28
30
  version: number;
29
31
  debugId?: string;
@@ -1,7 +1,26 @@
1
1
  import { FormexController } from "./types";
2
- export declare function useCreateFormex<T = any>({ initialValues, initialErrors, initialDirty, initialTouched, validation, validateOnChange, validateOnInitialRender, onSubmit, onReset, onValuesChangeDeferred, debugId }: {
2
+ export declare function useCreateFormex<T = any>({ initialValues, initialModifiedValues, initialErrors, initialDirty, initialTouched, validation, validateOnChange, validateOnInitialRender, onSubmit, onReset, onValuesChangeDeferred, debugId }: {
3
+ /**
4
+ * The **baseline**: what the values are stored as. Everything the form
5
+ * calls "dirty" is a difference from this, so it has to be the stored
6
+ * record and nothing else. To open a form already carrying an edit, pass
7
+ * that edit as {@link initialModifiedValues} — folding it into
8
+ * `initialValues` instead makes the baseline agree with the edit, and then
9
+ * nothing can tell that the edit is unsaved.
10
+ */
3
11
  initialValues: T;
12
+ /**
13
+ * What the form should *show* on its first render, when that is not the
14
+ * baseline — an edit handed over from somewhere else, a draft restored
15
+ * from a cache. Dirty is computed from the difference.
16
+ */
17
+ initialModifiedValues?: T;
4
18
  initialErrors?: Record<string, string>;
19
+ /**
20
+ * Force the starting dirty state. Only for callers that know the form
21
+ * opens modified but cannot supply the modified values; prefer
22
+ * {@link initialModifiedValues}, which lets it be derived.
23
+ */
5
24
  initialDirty?: boolean;
6
25
  initialTouched?: Record<string, boolean>;
7
26
  validateOnChange?: boolean;
package/dist/utils.d.ts CHANGED
@@ -14,3 +14,5 @@ export declare const isNaN: (obj: unknown) => boolean;
14
14
  export declare function getIn(obj: unknown, key: string | string[], def?: unknown, p?: number): unknown;
15
15
  export declare function setIn(obj: unknown, path: string, value: unknown): unknown;
16
16
  export declare function clone(value: unknown): unknown;
17
+ /** Whether any segment of this path would traverse the prototype chain. */
18
+ export declare function pathTraversesPrototype(path: string | string[]): boolean;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/forms",
3
3
  "type": "module",
4
- "version": "0.13.0",
4
+ "version": "0.13.1-canary.g18cfeb7",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
7
7
  "access": "public"
package/src/types.ts CHANGED
@@ -24,7 +24,9 @@ export type FormexController<T = any> = {
24
24
  isValidating: boolean;
25
25
  /**
26
26
  * The version of the form. This is incremented every time the form is reset
27
- * or the form is submitted.
27
+ * or the form is submitted, and whenever the `initialValues` it was created
28
+ * with are replaced — a container reading `values` off the controller needs
29
+ * to hear about a re-seed the same way it hears about a reset.
28
30
  */
29
31
  version: number;
30
32
 
@@ -6,6 +6,7 @@ import { FormexController, FormexResetProps } from "./types";
6
6
 
7
7
  export function useCreateFormex<T = any>({
8
8
  initialValues,
9
+ initialModifiedValues,
9
10
  initialErrors,
10
11
  initialDirty,
11
12
  initialTouched,
@@ -17,8 +18,27 @@ export function useCreateFormex<T = any>({
17
18
  onValuesChangeDeferred,
18
19
  debugId
19
20
  }: {
21
+ /**
22
+ * The **baseline**: what the values are stored as. Everything the form
23
+ * calls "dirty" is a difference from this, so it has to be the stored
24
+ * record and nothing else. To open a form already carrying an edit, pass
25
+ * that edit as {@link initialModifiedValues} — folding it into
26
+ * `initialValues` instead makes the baseline agree with the edit, and then
27
+ * nothing can tell that the edit is unsaved.
28
+ */
20
29
  initialValues: T;
30
+ /**
31
+ * What the form should *show* on its first render, when that is not the
32
+ * baseline — an edit handed over from somewhere else, a draft restored
33
+ * from a cache. Dirty is computed from the difference.
34
+ */
35
+ initialModifiedValues?: T;
21
36
  initialErrors?: Record<string, string>;
37
+ /**
38
+ * Force the starting dirty state. Only for callers that know the form
39
+ * opens modified but cannot supply the modified values; prefer
40
+ * {@link initialModifiedValues}, which lets it be derived.
41
+ */
22
42
  initialDirty?: boolean;
23
43
  initialTouched?: Record<string, boolean>;
24
44
  validateOnChange?: boolean;
@@ -35,14 +55,20 @@ export function useCreateFormex<T = any>({
35
55
  onReset?: (controller: FormexController<T>) => void | Promise<void>;
36
56
  debugId?: string;
37
57
  }): FormexController<T> {
58
+ // The baseline and the current values start apart when the form opens
59
+ // already carrying an edit. Keeping them separate is what lets the dirty
60
+ // flag be *derived* rather than asserted, and what lets the baseline be
61
+ // replaced later without touching what the user is looking at.
62
+ const startValues = initialModifiedValues ?? initialValues;
63
+
38
64
  const initialValuesRef = useRef<T>(initialValues);
39
- const valuesRef = useRef<T>(initialValues);
65
+ const valuesRef = useRef<T>(startValues);
40
66
  const debugIdRef = useRef<string | undefined>(debugId);
41
67
 
42
- const [values, setValuesInner] = useState<T>(initialValues);
68
+ const [values, setValuesInner] = useState<T>(startValues);
43
69
  const [touchedState, setTouchedState] = useState<Record<string, boolean>>(initialTouched ?? {});
44
70
  const [errors, setErrors] = useState<Record<string, string>>(initialErrors ?? {});
45
- const [dirty, setDirty] = useState(initialDirty ?? false);
71
+ const [dirty, setDirty] = useState(initialDirty ?? !equal(initialValues, startValues));
46
72
  const [submitCount, setSubmitCount] = useState(0);
47
73
  const [isSubmitting, setIsSubmitting] = useState(false);
48
74
  const [isValidating, setIsValidating] = useState(false);
@@ -64,7 +90,7 @@ export function useCreateFormex<T = any>({
64
90
  }, []);
65
91
 
66
92
  // Replace state for history with refs
67
- const historyRef = useRef<T[]>([initialValues]);
93
+ const historyRef = useRef<T[]>([startValues]);
68
94
  const historyIndexRef = useRef<number>(0);
69
95
 
70
96
  useEffect(() => {
@@ -203,11 +229,44 @@ export function useCreateFormex<T = any>({
203
229
  historyIndexRef.current = 0;
204
230
  }, [onReset, initialTouched]);
205
231
 
232
+ /**
233
+ * The `initialValues` prop moved: the record this form edits finished
234
+ * loading, or was replaced. That is a **re-baseline**, not a reset.
235
+ *
236
+ * It used to call `resetForm({ values: initialValues })`, which is a reset
237
+ * in both of the ways that matter, and both were wrong here:
238
+ *
239
+ * - it fired `onReset`, which callers reasonably read as "the user
240
+ * discarded their changes". The admin clears the cache that seeds an
241
+ * in-flight edit handed over from the side panel there, so a record's own
242
+ * data arriving deleted the edit the form had just been opened with, and
243
+ * the form then re-seeded itself from the server — an expanded record
244
+ * silently lost whatever had been typed into it.
245
+ * - it overwrote `values`, so anything typed while the record was still
246
+ * loading was thrown away without a word.
247
+ *
248
+ * So: move the baseline, leave the edit alone, and re-judge one against
249
+ * the other. Only an untouched form follows the baseline to its new value.
250
+ */
206
251
  useEffect(() => {
207
- if (!equal(initialValuesRef.current, initialValues)) {
208
- resetForm({ values: initialValues });
252
+ if (equal(initialValuesRef.current, initialValues)) return;
253
+
254
+ const modified = !equal(initialValuesRef.current, valuesRef.current);
255
+ initialValuesRef.current = initialValues;
256
+
257
+ if (modified) {
258
+ setDirty(!equal(initialValues, valuesRef.current));
259
+ } else {
260
+ valuesRef.current = initialValues;
261
+ setValuesInner(initialValues);
262
+ historyRef.current = [initialValues];
263
+ historyIndexRef.current = 0;
264
+ setDirty(false);
209
265
  }
210
- }, [initialValues, resetForm]);
266
+ // Containers that read `values` off the controller key on `version`;
267
+ // a re-seed changes what they are holding just as a reset does.
268
+ setVersion((prev: number) => prev + 1);
269
+ }, [initialValues]);
211
270
 
212
271
  const undo = useCallback(() => {
213
272
  if (historyIndexRef.current > 0) {
package/src/utils.ts CHANGED
@@ -28,6 +28,11 @@ export function getIn(
28
28
  def?: unknown,
29
29
  p = 0
30
30
  ): unknown {
31
+ // The read counterpart. `getIn(values, "constructor.prototype")` handing
32
+ // back `Object.prototype` is how a polluted value gets read back out, and
33
+ // how a form comes to render one.
34
+ if (pathTraversesPrototype(key)) return def;
35
+
31
36
  const path = toPath(key);
32
37
  let current: unknown = obj;
33
38
  while (current && p < path.length) {
@@ -43,6 +48,13 @@ export function getIn(
43
48
  }
44
49
 
45
50
  export function setIn(obj: unknown, path: string, value: unknown): unknown {
51
+ // Refused rather than sanitised: there is no legitimate reading of a form
52
+ // field whose path names the prototype chain, and silently rewriting the
53
+ // path would write the value somewhere the caller did not ask for.
54
+ // Returning the original object is what every other no-op in this function
55
+ // does.
56
+ if (pathTraversesPrototype(path)) return obj;
57
+
46
58
  const res = clone(obj) as Record<string, unknown>; // this keeps inheritance when obj is a class
47
59
  let resVal: Record<string, unknown> = res;
48
60
  let i = 0;
@@ -95,6 +107,25 @@ export function clone(value: unknown): unknown {
95
107
  }
96
108
  }
97
109
 
110
+ /**
111
+ * Segments that reach the prototype chain rather than a property of the object.
112
+ *
113
+ * `res["__proto__"] = x` is a setter for the object's prototype, not an own
114
+ * property, so a path of `__proto__.polluted` wrote straight onto
115
+ * `Object.prototype` and gave every object in the process a `polluted`
116
+ * property. `constructor.prototype.x` arrived by a second route, and
117
+ * `__proto__.0` did it to arrays.
118
+ *
119
+ * These are paths, and a path here is a property key — which for a map property
120
+ * or a column mapped out of an imported CSV is data, not code.
121
+ */
122
+ const UNSAFE_PATH_SEGMENTS = new Set(["__proto__", "constructor", "prototype"]);
123
+
124
+ /** Whether any segment of this path would traverse the prototype chain. */
125
+ export function pathTraversesPrototype(path: string | string[]): boolean {
126
+ return toPath(path).some(segment => UNSAFE_PATH_SEGMENTS.has(segment));
127
+ }
128
+
98
129
  function toPath(value: string | string[]) {
99
130
  if (Array.isArray(value)) return value; // Already in path array form.
100
131
  // Replace brackets with dots, remove leading/trailing dots, then split by dot.