@lucas-barake/effect-form 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/FormAtoms.js +91 -34
- package/dist/cjs/FormAtoms.js.map +1 -1
- package/dist/cjs/FormBuilder.js.map +1 -1
- package/dist/cjs/internal/dirty.js +29 -6
- package/dist/cjs/internal/dirty.js.map +1 -1
- package/dist/cjs/internal/path.js +13 -2
- package/dist/cjs/internal/path.js.map +1 -1
- package/dist/dts/FormAtoms.d.ts +14 -16
- package/dist/dts/FormAtoms.d.ts.map +1 -1
- package/dist/dts/FormBuilder.d.ts +9 -1
- package/dist/dts/FormBuilder.d.ts.map +1 -1
- package/dist/dts/index.d.ts +2 -4
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/internal/dirty.d.ts.map +1 -1
- package/dist/dts/internal/path.d.ts +10 -0
- package/dist/dts/internal/path.d.ts.map +1 -1
- package/dist/esm/FormAtoms.js +92 -34
- package/dist/esm/FormAtoms.js.map +1 -1
- package/dist/esm/FormBuilder.js.map +1 -1
- package/dist/esm/index.js +2 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internal/dirty.js +30 -7
- package/dist/esm/internal/dirty.js.map +1 -1
- package/dist/esm/internal/path.js +10 -0
- package/dist/esm/internal/path.js.map +1 -1
- package/package.json +1 -1
- package/src/FormAtoms.ts +120 -61
- package/src/FormBuilder.ts +10 -1
- package/src/index.ts +2 -4
- package/src/internal/dirty.ts +38 -12
- package/src/internal/path.ts +12 -0
package/src/FormAtoms.ts
CHANGED
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Atom infrastructure for form state management.
|
|
3
|
-
*
|
|
4
|
-
* This module provides the core atom infrastructure that framework adapters
|
|
5
|
-
* (React, Vue, Svelte, Solid) can use to build reactive form components.
|
|
6
|
-
*
|
|
7
|
-
* @since 1.0.0
|
|
8
|
-
*/
|
|
9
1
|
import * as Atom from "@effect-atom/atom/Atom"
|
|
10
|
-
import type * as Registry from "@effect-atom/atom/Registry"
|
|
11
2
|
import * as Effect from "effect/Effect"
|
|
12
3
|
import { pipe } from "effect/Function"
|
|
13
4
|
import * as Option from "effect/Option"
|
|
@@ -16,8 +7,9 @@ import * as Schema from "effect/Schema"
|
|
|
16
7
|
import * as Field from "./Field.js"
|
|
17
8
|
import * as FormBuilder from "./FormBuilder.js"
|
|
18
9
|
import { recalculateDirtyFieldsForArray, recalculateDirtySubtree } from "./internal/dirty.js"
|
|
19
|
-
import { getNestedValue, setNestedValue } from "./internal/path.js"
|
|
10
|
+
import { getNestedValue, isPathUnderRoot, setNestedValue } from "./internal/path.js"
|
|
20
11
|
import { createWeakRegistry, type WeakRegistry } from "./internal/weak-registry.js"
|
|
12
|
+
import * as Validation from "./Validation.js"
|
|
21
13
|
|
|
22
14
|
/**
|
|
23
15
|
* Atoms for a single field.
|
|
@@ -38,9 +30,10 @@ export interface FieldAtoms {
|
|
|
38
30
|
* @since 1.0.0
|
|
39
31
|
* @category Models
|
|
40
32
|
*/
|
|
41
|
-
export interface FormAtomsConfig<TFields extends Field.FieldsRecord, R> {
|
|
33
|
+
export interface FormAtomsConfig<TFields extends Field.FieldsRecord, R, A, E> {
|
|
42
34
|
readonly runtime: Atom.AtomRuntime<R, any>
|
|
43
35
|
readonly formBuilder: FormBuilder.FormBuilder<TFields, R>
|
|
36
|
+
readonly onSubmit: (decoded: Field.DecodedFromFields<TFields>, get: Atom.FnContext) => A | Effect.Effect<A, E, R>
|
|
44
37
|
}
|
|
45
38
|
|
|
46
39
|
/**
|
|
@@ -63,7 +56,7 @@ export type FieldRefs<TFields extends Field.FieldsRecord> = {
|
|
|
63
56
|
* @since 1.0.0
|
|
64
57
|
* @category Models
|
|
65
58
|
*/
|
|
66
|
-
export interface FormAtoms<TFields extends Field.FieldsRecord, R> {
|
|
59
|
+
export interface FormAtoms<TFields extends Field.FieldsRecord, R, A = void, E = never> {
|
|
67
60
|
readonly stateAtom: Atom.Writable<
|
|
68
61
|
Option.Option<FormBuilder.FormState<TFields>>,
|
|
69
62
|
Option.Option<FormBuilder.FormState<TFields>>
|
|
@@ -72,15 +65,11 @@ export interface FormAtoms<TFields extends Field.FieldsRecord, R> {
|
|
|
72
65
|
readonly dirtyFieldsAtom: Atom.Atom<ReadonlySet<string>>
|
|
73
66
|
readonly isDirtyAtom: Atom.Atom<boolean>
|
|
74
67
|
readonly submitCountAtom: Atom.Atom<number>
|
|
75
|
-
readonly lastSubmittedValuesAtom: Atom.Atom<Option.Option<
|
|
68
|
+
readonly lastSubmittedValuesAtom: Atom.Atom<Option.Option<FormBuilder.SubmittedValues<TFields>>>
|
|
76
69
|
readonly changedSinceSubmitFieldsAtom: Atom.Atom<ReadonlySet<string>>
|
|
77
70
|
readonly hasChangedSinceSubmitAtom: Atom.Atom<boolean>
|
|
78
|
-
readonly onSubmitAtom: Atom.Writable<
|
|
79
|
-
Atom.AtomResultFn<Field.DecodedFromFields<TFields>, unknown, unknown> | null,
|
|
80
|
-
Atom.AtomResultFn<Field.DecodedFromFields<TFields>, unknown, unknown> | null
|
|
81
|
-
>
|
|
82
71
|
|
|
83
|
-
readonly
|
|
72
|
+
readonly submitAtom: Atom.AtomResultFn<void, A, E | ParseResult.ParseError>
|
|
84
73
|
|
|
85
74
|
readonly combinedSchema: Schema.Schema<Field.DecodedFromFields<TFields>, Field.EncodedFromFields<TFields>, R>
|
|
86
75
|
|
|
@@ -96,9 +85,14 @@ export interface FormAtoms<TFields extends Field.FieldsRecord, R> {
|
|
|
96
85
|
|
|
97
86
|
readonly getOrCreateFieldAtoms: (fieldPath: string) => FieldAtoms
|
|
98
87
|
|
|
99
|
-
readonly resetValidationAtoms: (
|
|
88
|
+
readonly resetValidationAtoms: (ctx: { set: <R, W>(atom: Atom.Writable<R, W>, value: W) => void }) => void
|
|
100
89
|
|
|
101
90
|
readonly operations: FormOperations<TFields>
|
|
91
|
+
|
|
92
|
+
readonly resetAtom: Atom.Writable<void, void>
|
|
93
|
+
readonly revertToLastSubmitAtom: Atom.Writable<void, void>
|
|
94
|
+
readonly setValuesAtom: Atom.Writable<void, Field.EncodedFromFields<TFields>>
|
|
95
|
+
readonly setValue: <S>(field: FormBuilder.FieldRef<S>) => Atom.Writable<void, S | ((prev: S) => S)>
|
|
102
96
|
}
|
|
103
97
|
|
|
104
98
|
/**
|
|
@@ -191,9 +185,9 @@ export interface FormOperations<TFields extends Field.FieldsRecord> {
|
|
|
191
185
|
* @since 1.0.0
|
|
192
186
|
* @category Constructors
|
|
193
187
|
*/
|
|
194
|
-
export const make = <TFields extends Field.FieldsRecord, R>(
|
|
195
|
-
config: FormAtomsConfig<TFields, R>,
|
|
196
|
-
): FormAtoms<TFields, R> => {
|
|
188
|
+
export const make = <TFields extends Field.FieldsRecord, R, A, E>(
|
|
189
|
+
config: FormAtomsConfig<TFields, R, A, E>,
|
|
190
|
+
): FormAtoms<TFields, R, A, E> => {
|
|
197
191
|
const { formBuilder, runtime } = config
|
|
198
192
|
const { fields } = formBuilder
|
|
199
193
|
|
|
@@ -222,21 +216,17 @@ export const make = <TFields extends Field.FieldsRecord, R>(
|
|
|
222
216
|
const state = Option.getOrThrow(get(stateAtom))
|
|
223
217
|
return Option.match(state.lastSubmittedValues, {
|
|
224
218
|
onNone: () => new Set<string>(),
|
|
225
|
-
onSome: (lastSubmitted) => recalculateDirtySubtree(new Set(), lastSubmitted, state.values, ""),
|
|
219
|
+
onSome: (lastSubmitted) => recalculateDirtySubtree(new Set(), lastSubmitted.encoded, state.values, ""),
|
|
226
220
|
})
|
|
227
221
|
}).pipe(Atom.setIdleTTL(0))
|
|
228
222
|
|
|
229
223
|
const hasChangedSinceSubmitAtom = Atom.readable((get) => {
|
|
230
224
|
const state = Option.getOrThrow(get(stateAtom))
|
|
231
225
|
if (Option.isNone(state.lastSubmittedValues)) return false
|
|
232
|
-
if (state.values === state.lastSubmittedValues.value) return false
|
|
226
|
+
if (state.values === state.lastSubmittedValues.value.encoded) return false
|
|
233
227
|
return get(changedSinceSubmitFieldsAtom).size > 0
|
|
234
228
|
}).pipe(Atom.setIdleTTL(0))
|
|
235
229
|
|
|
236
|
-
const onSubmitAtom = Atom.make<Atom.AtomResultFn<Field.DecodedFromFields<TFields>, unknown, unknown> | null>(
|
|
237
|
-
null,
|
|
238
|
-
).pipe(Atom.setIdleTTL(0))
|
|
239
|
-
|
|
240
230
|
const validationAtomsRegistry = createWeakRegistry<Atom.AtomResultFn<unknown, void, ParseResult.ParseError>>()
|
|
241
231
|
const fieldAtomsRegistry = createWeakRegistry<FieldAtoms>()
|
|
242
232
|
|
|
@@ -266,20 +256,7 @@ export const make = <TFields extends Field.FieldsRecord, R>(
|
|
|
266
256
|
(get) => getNestedValue(Option.getOrThrow(get(stateAtom)).values, fieldPath),
|
|
267
257
|
(ctx, value) => {
|
|
268
258
|
const currentState = Option.getOrThrow(ctx.get(stateAtom))
|
|
269
|
-
|
|
270
|
-
ctx.set(
|
|
271
|
-
stateAtom,
|
|
272
|
-
Option.some({
|
|
273
|
-
...currentState,
|
|
274
|
-
values: newValues,
|
|
275
|
-
dirtyFields: recalculateDirtySubtree(
|
|
276
|
-
currentState.dirtyFields,
|
|
277
|
-
currentState.initialValues,
|
|
278
|
-
newValues,
|
|
279
|
-
fieldPath,
|
|
280
|
-
),
|
|
281
|
-
}),
|
|
282
|
-
)
|
|
259
|
+
ctx.set(stateAtom, Option.some(operations.setFieldValue(currentState, fieldPath, value)))
|
|
283
260
|
},
|
|
284
261
|
).pipe(Atom.setIdleTTL(0))
|
|
285
262
|
|
|
@@ -312,26 +289,49 @@ export const make = <TFields extends Field.FieldsRecord, R>(
|
|
|
312
289
|
return atoms
|
|
313
290
|
}
|
|
314
291
|
|
|
315
|
-
const resetValidationAtoms = (
|
|
292
|
+
const resetValidationAtoms = (ctx: { set: <R, W>(atom: Atom.Writable<R, W>, value: W) => void }) => {
|
|
316
293
|
for (const validationAtom of validationAtomsRegistry.values()) {
|
|
317
|
-
|
|
294
|
+
ctx.set(validationAtom, Atom.Reset)
|
|
318
295
|
}
|
|
319
296
|
validationAtomsRegistry.clear()
|
|
320
297
|
fieldAtomsRegistry.clear()
|
|
321
298
|
}
|
|
322
299
|
|
|
323
|
-
const
|
|
300
|
+
const submitAtom = runtime.fn<void>()((_void, get) =>
|
|
324
301
|
Effect.gen(function*() {
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
302
|
+
const state = get(stateAtom)
|
|
303
|
+
if (Option.isNone(state)) return yield* Effect.die("Form not initialized")
|
|
304
|
+
const values = state.value.values
|
|
305
|
+
get.set(crossFieldErrorsAtom, new Map())
|
|
306
|
+
const decoded = yield* pipe(
|
|
307
|
+
Schema.decodeUnknown(combinedSchema)(values) as Effect.Effect<
|
|
308
|
+
Field.DecodedFromFields<TFields>,
|
|
309
|
+
ParseResult.ParseError,
|
|
310
|
+
R
|
|
311
|
+
>,
|
|
312
|
+
Effect.tapError((parseError) =>
|
|
313
|
+
Effect.sync(() => {
|
|
314
|
+
const routedErrors = Validation.routeErrors(parseError)
|
|
315
|
+
get.set(crossFieldErrorsAtom, routedErrors)
|
|
316
|
+
get.set(stateAtom, Option.some(operations.createSubmitState(state.value)))
|
|
317
|
+
})
|
|
318
|
+
),
|
|
319
|
+
)
|
|
320
|
+
const submitState = operations.createSubmitState(state.value)
|
|
321
|
+
get.set(
|
|
322
|
+
stateAtom,
|
|
323
|
+
Option.some({
|
|
324
|
+
...submitState,
|
|
325
|
+
lastSubmittedValues: Option.some({ encoded: values, decoded }),
|
|
326
|
+
}),
|
|
327
|
+
)
|
|
328
|
+
const result = config.onSubmit(decoded, get)
|
|
329
|
+
if (Effect.isEffect(result)) {
|
|
330
|
+
return yield* (result as Effect.Effect<A, E, R>)
|
|
331
|
+
}
|
|
332
|
+
return result as A
|
|
333
333
|
})
|
|
334
|
-
).pipe(Atom.setIdleTTL(0)) as Atom.AtomResultFn<
|
|
334
|
+
).pipe(Atom.setIdleTTL(0)) as Atom.AtomResultFn<void, A, E | ParseResult.ParseError>
|
|
335
335
|
|
|
336
336
|
const fieldRefs = Object.fromEntries(
|
|
337
337
|
Object.keys(fields).map((key) => [key, FormBuilder.makeFieldRef(key)]),
|
|
@@ -358,7 +358,6 @@ export const make = <TFields extends Field.FieldsRecord, R>(
|
|
|
358
358
|
|
|
359
359
|
createSubmitState: (state) => ({
|
|
360
360
|
...state,
|
|
361
|
-
lastSubmittedValues: Option.some(state.values),
|
|
362
361
|
touched: Field.createTouchedRecord(fields, true) as { readonly [K in keyof TFields]: boolean },
|
|
363
362
|
submitCount: state.submitCount + 1,
|
|
364
363
|
}),
|
|
@@ -462,25 +461,82 @@ export const make = <TFields extends Field.FieldsRecord, R>(
|
|
|
462
461
|
return state
|
|
463
462
|
}
|
|
464
463
|
|
|
465
|
-
|
|
464
|
+
const lastEncoded = state.lastSubmittedValues.value.encoded
|
|
465
|
+
if (state.values === lastEncoded) {
|
|
466
466
|
return state
|
|
467
467
|
}
|
|
468
468
|
|
|
469
469
|
const newDirtyFields = recalculateDirtySubtree(
|
|
470
470
|
state.dirtyFields,
|
|
471
471
|
state.initialValues,
|
|
472
|
-
|
|
472
|
+
lastEncoded,
|
|
473
473
|
"",
|
|
474
474
|
)
|
|
475
475
|
|
|
476
476
|
return {
|
|
477
477
|
...state,
|
|
478
|
-
values:
|
|
478
|
+
values: lastEncoded,
|
|
479
479
|
dirtyFields: newDirtyFields,
|
|
480
480
|
}
|
|
481
481
|
},
|
|
482
482
|
}
|
|
483
483
|
|
|
484
|
+
const resetAtom = Atom.fnSync<void>()((_: void, get) => {
|
|
485
|
+
const state = get(stateAtom)
|
|
486
|
+
if (Option.isNone(state)) return
|
|
487
|
+
get.set(stateAtom, Option.some(operations.createResetState(state.value)))
|
|
488
|
+
get.set(crossFieldErrorsAtom, new Map())
|
|
489
|
+
resetValidationAtoms(get)
|
|
490
|
+
get.set(submitAtom, Atom.Reset)
|
|
491
|
+
}, { initialValue: undefined as void }).pipe(Atom.setIdleTTL(0))
|
|
492
|
+
|
|
493
|
+
const revertToLastSubmitAtom = Atom.fnSync<void>()((_: void, get) => {
|
|
494
|
+
const state = get(stateAtom)
|
|
495
|
+
if (Option.isNone(state)) return
|
|
496
|
+
get.set(stateAtom, Option.some(operations.revertToLastSubmit(state.value)))
|
|
497
|
+
get.set(crossFieldErrorsAtom, new Map())
|
|
498
|
+
}, { initialValue: undefined as void }).pipe(Atom.setIdleTTL(0))
|
|
499
|
+
|
|
500
|
+
const setValuesAtom = Atom.fnSync<Field.EncodedFromFields<TFields>>()((_values, get) => {
|
|
501
|
+
const state = get(stateAtom)
|
|
502
|
+
if (Option.isNone(state)) return
|
|
503
|
+
get.set(stateAtom, Option.some(operations.setFormValues(state.value, _values)))
|
|
504
|
+
get.set(crossFieldErrorsAtom, new Map())
|
|
505
|
+
}, { initialValue: undefined as void }).pipe(Atom.setIdleTTL(0))
|
|
506
|
+
|
|
507
|
+
const setValueAtomsRegistry = createWeakRegistry<Atom.Writable<void, any>>()
|
|
508
|
+
|
|
509
|
+
const setValue = <S>(field: FormBuilder.FieldRef<S>): Atom.Writable<void, S | ((prev: S) => S)> => {
|
|
510
|
+
const cached = setValueAtomsRegistry.get(field.key)
|
|
511
|
+
if (cached) return cached
|
|
512
|
+
|
|
513
|
+
const atom = Atom.fnSync<S | ((prev: S) => S)>()((update, get) => {
|
|
514
|
+
const state = get(stateAtom)
|
|
515
|
+
if (Option.isNone(state)) return
|
|
516
|
+
|
|
517
|
+
const currentValue = getNestedValue(state.value.values, field.key) as S
|
|
518
|
+
const newValue = typeof update === "function"
|
|
519
|
+
? (update as (prev: S) => S)(currentValue)
|
|
520
|
+
: update
|
|
521
|
+
|
|
522
|
+
get.set(stateAtom, Option.some(operations.setFieldValue(state.value, field.key, newValue)))
|
|
523
|
+
|
|
524
|
+
const currentErrors = get(crossFieldErrorsAtom)
|
|
525
|
+
const nextErrors = new Map<string, string>()
|
|
526
|
+
for (const [errorPath, message] of currentErrors) {
|
|
527
|
+
if (!isPathUnderRoot(errorPath, field.key)) {
|
|
528
|
+
nextErrors.set(errorPath, message)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (nextErrors.size !== currentErrors.size) {
|
|
532
|
+
get.set(crossFieldErrorsAtom, nextErrors)
|
|
533
|
+
}
|
|
534
|
+
}, { initialValue: undefined as void }).pipe(Atom.setIdleTTL(0))
|
|
535
|
+
|
|
536
|
+
setValueAtomsRegistry.set(field.key, atom)
|
|
537
|
+
return atom
|
|
538
|
+
}
|
|
539
|
+
|
|
484
540
|
return {
|
|
485
541
|
stateAtom,
|
|
486
542
|
crossFieldErrorsAtom,
|
|
@@ -490,8 +546,7 @@ export const make = <TFields extends Field.FieldsRecord, R>(
|
|
|
490
546
|
lastSubmittedValuesAtom,
|
|
491
547
|
changedSinceSubmitFieldsAtom,
|
|
492
548
|
hasChangedSinceSubmitAtom,
|
|
493
|
-
|
|
494
|
-
decodeAndSubmit,
|
|
549
|
+
submitAtom,
|
|
495
550
|
combinedSchema,
|
|
496
551
|
fieldRefs,
|
|
497
552
|
validationAtomsRegistry,
|
|
@@ -500,5 +555,9 @@ export const make = <TFields extends Field.FieldsRecord, R>(
|
|
|
500
555
|
getOrCreateFieldAtoms,
|
|
501
556
|
resetValidationAtoms,
|
|
502
557
|
operations,
|
|
503
|
-
|
|
558
|
+
resetAtom,
|
|
559
|
+
revertToLastSubmitAtom,
|
|
560
|
+
setValuesAtom,
|
|
561
|
+
setValue,
|
|
562
|
+
} as FormAtoms<TFields, R, A, E>
|
|
504
563
|
}
|
package/src/FormBuilder.ts
CHANGED
|
@@ -16,6 +16,15 @@ import type {
|
|
|
16
16
|
} from "./Field.js"
|
|
17
17
|
import { isArrayFieldDef, isFieldDef } from "./Field.js"
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* @since 1.0.0
|
|
21
|
+
* @category Models
|
|
22
|
+
*/
|
|
23
|
+
export interface SubmittedValues<TFields extends FieldsRecord> {
|
|
24
|
+
readonly encoded: EncodedFromFields<TFields>
|
|
25
|
+
readonly decoded: DecodedFromFields<TFields>
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
/**
|
|
20
29
|
* Unique identifier for Field references.
|
|
21
30
|
*
|
|
@@ -84,7 +93,7 @@ export type TypeId = typeof TypeId
|
|
|
84
93
|
export interface FormState<TFields extends FieldsRecord> {
|
|
85
94
|
readonly values: EncodedFromFields<TFields>
|
|
86
95
|
readonly initialValues: EncodedFromFields<TFields>
|
|
87
|
-
readonly lastSubmittedValues: Option.Option<
|
|
96
|
+
readonly lastSubmittedValues: Option.Option<SubmittedValues<TFields>>
|
|
88
97
|
readonly touched: { readonly [K in keyof TFields]: boolean }
|
|
89
98
|
readonly submitCount: number
|
|
90
99
|
readonly dirtyFields: ReadonlySet<string>
|
package/src/index.ts
CHANGED
|
@@ -6,12 +6,10 @@
|
|
|
6
6
|
export * as Field from "./Field.js"
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* This module provides the core atom infrastructure that framework adapters
|
|
12
|
-
* (React, Vue, Svelte, Solid) can use to build reactive form components.
|
|
9
|
+
* Atoms for a single field.
|
|
13
10
|
*
|
|
14
11
|
* @since 1.0.0
|
|
12
|
+
* @category Models
|
|
15
13
|
*/
|
|
16
14
|
export * as FormAtoms from "./FormAtoms.js"
|
|
17
15
|
|
package/src/internal/dirty.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as Equal from "effect/Equal"
|
|
7
7
|
import * as Utils from "effect/Utils"
|
|
8
|
-
import { getNestedValue } from "./path.js"
|
|
8
|
+
import { getNestedValue, isPathUnderRoot } from "./path.js"
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Recalculates dirty fields for an array after mutation.
|
|
@@ -17,20 +17,24 @@ export const recalculateDirtyFieldsForArray = (
|
|
|
17
17
|
arrayPath: string,
|
|
18
18
|
newItems: ReadonlyArray<unknown>,
|
|
19
19
|
): ReadonlySet<string> => {
|
|
20
|
+
const initialItems = (getNestedValue(initialValues, arrayPath) ?? []) as ReadonlyArray<unknown>
|
|
21
|
+
|
|
22
|
+
if (newItems === initialItems) {
|
|
23
|
+
return dirtyFields
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
const nextDirty = new Set(
|
|
21
|
-
Array.from(dirtyFields).filter(
|
|
22
|
-
(path) => path !== arrayPath && !path.startsWith(arrayPath + ".") && !path.startsWith(arrayPath + "["),
|
|
23
|
-
),
|
|
27
|
+
Array.from(dirtyFields).filter((path) => !isPathUnderRoot(path, arrayPath)),
|
|
24
28
|
)
|
|
25
29
|
|
|
26
|
-
const initialItems = (getNestedValue(initialValues, arrayPath) ?? []) as ReadonlyArray<unknown>
|
|
27
|
-
|
|
28
30
|
const loopLength = Math.max(newItems.length, initialItems.length)
|
|
29
31
|
for (let i = 0; i < loopLength; i++) {
|
|
30
32
|
const itemPath = `${arrayPath}[${i}]`
|
|
31
33
|
const newItem = newItems[i]
|
|
32
34
|
const initialItem = initialItems[i]
|
|
33
35
|
|
|
36
|
+
if (newItem === initialItem) continue
|
|
37
|
+
|
|
34
38
|
const isEqual = Utils.structuralRegion(() => Equal.equals(newItem, initialItem))
|
|
35
39
|
if (!isEqual) {
|
|
36
40
|
nextDirty.add(itemPath)
|
|
@@ -58,22 +62,40 @@ export const recalculateDirtySubtree = (
|
|
|
58
62
|
allValues: unknown,
|
|
59
63
|
rootPath: string = "",
|
|
60
64
|
): ReadonlySet<string> => {
|
|
65
|
+
const targetValue = rootPath ? getNestedValue(allValues, rootPath) : allValues
|
|
66
|
+
const targetInitial = rootPath ? getNestedValue(allInitial, rootPath) : allInitial
|
|
67
|
+
|
|
68
|
+
if (targetValue === targetInitial) {
|
|
69
|
+
if (rootPath === "") {
|
|
70
|
+
return new Set()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let changed = false
|
|
74
|
+
const nextDirty = new Set(currentDirty)
|
|
75
|
+
for (const path of currentDirty) {
|
|
76
|
+
if (isPathUnderRoot(path, rootPath)) {
|
|
77
|
+
nextDirty.delete(path)
|
|
78
|
+
changed = true
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return changed ? nextDirty : currentDirty
|
|
82
|
+
}
|
|
83
|
+
|
|
61
84
|
const nextDirty = new Set(currentDirty)
|
|
62
85
|
|
|
63
86
|
if (rootPath === "") {
|
|
64
87
|
nextDirty.clear()
|
|
65
88
|
} else {
|
|
66
89
|
for (const path of nextDirty) {
|
|
67
|
-
if (path
|
|
90
|
+
if (isPathUnderRoot(path, rootPath)) {
|
|
68
91
|
nextDirty.delete(path)
|
|
69
92
|
}
|
|
70
93
|
}
|
|
71
94
|
}
|
|
72
95
|
|
|
73
|
-
const targetValue = rootPath ? getNestedValue(allValues, rootPath) : allValues
|
|
74
|
-
const targetInitial = rootPath ? getNestedValue(allInitial, rootPath) : allInitial
|
|
75
|
-
|
|
76
96
|
const recurse = (current: unknown, initial: unknown, path: string): void => {
|
|
97
|
+
if (current === initial) return
|
|
98
|
+
|
|
77
99
|
if (Array.isArray(current)) {
|
|
78
100
|
const initialArr = (initial ?? []) as ReadonlyArray<unknown>
|
|
79
101
|
for (let i = 0; i < Math.max(current.length, initialArr.length); i++) {
|
|
@@ -81,10 +103,14 @@ export const recalculateDirtySubtree = (
|
|
|
81
103
|
}
|
|
82
104
|
} else if (current !== null && typeof current === "object") {
|
|
83
105
|
const initialObj = (initial ?? {}) as Record<string, unknown>
|
|
84
|
-
const
|
|
85
|
-
for (const key of allKeys) {
|
|
106
|
+
for (const key in current as object) {
|
|
86
107
|
recurse((current as Record<string, unknown>)[key], initialObj[key], path ? `${path}.${key}` : key)
|
|
87
108
|
}
|
|
109
|
+
for (const key in initialObj) {
|
|
110
|
+
if (!(key in (current as object))) {
|
|
111
|
+
recurse(undefined, initialObj[key], path ? `${path}.${key}` : key)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
88
114
|
} else {
|
|
89
115
|
const isEqual = Utils.structuralRegion(() => Equal.equals(current, initial))
|
|
90
116
|
if (!isEqual && path) nextDirty.add(path)
|
package/src/internal/path.ts
CHANGED
|
@@ -27,6 +27,18 @@ export const schemaPathToFieldPath = (path: ReadonlyArray<PropertyKey>): string
|
|
|
27
27
|
return result
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Checks if a path matches a root path or is a descendant of it.
|
|
32
|
+
* Handles both dot notation (root.child) and bracket notation (root[0]).
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* isPathUnderRoot("items[0].name", "items[0]") // true
|
|
36
|
+
* isPathUnderRoot("items[0].name", "items") // true
|
|
37
|
+
* isPathUnderRoot("other", "items") // false
|
|
38
|
+
*/
|
|
39
|
+
export const isPathUnderRoot = (path: string, rootPath: string): boolean =>
|
|
40
|
+
path === rootPath || path.startsWith(rootPath + ".") || path.startsWith(rootPath + "[")
|
|
41
|
+
|
|
30
42
|
/**
|
|
31
43
|
* Checks if a field path or any of its parent paths are in the dirty set.
|
|
32
44
|
*/
|