@rebasepro/forms 0.23.0 → 0.24.1-canary.g56af4a4
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/Formex.d.ts +22 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +192 -21
- package/dist/index.es.js.map +1 -1
- package/dist/rebase.d.ts +19 -0
- package/dist/useCreateFormex.d.ts +23 -1
- package/package.json +1 -1
package/dist/Formex.d.ts
CHANGED
|
@@ -5,3 +5,25 @@ export declare const Formex: <T = any>({ value, children }: {
|
|
|
5
5
|
value: FormexController<T>;
|
|
6
6
|
children: React.ReactNode;
|
|
7
7
|
}) => React.JSX.Element;
|
|
8
|
+
/**
|
|
9
|
+
* A slice of the enclosing form's controller. The caller re-renders when the
|
|
10
|
+
* slice changes — judged by `isEqual` — and not on every change to the form,
|
|
11
|
+
* which is what {@link useFormex} does.
|
|
12
|
+
*
|
|
13
|
+
* `selector` and `isEqual` are keys: pass stable functions (module-level, or
|
|
14
|
+
* `useCallback`), or the slice is recomputed on every render.
|
|
15
|
+
*
|
|
16
|
+
* @internal Exported for `@rebasepro/cms`'s field bindings, so a keystroke in a
|
|
17
|
+
* record form re-renders the field it went into and not every field. Not part
|
|
18
|
+
* of the supported API; use {@link useFormex}.
|
|
19
|
+
*/
|
|
20
|
+
export declare function useFormexSelector<S, T = any>(selector: (controller: FormexController<T>) => S, isEqual?: (a: S, b: S) => boolean): S;
|
|
21
|
+
/**
|
|
22
|
+
* The enclosing form's controller as it is now, read without subscribing to
|
|
23
|
+
* it: for event handlers and effects of a component that renders from
|
|
24
|
+
* {@link useFormexSelector}. The function itself never changes.
|
|
25
|
+
*
|
|
26
|
+
* @internal Exported for `@rebasepro/cms`'s field bindings. Use
|
|
27
|
+
* {@link useFormex}.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useFormexGetter<T = any>(): () => FormexController<T>;
|
package/dist/index.d.ts
CHANGED
package/dist/index.es.js
CHANGED
|
@@ -1,20 +1,94 @@
|
|
|
1
1
|
import * as React$1 from "react";
|
|
2
|
-
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import React, { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
import { deepEqual } from "fast-equals";
|
|
5
5
|
//#region src/Formex.tsx
|
|
6
6
|
var FormexContext = React.createContext(null);
|
|
7
|
+
var FormexStoreContext = React.createContext(null);
|
|
8
|
+
function createFormexStore(controller) {
|
|
9
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
10
|
+
const store = {
|
|
11
|
+
controller,
|
|
12
|
+
listeners,
|
|
13
|
+
subscribe: (listener) => {
|
|
14
|
+
listeners.add(listener);
|
|
15
|
+
return () => {
|
|
16
|
+
listeners.delete(listener);
|
|
17
|
+
};
|
|
18
|
+
},
|
|
19
|
+
getController: () => store.controller
|
|
20
|
+
};
|
|
21
|
+
return store;
|
|
22
|
+
}
|
|
23
|
+
var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
7
24
|
var useFormex = () => {
|
|
8
25
|
const ctx = useContext(FormexContext);
|
|
9
26
|
if (!ctx) throw new Error("useFormex must be used within a Formex provider");
|
|
10
27
|
return ctx;
|
|
11
28
|
};
|
|
12
29
|
var Formex = ({ value, children }) => {
|
|
30
|
+
const [store] = useState(() => createFormexStore(value));
|
|
31
|
+
store.controller = value;
|
|
32
|
+
useIsomorphicLayoutEffect(() => {
|
|
33
|
+
for (const listener of Array.from(store.listeners)) listener();
|
|
34
|
+
}, [store, value]);
|
|
13
35
|
return /* @__PURE__ */ jsx(FormexContext.Provider, {
|
|
14
36
|
value,
|
|
15
|
-
children
|
|
37
|
+
children: /* @__PURE__ */ jsx(FormexStoreContext.Provider, {
|
|
38
|
+
value: store,
|
|
39
|
+
children
|
|
40
|
+
})
|
|
16
41
|
});
|
|
17
42
|
};
|
|
43
|
+
function useFormexStore() {
|
|
44
|
+
const store = useContext(FormexStoreContext);
|
|
45
|
+
if (!store) throw new Error("useFormex must be used within a Formex provider");
|
|
46
|
+
return store;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A slice of the enclosing form's controller. The caller re-renders when the
|
|
50
|
+
* slice changes — judged by `isEqual` — and not on every change to the form,
|
|
51
|
+
* which is what {@link useFormex} does.
|
|
52
|
+
*
|
|
53
|
+
* `selector` and `isEqual` are keys: pass stable functions (module-level, or
|
|
54
|
+
* `useCallback`), or the slice is recomputed on every render.
|
|
55
|
+
*
|
|
56
|
+
* @internal Exported for `@rebasepro/cms`'s field bindings, so a keystroke in a
|
|
57
|
+
* record form re-renders the field it went into and not every field. Not part
|
|
58
|
+
* of the supported API; use {@link useFormex}.
|
|
59
|
+
*/
|
|
60
|
+
function useFormexSelector(selector, isEqual = Object.is) {
|
|
61
|
+
const store = useFormexStore();
|
|
62
|
+
const getSelection = useMemo(() => {
|
|
63
|
+
let memo;
|
|
64
|
+
return () => {
|
|
65
|
+
const controller = store.controller;
|
|
66
|
+
if (memo && memo.controller === controller) return memo.selection;
|
|
67
|
+
const selection = selector(controller);
|
|
68
|
+
memo = {
|
|
69
|
+
controller,
|
|
70
|
+
selection: memo && isEqual(memo.selection, selection) ? memo.selection : selection
|
|
71
|
+
};
|
|
72
|
+
return memo.selection;
|
|
73
|
+
};
|
|
74
|
+
}, [
|
|
75
|
+
store,
|
|
76
|
+
selector,
|
|
77
|
+
isEqual
|
|
78
|
+
]);
|
|
79
|
+
return useSyncExternalStore(store.subscribe, getSelection, getSelection);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The enclosing form's controller as it is now, read without subscribing to
|
|
83
|
+
* it: for event handlers and effects of a component that renders from
|
|
84
|
+
* {@link useFormexSelector}. The function itself never changes.
|
|
85
|
+
*
|
|
86
|
+
* @internal Exported for `@rebasepro/cms`'s field bindings. Use
|
|
87
|
+
* {@link useFormex}.
|
|
88
|
+
*/
|
|
89
|
+
function useFormexGetter() {
|
|
90
|
+
return useFormexStore().getController;
|
|
91
|
+
}
|
|
18
92
|
//#endregion
|
|
19
93
|
//#region src/utils.ts
|
|
20
94
|
/** @private is the value an empty array? */
|
|
@@ -146,8 +220,76 @@ var getFieldProps = (nameOrOptions, formex) => {
|
|
|
146
220
|
return field;
|
|
147
221
|
};
|
|
148
222
|
//#endregion
|
|
223
|
+
//#region src/rebase.ts
|
|
224
|
+
/**
|
|
225
|
+
* Only plain objects are merged key by key. Arrays, dates and class instances
|
|
226
|
+
* (a reference, a geopoint, a vector) are values: an edit to one replaces it
|
|
227
|
+
* whole, so two edits to one are a conflict rather than something to splice.
|
|
228
|
+
*/
|
|
229
|
+
function isPlainObject(value) {
|
|
230
|
+
if (value === null || typeof value !== "object") return false;
|
|
231
|
+
const proto = Object.getPrototypeOf(value);
|
|
232
|
+
return proto === Object.prototype || proto === null;
|
|
233
|
+
}
|
|
234
|
+
function merge(previous, next, current, path, conflicts) {
|
|
235
|
+
if (deepEqual(current, previous)) return next;
|
|
236
|
+
if (deepEqual(next, previous)) return current;
|
|
237
|
+
if (deepEqual(current, next)) return current;
|
|
238
|
+
if (isPlainObject(previous) && isPlainObject(next) && isPlainObject(current)) {
|
|
239
|
+
const merged = {};
|
|
240
|
+
const keys = /* @__PURE__ */ new Set([
|
|
241
|
+
...Object.keys(previous),
|
|
242
|
+
...Object.keys(next),
|
|
243
|
+
...Object.keys(current)
|
|
244
|
+
]);
|
|
245
|
+
for (const key of keys) {
|
|
246
|
+
const value = merge(previous[key], next[key], current[key], [...path, key], conflicts);
|
|
247
|
+
if (value === void 0 && !(key in current) && !(key in next)) continue;
|
|
248
|
+
merged[key] = value;
|
|
249
|
+
}
|
|
250
|
+
return merged;
|
|
251
|
+
}
|
|
252
|
+
conflicts.push(path.join("."));
|
|
253
|
+
return current;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Rebase an edit onto a baseline that moved under it — a three-way merge of
|
|
257
|
+
* the form's values against the old and new baselines.
|
|
258
|
+
*
|
|
259
|
+
* - a field the edit left alone takes the new baseline's value;
|
|
260
|
+
* - a field the edit changed keeps the edit;
|
|
261
|
+
* - a field both changed keeps the edit and is listed in `conflicts`, by its
|
|
262
|
+
* dotted path.
|
|
263
|
+
*
|
|
264
|
+
* Plain objects are merged field by field, so an edit to `seo.slug` and a
|
|
265
|
+
* change to `seo.description` elsewhere both survive. Arrays and other values
|
|
266
|
+
* are compared whole.
|
|
267
|
+
*
|
|
268
|
+
* @group Form
|
|
269
|
+
*/
|
|
270
|
+
function rebaseEdits(previousBaseline, nextBaseline, current) {
|
|
271
|
+
const conflicts = [];
|
|
272
|
+
return {
|
|
273
|
+
values: merge(previousBaseline, nextBaseline, current, [], conflicts),
|
|
274
|
+
conflicts
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
//#endregion
|
|
149
278
|
//#region src/useCreateFormex.tsx
|
|
150
|
-
|
|
279
|
+
/**
|
|
280
|
+
* How many steps {@link FormexController.undo} can go back. Every edit pushes a
|
|
281
|
+
* snapshot of the whole values, so an unbounded history grew by one copy per
|
|
282
|
+
* keystroke for as long as the form stayed open — 43.5 MB after 10,000
|
|
283
|
+
* keystrokes inside a 500-item array — and every re-baseline re-merged all of
|
|
284
|
+
* it. The oldest steps are dropped past this.
|
|
285
|
+
*/
|
|
286
|
+
var FORMEX_MAX_UNDO_STEPS = 200;
|
|
287
|
+
/** The history with at most {@link FORMEX_MAX_UNDO_STEPS} steps behind its last entry. */
|
|
288
|
+
function capHistory(history) {
|
|
289
|
+
const excess = history.length - 201;
|
|
290
|
+
return excess > 0 ? history.slice(excess) : history;
|
|
291
|
+
}
|
|
292
|
+
function useCreateFormex({ initialValues, initialModifiedValues, initialErrors, initialDirty, initialTouched, validation, validateOnChange = false, validateOnInitialRender = false, onSubmit, beforeSubmit, onReset, onValuesChangeDeferred, onBaselineConflict, debugId }) {
|
|
151
293
|
const startValues = initialModifiedValues ?? initialValues;
|
|
152
294
|
const initialValuesRef = useRef(initialValues);
|
|
153
295
|
const valuesRef = useRef(startValues);
|
|
@@ -164,6 +306,18 @@ function useCreateFormex({ initialValues, initialModifiedValues, initialErrors,
|
|
|
164
306
|
const [version, setVersion] = useState(0);
|
|
165
307
|
const onValuesChangeRef = useRef(onValuesChangeDeferred);
|
|
166
308
|
onValuesChangeRef.current = onValuesChangeDeferred;
|
|
309
|
+
const onBaselineConflictRef = useRef(onBaselineConflict);
|
|
310
|
+
onBaselineConflictRef.current = onBaselineConflict;
|
|
311
|
+
const validationRef = useRef(validation);
|
|
312
|
+
validationRef.current = validation;
|
|
313
|
+
const onSubmitRef = useRef(onSubmit);
|
|
314
|
+
onSubmitRef.current = onSubmit;
|
|
315
|
+
const beforeSubmitRef = useRef(beforeSubmit);
|
|
316
|
+
beforeSubmitRef.current = beforeSubmit;
|
|
317
|
+
const onResetRef = useRef(onReset);
|
|
318
|
+
onResetRef.current = onReset;
|
|
319
|
+
const initialTouchedRef = useRef(initialTouched);
|
|
320
|
+
initialTouchedRef.current = initialTouched;
|
|
167
321
|
const debounceTimeoutRef = useRef(void 0);
|
|
168
322
|
const callDebouncedOnValuesChange = useCallback((values) => {
|
|
169
323
|
if (onValuesChangeRef.current) {
|
|
@@ -181,8 +335,8 @@ function useCreateFormex({ initialValues, initialModifiedValues, initialErrors,
|
|
|
181
335
|
const pushHistory = useCallback((entry) => {
|
|
182
336
|
const newHistory = historyRef.current.slice(0, historyIndexRef.current + 1);
|
|
183
337
|
newHistory.push(entry);
|
|
184
|
-
historyRef.current = newHistory;
|
|
185
|
-
historyIndexRef.current =
|
|
338
|
+
historyRef.current = capHistory(newHistory);
|
|
339
|
+
historyIndexRef.current = historyRef.current.length - 1;
|
|
186
340
|
}, []);
|
|
187
341
|
useEffect(() => {
|
|
188
342
|
if (validateOnInitialRender) validate();
|
|
@@ -196,16 +350,17 @@ function useCreateFormex({ initialValues, initialModifiedValues, initialErrors,
|
|
|
196
350
|
}, [callDebouncedOnValuesChange, pushHistory]);
|
|
197
351
|
const validate = useCallback(async () => {
|
|
198
352
|
setIsValidating(true);
|
|
199
|
-
const validationErrors = await
|
|
353
|
+
const validationErrors = await validationRef.current?.(valuesRef.current);
|
|
200
354
|
setErrors(validationErrors ?? {});
|
|
201
355
|
setIsValidating(false);
|
|
202
356
|
return validationErrors;
|
|
203
|
-
}, [
|
|
357
|
+
}, []);
|
|
204
358
|
const setFieldValue = useCallback((key, value, shouldValidate) => {
|
|
205
359
|
const newValues = setIn(valuesRef.current, key, value);
|
|
206
360
|
valuesRef.current = newValues;
|
|
207
361
|
setValuesInner(newValues);
|
|
208
362
|
if (!deepEqual(getIn(initialValuesRef.current, key), value)) setDirty(true);
|
|
363
|
+
else setDirty(!deepEqual(initialValuesRef.current, newValues));
|
|
209
364
|
if (shouldValidate) validate();
|
|
210
365
|
pushHistory({ values: newValues });
|
|
211
366
|
callDebouncedOnValuesChange(newValues);
|
|
@@ -252,21 +407,22 @@ function useCreateFormex({ initialValues, initialModifiedValues, initialErrors,
|
|
|
252
407
|
e?.stopPropagation();
|
|
253
408
|
setIsSubmitting(true);
|
|
254
409
|
setSubmitCount((prev) => prev + 1);
|
|
255
|
-
|
|
410
|
+
await beforeSubmitRef.current?.();
|
|
411
|
+
const validationErrors = await validationRef.current?.(valuesRef.current);
|
|
256
412
|
if (validationErrors && Object.keys(validationErrors).length > 0) setErrors(validationErrors);
|
|
257
413
|
else {
|
|
258
414
|
setErrors({});
|
|
259
|
-
await
|
|
415
|
+
await onSubmitRef.current?.(valuesRef.current, controllerRef.current);
|
|
260
416
|
}
|
|
261
417
|
setIsSubmitting(false);
|
|
262
418
|
setVersion((prev) => prev + 1);
|
|
263
|
-
}, [
|
|
419
|
+
}, []);
|
|
264
420
|
const resetForm = useCallback((props) => {
|
|
265
421
|
const { submitCount: submitCountProp, values: valuesProp, errors: errorsProp, touched: touchedProp, undoable } = props ?? {};
|
|
266
422
|
const priorValues = valuesRef.current;
|
|
267
423
|
const priorTouched = touchedRef.current;
|
|
268
424
|
const nextValues = valuesProp ?? initialValuesRef.current;
|
|
269
|
-
const nextTouched = touchedProp ??
|
|
425
|
+
const nextTouched = touchedProp ?? initialTouchedRef.current ?? {};
|
|
270
426
|
valuesRef.current = nextValues;
|
|
271
427
|
initialValuesRef.current = nextValues;
|
|
272
428
|
setValuesInner(nextValues);
|
|
@@ -275,7 +431,7 @@ function useCreateFormex({ initialValues, initialModifiedValues, initialErrors,
|
|
|
275
431
|
setDirty(false);
|
|
276
432
|
setSubmitCount(submitCountProp ?? 0);
|
|
277
433
|
setVersion((prev) => prev + 1);
|
|
278
|
-
|
|
434
|
+
onResetRef.current?.(controllerRef.current);
|
|
279
435
|
if (undoable) {
|
|
280
436
|
const kept = historyRef.current.slice(0, historyIndexRef.current + 1);
|
|
281
437
|
kept[kept.length - 1] = {
|
|
@@ -287,13 +443,13 @@ function useCreateFormex({ initialValues, initialModifiedValues, initialErrors,
|
|
|
287
443
|
touched: nextTouched,
|
|
288
444
|
boundary: true
|
|
289
445
|
});
|
|
290
|
-
historyRef.current = kept;
|
|
291
|
-
historyIndexRef.current =
|
|
446
|
+
historyRef.current = capHistory(kept);
|
|
447
|
+
historyIndexRef.current = historyRef.current.length - 1;
|
|
292
448
|
} else {
|
|
293
449
|
historyRef.current = [{ values: nextValues }];
|
|
294
450
|
historyIndexRef.current = 0;
|
|
295
451
|
}
|
|
296
|
-
}, [
|
|
452
|
+
}, []);
|
|
297
453
|
/**
|
|
298
454
|
* The `initialValues` prop moved: the record this form edits finished
|
|
299
455
|
* loading, or was replaced. That is a **re-baseline**, not a reset.
|
|
@@ -310,15 +466,30 @@ function useCreateFormex({ initialValues, initialModifiedValues, initialErrors,
|
|
|
310
466
|
* - it overwrote `values`, so anything typed while the record was still
|
|
311
467
|
* loading was thrown away without a word.
|
|
312
468
|
*
|
|
313
|
-
* So: move the baseline,
|
|
314
|
-
*
|
|
469
|
+
* So: move the baseline, and rebase the edit onto it. Leaving the edit
|
|
470
|
+
* alone *whole* was the next bug: it kept the old value of every field the
|
|
471
|
+
* user never touched, and save — which writes the difference from the live
|
|
472
|
+
* record — wrote those old values back, silently reverting whatever someone
|
|
473
|
+
* else had just changed. So a field the edit left alone follows the new
|
|
474
|
+
* baseline, a field it changed keeps the edit, and a field both changed
|
|
475
|
+
* keeps the edit and is reported through `onBaselineConflict`.
|
|
315
476
|
*/
|
|
316
477
|
useEffect(() => {
|
|
317
478
|
if (deepEqual(initialValuesRef.current, initialValues)) return;
|
|
318
|
-
const
|
|
479
|
+
const previousBaseline = initialValuesRef.current;
|
|
480
|
+
const modified = !deepEqual(previousBaseline, valuesRef.current);
|
|
319
481
|
initialValuesRef.current = initialValues;
|
|
320
|
-
if (modified)
|
|
321
|
-
|
|
482
|
+
if (modified) {
|
|
483
|
+
const { values: rebased, conflicts } = rebaseEdits(previousBaseline, initialValues, valuesRef.current);
|
|
484
|
+
valuesRef.current = rebased;
|
|
485
|
+
setValuesInner(rebased);
|
|
486
|
+
historyRef.current = historyRef.current.map((entry) => ({
|
|
487
|
+
...entry,
|
|
488
|
+
values: rebaseEdits(previousBaseline, initialValues, entry.values).values
|
|
489
|
+
}));
|
|
490
|
+
setDirty(!deepEqual(initialValues, rebased));
|
|
491
|
+
if (conflicts.length > 0) onBaselineConflictRef.current?.(conflicts, controllerRef.current);
|
|
492
|
+
} else {
|
|
322
493
|
valuesRef.current = initialValues;
|
|
323
494
|
setValuesInner(initialValues);
|
|
324
495
|
historyRef.current = [{ values: initialValues }];
|
|
@@ -402,6 +573,6 @@ function useCreateFormex({ initialValues, initialModifiedValues, initialErrors,
|
|
|
402
573
|
return controller;
|
|
403
574
|
}
|
|
404
575
|
//#endregion
|
|
405
|
-
export { Field, Formex, clone, getIn, isEmptyArray, isFunction, isInteger, isNaN, isObject, pathTraversesPrototype, setIn, useCreateFormex, useFormex };
|
|
576
|
+
export { FORMEX_MAX_UNDO_STEPS, Field, Formex, clone, getIn, isEmptyArray, isFunction, isInteger, isNaN, isObject, pathTraversesPrototype, rebaseEdits, setIn, useCreateFormex, useFormex, useFormexGetter, useFormexSelector };
|
|
406
577
|
|
|
407
578
|
//# sourceMappingURL=index.es.js.map
|
package/dist/index.es.js.map
CHANGED
|
@@ -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\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":"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"}
|
|
1
|
+
{"version":3,"file":"index.es.js","names":[],"sources":["../src/Formex.tsx","../src/utils.ts","../src/Field.tsx","../src/rebase.ts","../src/useCreateFormex.tsx"],"sourcesContent":["import React, { useContext, useEffect, useLayoutEffect, useMemo, useState, useSyncExternalStore } from \"react\";\nimport { FormexController } from \"./types\";\n\n\nconst FormexContext = React.createContext<FormexController<any> | null>(null);\n\n/**\n * The controller a {@link Formex} provides, held where a component can read it\n * without subscribing to every change of it — which is what reading the\n * context does. See {@link useFormexSelector}.\n */\ntype FormexStore = {\n controller: FormexController<any>;\n readonly listeners: Set<() => void>;\n readonly subscribe: (listener: () => void) => () => void;\n readonly getController: () => FormexController<any>;\n};\n\nconst FormexStoreContext = React.createContext<FormexStore | null>(null);\n\nfunction createFormexStore(controller: FormexController<any>): FormexStore {\n const listeners = new Set<() => void>();\n const store: FormexStore = {\n controller,\n listeners,\n subscribe: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n getController: () => store.controller\n };\n return store;\n}\n\nconst useIsomorphicLayoutEffect = typeof window !== \"undefined\" ? useLayoutEffect : useEffect;\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 const [store] = useState(() => createFormexStore(value));\n // During render rather than in an effect: a component this same render\n // reaches — an array item mounting, a field whose props changed — has to\n // read the controller it is being rendered with, not the one before it.\n store.controller = value;\n useIsomorphicLayoutEffect(() => {\n // Before paint, so a field that subscribed to its own slice of the form\n // shows a change in the same frame as everything that read the context.\n for (const listener of Array.from(store.listeners)) listener();\n }, [store, value]);\n return <FormexContext.Provider value={value}>\n <FormexStoreContext.Provider value={store}>{children}</FormexStoreContext.Provider>\n </FormexContext.Provider>;\n};\n\nfunction useFormexStore(): FormexStore {\n const store = useContext(FormexStoreContext);\n if (!store) throw new Error(\"useFormex must be used within a Formex provider\");\n return store;\n}\n\n/**\n * A slice of the enclosing form's controller. The caller re-renders when the\n * slice changes — judged by `isEqual` — and not on every change to the form,\n * which is what {@link useFormex} does.\n *\n * `selector` and `isEqual` are keys: pass stable functions (module-level, or\n * `useCallback`), or the slice is recomputed on every render.\n *\n * @internal Exported for `@rebasepro/cms`'s field bindings, so a keystroke in a\n * record form re-renders the field it went into and not every field. Not part\n * of the supported API; use {@link useFormex}.\n */\nexport function useFormexSelector<S, T = any>(\n selector: (controller: FormexController<T>) => S,\n isEqual: (a: S, b: S) => boolean = Object.is\n): S {\n const store = useFormexStore();\n const getSelection = useMemo(() => {\n // Recomputed once per controller, and handed back unchanged while it is\n // equal to the last one: `useSyncExternalStore` re-renders on identity.\n let memo: { controller: FormexController<any>, selection: S } | undefined;\n return () => {\n const controller = store.controller;\n if (memo && memo.controller === controller) return memo.selection;\n const selection = selector(controller as FormexController<T>);\n memo = { controller, selection: memo && isEqual(memo.selection, selection) ? memo.selection : selection };\n return memo.selection;\n };\n }, [store, selector, isEqual]);\n return useSyncExternalStore(store.subscribe, getSelection, getSelection);\n}\n\n/**\n * The enclosing form's controller as it is now, read without subscribing to\n * it: for event handlers and effects of a component that renders from\n * {@link useFormexSelector}. The function itself never changes.\n *\n * @internal Exported for `@rebasepro/cms`'s field bindings. Use\n * {@link useFormex}.\n */\nexport function useFormexGetter<T = any>(): () => FormexController<T> {\n return useFormexStore().getController as () => FormexController<T>;\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 { deepEqual as equal } from \"fast-equals\";\n\n/**\n * Only plain objects are merged key by key. Arrays, dates and class instances\n * (a reference, a geopoint, a vector) are values: an edit to one replaces it\n * whole, so two edits to one are a conflict rather than something to splice.\n */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== \"object\") return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\nfunction merge(\n previous: unknown,\n next: unknown,\n current: unknown,\n path: string[],\n conflicts: string[]\n): unknown {\n // Not edited here: follow the new baseline.\n if (equal(current, previous)) return next;\n // Not changed there: keep the edit.\n if (equal(next, previous)) return current;\n // Both sides arrived at the same value.\n if (equal(current, next)) return current;\n\n if (isPlainObject(previous) && isPlainObject(next) && isPlainObject(current)) {\n const merged: Record<string, unknown> = {};\n const keys = new Set([...Object.keys(previous), ...Object.keys(next), ...Object.keys(current)]);\n for (const key of keys) {\n const value = merge(previous[key], next[key], current[key], [...path, key], conflicts);\n if (value === undefined && !(key in current) && !(key in next)) continue;\n merged[key] = value;\n }\n return merged;\n }\n\n // Both sides changed it, differently. The edit is what the user is looking\n // at, so it stays; the caller is told so it can say so.\n conflicts.push(path.join(\".\"));\n return current;\n}\n\n/**\n * Rebase an edit onto a baseline that moved under it — a three-way merge of\n * the form's values against the old and new baselines.\n *\n * - a field the edit left alone takes the new baseline's value;\n * - a field the edit changed keeps the edit;\n * - a field both changed keeps the edit and is listed in `conflicts`, by its\n * dotted path.\n *\n * Plain objects are merged field by field, so an edit to `seo.slug` and a\n * change to `seo.description` elsewhere both survive. Arrays and other values\n * are compared whole.\n *\n * @group Form\n */\nexport function rebaseEdits<T>(previousBaseline: T, nextBaseline: T, current: T): { values: T; conflicts: string[] } {\n const conflicts: string[] = [];\n const values = merge(previousBaseline, nextBaseline, current, [], conflicts) as T;\n return { values, conflicts };\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\";\nimport { rebaseEdits } from \"./rebase\";\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\n/**\n * How many steps {@link FormexController.undo} can go back. Every edit pushes a\n * snapshot of the whole values, so an unbounded history grew by one copy per\n * keystroke for as long as the form stayed open — 43.5 MB after 10,000\n * keystrokes inside a 500-item array — and every re-baseline re-merged all of\n * it. The oldest steps are dropped past this.\n */\nexport const FORMEX_MAX_UNDO_STEPS = 200;\n\n/** The history with at most {@link FORMEX_MAX_UNDO_STEPS} steps behind its last entry. */\nfunction capHistory<E>(history: E[]): E[] {\n const excess = history.length - (FORMEX_MAX_UNDO_STEPS + 1);\n return excess > 0 ? history.slice(excess) : history;\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 beforeSubmit,\n onReset,\n onValuesChangeDeferred,\n onBaselineConflict,\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 /**\n * Awaited at the start of a submit, before the values are read — so\n * validation and `onSubmit` both see what it left. The record form waits\n * here for a file upload in progress, whose path lands in the values\n * only when it finishes.\n */\n beforeSubmit?: () => void | Promise<void>;\n onReset?: (controller: FormexController<T>) => void | Promise<void>;\n /**\n * The baseline moved while the form was edited, and some fields were\n * changed on both sides. Called with their dotted paths. The form keeps\n * the user's value for those (see the re-baseline below); this is the\n * chance to tell them that saving will replace someone else's change.\n */\n onBaselineConflict?: (paths: string[], controller: FormexController<T>) => 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 // Every callback the caller hands in is read through a ref, so the\n // controller's own functions never change identity. A caller passing\n // inline functions — `EntityForm` does — used to give the controller a new\n // identity on *every* render, and every field reading it re-rendered with\n // it. The controller now changes with its state, and only with its state.\n const onValuesChangeRef = useRef(onValuesChangeDeferred);\n onValuesChangeRef.current = onValuesChangeDeferred;\n const onBaselineConflictRef = useRef(onBaselineConflict);\n onBaselineConflictRef.current = onBaselineConflict;\n const validationRef = useRef(validation);\n validationRef.current = validation;\n const onSubmitRef = useRef(onSubmit);\n onSubmitRef.current = onSubmit;\n const beforeSubmitRef = useRef(beforeSubmit);\n beforeSubmitRef.current = beforeSubmit;\n const onResetRef = useRef(onReset);\n onResetRef.current = onReset;\n const initialTouchedRef = useRef(initialTouched);\n initialTouchedRef.current = initialTouched;\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 = capHistory(newHistory);\n historyIndexRef.current = historyRef.current.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 validationRef.current?.(valuesRef.current);\n setErrors(validationErrors ?? {});\n setIsValidating(false);\n return validationErrors;\n }, []);\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 } else {\n // Typed back to its stored value: the form is clean again\n // unless another field still differs. The whole comparison\n // runs only here, not on every keystroke.\n setDirty(!equal(initialValuesRef.current, newValues));\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 await beforeSubmitRef.current?.();\n const validationErrors = await validationRef.current?.(valuesRef.current);\n if (validationErrors && Object.keys(validationErrors).length > 0) {\n setErrors(validationErrors);\n } else {\n setErrors({});\n await onSubmitRef.current?.(valuesRef.current, controllerRef.current);\n }\n setIsSubmitting(false);\n setVersion((prev: number) => prev + 1);\n },\n []\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 ?? initialTouchedRef.current ?? {};\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 onResetRef.current?.(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 // Capped like any push; the pair just written is the newest, so it\n // is never what goes.\n historyRef.current = capHistory(kept);\n historyIndexRef.current = historyRef.current.length - 1;\n } else {\n historyRef.current = [{ values: nextValues }];\n historyIndexRef.current = 0;\n }\n }, []);\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, and rebase the edit onto it. Leaving the edit\n * alone *whole* was the next bug: it kept the old value of every field the\n * user never touched, and save — which writes the difference from the live\n * record — wrote those old values back, silently reverting whatever someone\n * else had just changed. So a field the edit left alone follows the new\n * baseline, a field it changed keeps the edit, and a field both changed\n * keeps the edit and is reported through `onBaselineConflict`.\n */\n useEffect(() => {\n if (equal(initialValuesRef.current, initialValues)) return;\n\n const previousBaseline = initialValuesRef.current;\n const modified = !equal(previousBaseline, valuesRef.current);\n initialValuesRef.current = initialValues;\n\n if (modified) {\n const { values: rebased, conflicts } = rebaseEdits(previousBaseline, initialValues, valuesRef.current);\n valuesRef.current = rebased;\n setValuesInner(rebased);\n // Undo must not step back into a stale copy of an untouched field\n // either, so the history is rebased the same way.\n historyRef.current = historyRef.current.map((entry) => ({\n ...entry,\n values: rebaseEdits(previousBaseline, initialValues, entry.values).values\n }));\n setDirty(!equal(initialValues, rebased));\n if (conflicts.length > 0) {\n onBaselineConflictRef.current?.(conflicts, controllerRef.current);\n }\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":"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"}
|
package/dist/rebase.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rebase an edit onto a baseline that moved under it — a three-way merge of
|
|
3
|
+
* the form's values against the old and new baselines.
|
|
4
|
+
*
|
|
5
|
+
* - a field the edit left alone takes the new baseline's value;
|
|
6
|
+
* - a field the edit changed keeps the edit;
|
|
7
|
+
* - a field both changed keeps the edit and is listed in `conflicts`, by its
|
|
8
|
+
* dotted path.
|
|
9
|
+
*
|
|
10
|
+
* Plain objects are merged field by field, so an edit to `seo.slug` and a
|
|
11
|
+
* change to `seo.description` elsewhere both survive. Arrays and other values
|
|
12
|
+
* are compared whole.
|
|
13
|
+
*
|
|
14
|
+
* @group Form
|
|
15
|
+
*/
|
|
16
|
+
export declare function rebaseEdits<T>(previousBaseline: T, nextBaseline: T, current: T): {
|
|
17
|
+
values: T;
|
|
18
|
+
conflicts: string[];
|
|
19
|
+
};
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { FormexController } from "./types.js";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* How many steps {@link FormexController.undo} can go back. Every edit pushes a
|
|
4
|
+
* snapshot of the whole values, so an unbounded history grew by one copy per
|
|
5
|
+
* keystroke for as long as the form stayed open — 43.5 MB after 10,000
|
|
6
|
+
* keystrokes inside a 500-item array — and every re-baseline re-merged all of
|
|
7
|
+
* it. The oldest steps are dropped past this.
|
|
8
|
+
*/
|
|
9
|
+
export declare const FORMEX_MAX_UNDO_STEPS = 200;
|
|
10
|
+
export declare function useCreateFormex<T = any>({ initialValues, initialModifiedValues, initialErrors, initialDirty, initialTouched, validation, validateOnChange, validateOnInitialRender, onSubmit, beforeSubmit, onReset, onValuesChangeDeferred, onBaselineConflict, debugId }: {
|
|
3
11
|
/**
|
|
4
12
|
* The **baseline**: what the values are stored as. Everything the form
|
|
5
13
|
* calls "dirty" is a difference from this, so it has to be the stored
|
|
@@ -28,6 +36,20 @@ export declare function useCreateFormex<T = any>({ initialValues, initialModifie
|
|
|
28
36
|
validation?: (values: T) => Record<string, string> | Promise<Record<string, string>> | undefined | void;
|
|
29
37
|
onValuesChangeDeferred?: (values: T, controller: FormexController<T>) => void;
|
|
30
38
|
onSubmit?: (values: T, controller: FormexController<T>) => void | Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Awaited at the start of a submit, before the values are read — so
|
|
41
|
+
* validation and `onSubmit` both see what it left. The record form waits
|
|
42
|
+
* here for a file upload in progress, whose path lands in the values
|
|
43
|
+
* only when it finishes.
|
|
44
|
+
*/
|
|
45
|
+
beforeSubmit?: () => void | Promise<void>;
|
|
31
46
|
onReset?: (controller: FormexController<T>) => void | Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* The baseline moved while the form was edited, and some fields were
|
|
49
|
+
* changed on both sides. Called with their dotted paths. The form keeps
|
|
50
|
+
* the user's value for those (see the re-baseline below); this is the
|
|
51
|
+
* chance to tell them that saving will replace someone else's change.
|
|
52
|
+
*/
|
|
53
|
+
onBaselineConflict?: (paths: string[], controller: FormexController<T>) => void;
|
|
32
54
|
debugId?: string;
|
|
33
55
|
}): FormexController<T>;
|