@lucas-barake/effect-form 0.1.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/Form/package.json +6 -0
- package/FormAtoms/package.json +6 -0
- package/LICENSE +21 -0
- package/Mode/package.json +6 -0
- package/README.md +5 -0
- package/Validation/package.json +6 -0
- package/dist/cjs/Form.js +299 -0
- package/dist/cjs/Form.js.map +1 -0
- package/dist/cjs/FormAtoms.js +266 -0
- package/dist/cjs/FormAtoms.js.map +1 -0
- package/dist/cjs/Mode.js +64 -0
- package/dist/cjs/Mode.js.map +1 -0
- package/dist/cjs/Validation.js +69 -0
- package/dist/cjs/Validation.js.map +1 -0
- package/dist/cjs/index.js +35 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/internal/dirty.js +101 -0
- package/dist/cjs/internal/dirty.js.map +1 -0
- package/dist/cjs/internal/path.js +96 -0
- package/dist/cjs/internal/path.js.map +1 -0
- package/dist/cjs/internal/weak-registry.js +52 -0
- package/dist/cjs/internal/weak-registry.js.map +1 -0
- package/dist/dts/Form.d.ts +317 -0
- package/dist/dts/Form.d.ts.map +1 -0
- package/dist/dts/FormAtoms.d.ts +145 -0
- package/dist/dts/FormAtoms.d.ts.map +1 -0
- package/dist/dts/Mode.d.ts +55 -0
- package/dist/dts/Mode.d.ts.map +1 -0
- package/dist/dts/Validation.d.ts +23 -0
- package/dist/dts/Validation.d.ts.map +1 -0
- package/dist/dts/index.d.ts +26 -0
- package/dist/dts/index.d.ts.map +1 -0
- package/dist/dts/internal/dirty.d.ts +13 -0
- package/dist/dts/internal/dirty.d.ts.map +1 -0
- package/dist/dts/internal/path.d.ts +32 -0
- package/dist/dts/internal/path.d.ts.map +1 -0
- package/dist/dts/internal/weak-registry.d.ts +7 -0
- package/dist/dts/internal/weak-registry.d.ts.map +1 -0
- package/dist/esm/Form.js +263 -0
- package/dist/esm/Form.js.map +1 -0
- package/dist/esm/FormAtoms.js +238 -0
- package/dist/esm/FormAtoms.js.map +1 -0
- package/dist/esm/Mode.js +36 -0
- package/dist/esm/Mode.js.map +1 -0
- package/dist/esm/Validation.js +40 -0
- package/dist/esm/Validation.js.map +1 -0
- package/dist/esm/index.js +26 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/dirty.js +72 -0
- package/dist/esm/internal/dirty.js.map +1 -0
- package/dist/esm/internal/path.js +86 -0
- package/dist/esm/internal/path.js.map +1 -0
- package/dist/esm/internal/weak-registry.js +45 -0
- package/dist/esm/internal/weak-registry.js.map +1 -0
- package/dist/esm/package.json +4 -0
- package/package.json +64 -0
- package/src/Form.ts +522 -0
- package/src/FormAtoms.ts +485 -0
- package/src/Mode.ts +59 -0
- package/src/Validation.ts +43 -0
- package/src/index.ts +28 -0
- package/src/internal/dirty.ts +96 -0
- package/src/internal/path.ts +93 -0
- package/src/internal/weak-registry.ts +60 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal dirty tracking algorithms.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
import * as Equal from "effect/Equal"
|
|
7
|
+
import * as Utils from "effect/Utils"
|
|
8
|
+
import { getNestedValue } from "./path.js"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Recalculates dirty fields for an array after mutation.
|
|
12
|
+
* Clears all paths under the array and re-evaluates each item.
|
|
13
|
+
*/
|
|
14
|
+
export const recalculateDirtyFieldsForArray = (
|
|
15
|
+
dirtyFields: ReadonlySet<string>,
|
|
16
|
+
initialValues: unknown,
|
|
17
|
+
arrayPath: string,
|
|
18
|
+
newItems: ReadonlyArray<unknown>,
|
|
19
|
+
): ReadonlySet<string> => {
|
|
20
|
+
const nextDirty = new Set(
|
|
21
|
+
Array.from(dirtyFields).filter(
|
|
22
|
+
(path) => path !== arrayPath && !path.startsWith(arrayPath + ".") && !path.startsWith(arrayPath + "["),
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const initialItems = (getNestedValue(initialValues, arrayPath) ?? []) as ReadonlyArray<unknown>
|
|
27
|
+
|
|
28
|
+
const loopLength = Math.max(newItems.length, initialItems.length)
|
|
29
|
+
for (let i = 0; i < loopLength; i++) {
|
|
30
|
+
const itemPath = `${arrayPath}[${i}]`
|
|
31
|
+
const newItem = newItems[i]
|
|
32
|
+
const initialItem = initialItems[i]
|
|
33
|
+
|
|
34
|
+
const isEqual = Utils.structuralRegion(() => Equal.equals(newItem, initialItem))
|
|
35
|
+
if (!isEqual) {
|
|
36
|
+
nextDirty.add(itemPath)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (newItems.length !== initialItems.length) {
|
|
41
|
+
nextDirty.add(arrayPath)
|
|
42
|
+
} else {
|
|
43
|
+
nextDirty.delete(arrayPath)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return nextDirty
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Recalculates dirty fields for a subtree after value change.
|
|
51
|
+
* Clears the rootPath and all children, then re-evaluates recursively.
|
|
52
|
+
*
|
|
53
|
+
* @param rootPath - Empty string for full form, or a specific path for targeted update
|
|
54
|
+
*/
|
|
55
|
+
export const recalculateDirtySubtree = (
|
|
56
|
+
currentDirty: ReadonlySet<string>,
|
|
57
|
+
allInitial: unknown,
|
|
58
|
+
allValues: unknown,
|
|
59
|
+
rootPath: string = "",
|
|
60
|
+
): ReadonlySet<string> => {
|
|
61
|
+
const nextDirty = new Set(currentDirty)
|
|
62
|
+
|
|
63
|
+
if (rootPath === "") {
|
|
64
|
+
nextDirty.clear()
|
|
65
|
+
} else {
|
|
66
|
+
for (const path of nextDirty) {
|
|
67
|
+
if (path === rootPath || path.startsWith(rootPath + ".") || path.startsWith(rootPath + "[")) {
|
|
68
|
+
nextDirty.delete(path)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const targetValue = rootPath ? getNestedValue(allValues, rootPath) : allValues
|
|
74
|
+
const targetInitial = rootPath ? getNestedValue(allInitial, rootPath) : allInitial
|
|
75
|
+
|
|
76
|
+
const recurse = (current: unknown, initial: unknown, path: string): void => {
|
|
77
|
+
if (Array.isArray(current)) {
|
|
78
|
+
const initialArr = (initial ?? []) as ReadonlyArray<unknown>
|
|
79
|
+
for (let i = 0; i < Math.max(current.length, initialArr.length); i++) {
|
|
80
|
+
recurse(current[i], initialArr[i], path ? `${path}[${i}]` : `[${i}]`)
|
|
81
|
+
}
|
|
82
|
+
} else if (current !== null && typeof current === "object") {
|
|
83
|
+
const initialObj = (initial ?? {}) as Record<string, unknown>
|
|
84
|
+
const allKeys = new Set([...Object.keys(current as object), ...Object.keys(initialObj)])
|
|
85
|
+
for (const key of allKeys) {
|
|
86
|
+
recurse((current as Record<string, unknown>)[key], initialObj[key], path ? `${path}.${key}` : key)
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
const isEqual = Utils.structuralRegion(() => Equal.equals(current, initial))
|
|
90
|
+
if (!isEqual && path) nextDirty.add(path)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
recurse(targetValue, targetInitial, rootPath)
|
|
95
|
+
return nextDirty
|
|
96
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal path utilities for form field operations.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const BRACKET_NOTATION_REGEX = /\[(\d+)\]/g
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Converts a schema path array to a dot/bracket notation string.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* schemaPathToFieldPath(["items", 0, "name"]) // "items[0].name"
|
|
14
|
+
*/
|
|
15
|
+
export const schemaPathToFieldPath = (path: ReadonlyArray<PropertyKey>): string => {
|
|
16
|
+
if (path.length === 0) return ""
|
|
17
|
+
|
|
18
|
+
let result = String(path[0])
|
|
19
|
+
for (let i = 1; i < path.length; i++) {
|
|
20
|
+
const segment = path[i]
|
|
21
|
+
if (typeof segment === "number") {
|
|
22
|
+
result += `[${segment}]`
|
|
23
|
+
} else {
|
|
24
|
+
result += `.${String(segment)}`
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return result
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Checks if a field path or any of its parent paths are in the dirty set.
|
|
32
|
+
*/
|
|
33
|
+
export const isPathOrParentDirty = (dirtyFields: ReadonlySet<string>, path: string): boolean => {
|
|
34
|
+
if (dirtyFields.has(path)) return true
|
|
35
|
+
|
|
36
|
+
let parent = path
|
|
37
|
+
while (true) {
|
|
38
|
+
const lastDot = parent.lastIndexOf(".")
|
|
39
|
+
const lastBracket = parent.lastIndexOf("[")
|
|
40
|
+
const splitIndex = Math.max(lastDot, lastBracket)
|
|
41
|
+
|
|
42
|
+
if (splitIndex === -1) break
|
|
43
|
+
|
|
44
|
+
parent = parent.substring(0, splitIndex)
|
|
45
|
+
if (dirtyFields.has(parent)) return true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Gets a nested value from an object using dot/bracket notation path.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* getNestedValue({ items: [{ name: "A" }] }, "items[0].name") // "A"
|
|
56
|
+
*/
|
|
57
|
+
export const getNestedValue = (obj: unknown, path: string): unknown => {
|
|
58
|
+
if (path === "") return obj
|
|
59
|
+
const parts = path.replace(BRACKET_NOTATION_REGEX, ".$1").split(".")
|
|
60
|
+
let current: unknown = obj
|
|
61
|
+
for (const part of parts) {
|
|
62
|
+
if (current == null || typeof current !== "object") return undefined
|
|
63
|
+
current = (current as Record<string, unknown>)[part]
|
|
64
|
+
}
|
|
65
|
+
return current
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Sets a nested value in an object immutably using dot/bracket notation path.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* setNestedValue({ items: [{ name: "A" }] }, "items[0].name", "B")
|
|
73
|
+
* // { items: [{ name: "B" }] }
|
|
74
|
+
*/
|
|
75
|
+
export const setNestedValue = <T>(obj: T, path: string, value: unknown): T => {
|
|
76
|
+
if (path === "") return value as T
|
|
77
|
+
const parts = path.replace(BRACKET_NOTATION_REGEX, ".$1").split(".")
|
|
78
|
+
const result = { ...obj } as Record<string, unknown>
|
|
79
|
+
|
|
80
|
+
let current = result
|
|
81
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
82
|
+
const part = parts[i]
|
|
83
|
+
if (Array.isArray(current[part])) {
|
|
84
|
+
current[part] = [...(current[part] as Array<unknown>)]
|
|
85
|
+
} else {
|
|
86
|
+
current[part] = { ...(current[part] as Record<string, unknown>) }
|
|
87
|
+
}
|
|
88
|
+
current = current[part] as Record<string, unknown>
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
current[parts[parts.length - 1]] = value
|
|
92
|
+
return result as T
|
|
93
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal WeakRef-based registry for caching atoms.
|
|
3
|
+
*
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A registry that uses WeakRef to allow garbage collection of cached values.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export interface WeakRegistry<V extends object> {
|
|
13
|
+
readonly get: (key: string) => V | undefined
|
|
14
|
+
readonly set: (key: string, value: V) => void
|
|
15
|
+
readonly delete: (key: string) => boolean
|
|
16
|
+
readonly clear: () => void
|
|
17
|
+
readonly values: () => IterableIterator<V>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates a WeakRef-based registry with automatic cleanup via FinalizationRegistry.
|
|
22
|
+
* Falls back to a regular Map in environments without WeakRef support.
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
export const createWeakRegistry = <V extends object>(): WeakRegistry<V> => {
|
|
27
|
+
if (typeof WeakRef === "undefined" || typeof FinalizationRegistry === "undefined") {
|
|
28
|
+
const map = new Map<string, V>()
|
|
29
|
+
return {
|
|
30
|
+
get: (key) => map.get(key),
|
|
31
|
+
set: (key, value) => {
|
|
32
|
+
map.set(key, value)
|
|
33
|
+
},
|
|
34
|
+
delete: (key) => map.delete(key),
|
|
35
|
+
clear: () => map.clear(),
|
|
36
|
+
values: () => map.values(),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const map = new Map<string, WeakRef<V>>()
|
|
41
|
+
const registry = new FinalizationRegistry<string>((key) => {
|
|
42
|
+
map.delete(key)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
get: (key) => map.get(key)?.deref(),
|
|
47
|
+
set: (key, value) => {
|
|
48
|
+
map.set(key, new WeakRef(value))
|
|
49
|
+
registry.register(value, key)
|
|
50
|
+
},
|
|
51
|
+
delete: (key) => map.delete(key),
|
|
52
|
+
clear: () => map.clear(),
|
|
53
|
+
*values() {
|
|
54
|
+
for (const ref of map.values()) {
|
|
55
|
+
const value = ref.deref()
|
|
56
|
+
if (value !== undefined) yield value
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
}
|