@octanejs/formisch 0.0.1

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.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/UPSTREAM.md +95 -0
  4. package/package.json +65 -0
  5. package/src/components/Field/Field.tsrx +22 -0
  6. package/src/components/Field/Field.tsrx.d.ts +17 -0
  7. package/src/components/Field/index.ts +1 -0
  8. package/src/components/FieldArray/FieldArray.tsrx +22 -0
  9. package/src/components/FieldArray/FieldArray.tsrx.d.ts +18 -0
  10. package/src/components/FieldArray/index.ts +1 -0
  11. package/src/components/Form/Form.tsrx +28 -0
  12. package/src/components/Form/Form.tsrx.d.ts +15 -0
  13. package/src/components/Form/index.ts +1 -0
  14. package/src/components/index.ts +3 -0
  15. package/src/core/array/copyItemState/copyItemState.ts +101 -0
  16. package/src/core/array/copyItemState/index.ts +1 -0
  17. package/src/core/array/index.ts +3 -0
  18. package/src/core/array/resetItemState/index.ts +1 -0
  19. package/src/core/array/resetItemState/resetItemState.ts +172 -0
  20. package/src/core/array/swapItemState/index.ts +1 -0
  21. package/src/core/array/swapItemState/swapItemState.ts +138 -0
  22. package/src/core/field/focusFieldElement/focusFieldElement.ts +32 -0
  23. package/src/core/field/focusFieldElement/index.ts +1 -0
  24. package/src/core/field/getDirtyFieldInput/getDirtyFieldInput.ts +66 -0
  25. package/src/core/field/getDirtyFieldInput/index.ts +1 -0
  26. package/src/core/field/getElementInput/getElementInput.ts +78 -0
  27. package/src/core/field/getElementInput/index.ts +1 -0
  28. package/src/core/field/getFieldBool/getFieldBool.ts +22 -0
  29. package/src/core/field/getFieldBool/index.ts +1 -0
  30. package/src/core/field/getFieldInput/getFieldInput.ts +52 -0
  31. package/src/core/field/getFieldInput/index.ts +1 -0
  32. package/src/core/field/getFieldStore/getFieldStore.ts +34 -0
  33. package/src/core/field/getFieldStore/index.ts +1 -0
  34. package/src/core/field/index.ts +11 -0
  35. package/src/core/field/initializeFieldStore/index.ts +1 -0
  36. package/src/core/field/initializeFieldStore/initializeFieldStore.ts +325 -0
  37. package/src/core/field/setFieldBool/index.ts +1 -0
  38. package/src/core/field/setFieldBool/setFieldBool.ts +29 -0
  39. package/src/core/field/setFieldInput/index.ts +1 -0
  40. package/src/core/field/setFieldInput/setFieldInput.ts +180 -0
  41. package/src/core/field/setInitialFieldInput/index.ts +1 -0
  42. package/src/core/field/setInitialFieldInput/setInitialFieldInput.ts +99 -0
  43. package/src/core/field/walkFieldStore/index.ts +1 -0
  44. package/src/core/field/walkFieldStore/walkFieldStore.ts +49 -0
  45. package/src/core/form/createFormStore/createFormStore.ts +56 -0
  46. package/src/core/form/createFormStore/index.ts +1 -0
  47. package/src/core/form/decodeFormData/decodeFormData.ts +436 -0
  48. package/src/core/form/decodeFormData/index.ts +1 -0
  49. package/src/core/form/index.ts +4 -0
  50. package/src/core/form/validateFormInput/index.ts +1 -0
  51. package/src/core/form/validateFormInput/validateFormInput.ts +138 -0
  52. package/src/core/form/validateIfRequired/index.ts +1 -0
  53. package/src/core/form/validateIfRequired/validateIfRequired.ts +31 -0
  54. package/src/core/framework/index.ts +80 -0
  55. package/src/core/index.ts +6 -0
  56. package/src/core/types/field/field.ts +201 -0
  57. package/src/core/types/field/index.ts +1 -0
  58. package/src/core/types/form/form.ts +140 -0
  59. package/src/core/types/form/index.ts +1 -0
  60. package/src/core/types/index.ts +6 -0
  61. package/src/core/types/path/index.ts +10 -0
  62. package/src/core/types/path/path.ts +301 -0
  63. package/src/core/types/schema/index.ts +1 -0
  64. package/src/core/types/schema/schema.ts +18 -0
  65. package/src/core/types/signal/index.ts +1 -0
  66. package/src/core/types/signal/signal.ts +23 -0
  67. package/src/core/types/utils/index.ts +1 -0
  68. package/src/core/types/utils/utils.ts +46 -0
  69. package/src/core/values.ts +4 -0
  70. package/src/hooks/index.ts +3 -0
  71. package/src/hooks/useField/index.ts +1 -0
  72. package/src/hooks/useField/useField.ts +114 -0
  73. package/src/hooks/useFieldArray/index.ts +1 -0
  74. package/src/hooks/useFieldArray/useFieldArray.ts +63 -0
  75. package/src/hooks/useForm/index.ts +1 -0
  76. package/src/hooks/useForm/useForm.ts +68 -0
  77. package/src/hooks/useSignals/index.ts +1 -0
  78. package/src/hooks/useSignals/useSignals.ts +31 -0
  79. package/src/index.ts +19 -0
  80. package/src/internal.ts +32 -0
  81. package/src/methods/focus/focus.ts +35 -0
  82. package/src/methods/focus/index.ts +1 -0
  83. package/src/methods/getDeepErrorEntries/getDeepErrorEntries.ts +108 -0
  84. package/src/methods/getDeepErrorEntries/index.ts +1 -0
  85. package/src/methods/getDeepErrors/getDeepErrors.ts +90 -0
  86. package/src/methods/getDeepErrors/index.ts +1 -0
  87. package/src/methods/getDirtyInput/getDirtyInput.ts +87 -0
  88. package/src/methods/getDirtyInput/index.ts +1 -0
  89. package/src/methods/getDirtyPaths/getDirtyPaths.ts +123 -0
  90. package/src/methods/getDirtyPaths/index.ts +1 -0
  91. package/src/methods/getErrors/getErrors.ts +70 -0
  92. package/src/methods/getErrors/index.ts +1 -0
  93. package/src/methods/getInput/getInput.ts +75 -0
  94. package/src/methods/getInput/index.ts +1 -0
  95. package/src/methods/handleSubmit/handleSubmit.ts +83 -0
  96. package/src/methods/handleSubmit/index.ts +1 -0
  97. package/src/methods/index.ts +23 -0
  98. package/src/methods/insert/index.ts +1 -0
  99. package/src/methods/insert/insert.ts +134 -0
  100. package/src/methods/isDirty/index.ts +1 -0
  101. package/src/methods/isDirty/isDirty.ts +72 -0
  102. package/src/methods/isEdited/index.ts +1 -0
  103. package/src/methods/isEdited/isEdited.ts +72 -0
  104. package/src/methods/isTouched/index.ts +1 -0
  105. package/src/methods/isTouched/isTouched.ts +72 -0
  106. package/src/methods/isValid/index.ts +1 -0
  107. package/src/methods/isValid/isValid.ts +74 -0
  108. package/src/methods/move/index.ts +1 -0
  109. package/src/methods/move/move.ts +124 -0
  110. package/src/methods/pickDirty/index.ts +1 -0
  111. package/src/methods/pickDirty/pickDirty.ts +87 -0
  112. package/src/methods/remove/index.ts +1 -0
  113. package/src/methods/remove/remove.ts +76 -0
  114. package/src/methods/replace/index.ts +1 -0
  115. package/src/methods/replace/replace.ts +80 -0
  116. package/src/methods/reset/index.ts +1 -0
  117. package/src/methods/reset/reset.ts +216 -0
  118. package/src/methods/setErrors/index.ts +1 -0
  119. package/src/methods/setErrors/setErrors.ts +63 -0
  120. package/src/methods/setInput/index.ts +1 -0
  121. package/src/methods/setInput/setInput.ts +87 -0
  122. package/src/methods/submit/index.ts +1 -0
  123. package/src/methods/submit/submit.ts +11 -0
  124. package/src/methods/swap/index.ts +1 -0
  125. package/src/methods/swap/swap.ts +85 -0
  126. package/src/methods/validate/index.ts +1 -0
  127. package/src/methods/validate/validate.ts +34 -0
  128. package/src/types/field.ts +48 -0
  129. package/src/types/form.ts +12 -0
  130. package/src/types/index.ts +2 -0
@@ -0,0 +1,172 @@
1
+ import { initializeFieldStore } from '../../field/initializeFieldStore/index.ts';
2
+ import { batch, createId } from '../../framework/index.ts';
3
+ import type {
4
+ EmptyInput,
5
+ FieldElement,
6
+ InternalFieldStore,
7
+ InternalFormStore,
8
+ } from '../../types/index.ts';
9
+
10
+ /**
11
+ * Resets the state of a field store (signal values) deeply nested. Sets
12
+ * `elements` to empty array, `errors` to `null`, `isTouched`, `isEdited` and
13
+ * `isDirty` to `false`, and `startInput`, `input`, `startItems`, and `items` to
14
+ * the new input value. Keeps the `initialInput` and `initialItems` state
15
+ * unchanged for form reset functionality.
16
+ *
17
+ * @param internalFormStore The form store providing the empty input config.
18
+ * @param internalFieldStore The field store to reset.
19
+ * @param input The new input value (can be any type including array or object).
20
+ * @param keepStart Whether to keep `startInput` and `startItems` as the dirty
21
+ * baseline instead of resetting them to the new input. Used when a field store
22
+ * is reused for an in-place edit so its dirty state is detected correctly.
23
+ */
24
+ export function resetItemState(
25
+ internalFormStore: InternalFormStore,
26
+ internalFieldStore: InternalFieldStore,
27
+ input: unknown,
28
+ keepStart = false,
29
+ ): void {
30
+ // Batch all state updates for optimal reactivity performance
31
+ batch(() => {
32
+ // Clear elements array, keeping `initialElements` in sync while the store
33
+ // still owns it (not moved by a reorder) so a later `reset` restores the
34
+ // live element once the field remounts
35
+ const elements: FieldElement[] = [];
36
+ if (internalFieldStore.elements === internalFieldStore.initialElements) {
37
+ internalFieldStore.initialElements = elements;
38
+ }
39
+ internalFieldStore.elements = elements;
40
+
41
+ // Clear errors
42
+ internalFieldStore.errors.value = null;
43
+
44
+ // Reset touched to false
45
+ internalFieldStore.isTouched.value = false;
46
+
47
+ // Reset edited to false
48
+ internalFieldStore.isEdited.value = false;
49
+
50
+ // Reset dirty to false
51
+ internalFieldStore.isDirty.value = false;
52
+
53
+ // If field store is array or object, handle complex type reset
54
+ if (internalFieldStore.kind === 'array' || internalFieldStore.kind === 'object') {
55
+ // For arrays and objects, input is null/undefined or true (not actual
56
+ // value). Mirror `initializeFieldStore` so a missing input on a
57
+ // non-nullish array or object becomes a present empty container (`true`)
58
+ // instead of `undefined`, keeping reset consistent with the initial state.
59
+ const objectInput = internalFieldStore.isNullish && input == null ? input : true;
60
+
61
+ // Set start input unless it is kept as the dirty baseline
62
+ if (!keepStart) {
63
+ internalFieldStore.startInput.value = objectInput;
64
+ }
65
+
66
+ // Set current input
67
+ internalFieldStore.input.value = objectInput;
68
+
69
+ // If field store is array, handle array-specific reset
70
+ if (internalFieldStore.kind === 'array') {
71
+ // Tuples have a fixed number of children that the schema cannot
72
+ // recreate (no `item`), so they keep them even when the input is
73
+ // nullish, just like `initializeFieldStore`
74
+ const isTuple = internalFieldStore.schema.type !== 'array';
75
+
76
+ // If input is provided or store is a tuple, (re)create items with IDs
77
+ if (input || isTuple) {
78
+ // Dynamic arrays grow to the input length, while tuples keep their
79
+ // fixed number of children
80
+ const length = isTuple ? internalFieldStore.children.length : (input as unknown[]).length;
81
+
82
+ // Create new items array with unique IDs for each item
83
+ const newItems = Array.from({ length }, createId);
84
+
85
+ // Set start items unless they are kept as the dirty baseline
86
+ if (!keepStart) {
87
+ internalFieldStore.startItems.value = newItems;
88
+ }
89
+
90
+ // Set current items
91
+ internalFieldStore.items.value = newItems;
92
+
93
+ // Reset state for each array item
94
+ for (let index = 0; index < length; index++) {
95
+ // A tuple reset without input (or with nullish input) resets each
96
+ // child to undefined, mirroring `initializeFieldStore`
97
+ const itemInput = (input as unknown[] | null | undefined)?.[index];
98
+
99
+ // If child exists at this index, reset its state
100
+ if (internalFieldStore.children[index]) {
101
+ // Recursively reset child with corresponding input
102
+ resetItemState(
103
+ internalFormStore,
104
+ internalFieldStore.children[index],
105
+ itemInput,
106
+ keepStart,
107
+ );
108
+
109
+ // Otherwise, initialize a new child with the corresponding input
110
+ } else {
111
+ // Create empty child object
112
+ // @ts-expect-error
113
+ internalFieldStore.children[index] = {};
114
+
115
+ // Initialize field store for new child
116
+ initializeFieldStore(
117
+ internalFormStore,
118
+ internalFieldStore.children[index],
119
+ // @ts-expect-error
120
+ internalFieldStore.schema.item,
121
+ itemInput,
122
+ [...internalFieldStore.path, index],
123
+ );
124
+ }
125
+ }
126
+
127
+ // Otherwise, clear items arrays
128
+ } else {
129
+ // Set start items to empty array unless kept as the dirty baseline
130
+ if (!keepStart) {
131
+ internalFieldStore.startItems.value = [];
132
+ }
133
+
134
+ // Set current items to empty array
135
+ internalFieldStore.items.value = [];
136
+ }
137
+
138
+ // Otherwise, if field store is object, handle object-specific reset
139
+ } else {
140
+ // Reset state for each object property
141
+ for (const key in internalFieldStore.children) {
142
+ // Recursively reset child with corresponding input
143
+ resetItemState(
144
+ internalFormStore,
145
+ internalFieldStore.children[key],
146
+ // @ts-expect-error
147
+ input?.[key],
148
+ keepStart,
149
+ );
150
+ }
151
+ }
152
+
153
+ // Otherwise, if field store is value, handle primitive type reset
154
+ } else {
155
+ // Fall back to the empty input for this field's type when no input is
156
+ // provided so the reset value stays consistent with the initial input.
157
+ // Optional and nullable fields stay `undefined` as they accept it.
158
+ const valueInput =
159
+ input === undefined && !internalFieldStore.isNullish
160
+ ? internalFormStore.emptyInput[internalFieldStore.schema.type as keyof EmptyInput]
161
+ : input;
162
+
163
+ // Set start input unless it is kept as the dirty baseline
164
+ if (!keepStart) {
165
+ internalFieldStore.startInput.value = valueInput;
166
+ }
167
+
168
+ // Set current input
169
+ internalFieldStore.input.value = valueInput;
170
+ }
171
+ });
172
+ }
@@ -0,0 +1 @@
1
+ export * from './swapItemState.ts';
@@ -0,0 +1,138 @@
1
+ import { initializeFieldStore } from '../../field/initializeFieldStore/index.ts';
2
+ import { batch, untrack } from '../../framework/index.ts';
3
+ import type { InternalFieldStore, InternalFormStore } from '../../types/index.ts';
4
+
5
+ /**
6
+ * Swaps the deeply nested state (signal values) between two field stores. This
7
+ * includes the `elements`, `errors`, `startInput`, `input`, `isTouched`,
8
+ * `isEdited`, `isDirty`, and for arrays `startItems` and `items` properties.
9
+ * Recursively walks through the field stores and swaps all signal values.
10
+ *
11
+ * @param internalFormStore The form store providing the empty input config.
12
+ * @param firstInternalFieldStore The first field store to swap.
13
+ * @param secondInternalFieldStore The second field store to swap.
14
+ */
15
+ export function swapItemState(
16
+ internalFormStore: InternalFormStore,
17
+ firstInternalFieldStore: InternalFieldStore,
18
+ secondInternalFieldStore: InternalFieldStore,
19
+ ): void {
20
+ // Batch all state updates for optimal reactivity performance
21
+ batch(() => {
22
+ // Untrack to avoid creating reactive dependencies during swap operation
23
+ untrack(() => {
24
+ // Swap elements references
25
+ const tempElements = firstInternalFieldStore.elements;
26
+ firstInternalFieldStore.elements = secondInternalFieldStore.elements;
27
+ secondInternalFieldStore.elements = tempElements;
28
+
29
+ // Swap errors
30
+ const tempErrors = firstInternalFieldStore.errors.value;
31
+ firstInternalFieldStore.errors.value = secondInternalFieldStore.errors.value;
32
+ secondInternalFieldStore.errors.value = tempErrors;
33
+
34
+ // Swap start input
35
+ const tempStartInput = firstInternalFieldStore.startInput.value;
36
+ firstInternalFieldStore.startInput.value = secondInternalFieldStore.startInput.value;
37
+ secondInternalFieldStore.startInput.value = tempStartInput;
38
+
39
+ // Swap current input
40
+ const tempInput = firstInternalFieldStore.input.value;
41
+ firstInternalFieldStore.input.value = secondInternalFieldStore.input.value;
42
+ secondInternalFieldStore.input.value = tempInput;
43
+
44
+ // Swap touched state
45
+ const tempIsTouched = firstInternalFieldStore.isTouched.value;
46
+ firstInternalFieldStore.isTouched.value = secondInternalFieldStore.isTouched.value;
47
+ secondInternalFieldStore.isTouched.value = tempIsTouched;
48
+
49
+ // Swap edited state
50
+ const tempIsEdited = firstInternalFieldStore.isEdited.value;
51
+ firstInternalFieldStore.isEdited.value = secondInternalFieldStore.isEdited.value;
52
+ secondInternalFieldStore.isEdited.value = tempIsEdited;
53
+
54
+ // Swap dirty state
55
+ const tempIsDirty = firstInternalFieldStore.isDirty.value;
56
+ firstInternalFieldStore.isDirty.value = secondInternalFieldStore.isDirty.value;
57
+ secondInternalFieldStore.isDirty.value = tempIsDirty;
58
+
59
+ // If both stores are arrays, swap array-specific state
60
+ if (firstInternalFieldStore.kind === 'array' && secondInternalFieldStore.kind === 'array') {
61
+ // Get current items arrays for later use
62
+ const firstItems = firstInternalFieldStore.items.value;
63
+ const secondItems = secondInternalFieldStore.items.value;
64
+
65
+ // Swap start items
66
+ const tempStartItems = firstInternalFieldStore.startItems.value;
67
+ firstInternalFieldStore.startItems.value = secondInternalFieldStore.startItems.value;
68
+ secondInternalFieldStore.startItems.value = tempStartItems;
69
+
70
+ // Swap current items
71
+ firstInternalFieldStore.items.value = secondItems;
72
+ secondInternalFieldStore.items.value = firstItems;
73
+
74
+ // Calculate maximum length to ensure all children are swapped
75
+ const maxLength = Math.max(firstItems.length, secondItems.length);
76
+
77
+ // Swap state for each array item
78
+ for (let index = 0; index < maxLength; index++) {
79
+ // If first store child doesn't exist, initialize it
80
+ if (!firstInternalFieldStore.children[index]) {
81
+ // Create empty child object
82
+ // @ts-expect-error
83
+ firstInternalFieldStore.children[index] = {};
84
+
85
+ // Initialize field store for new child
86
+ initializeFieldStore(
87
+ internalFormStore,
88
+ firstInternalFieldStore.children[index],
89
+ // @ts-expect-error
90
+ firstInternalFieldStore.schema.item,
91
+ undefined,
92
+ [...firstInternalFieldStore.path, index],
93
+ );
94
+ }
95
+
96
+ // If second store child doesn't exist, initialize it
97
+ if (!secondInternalFieldStore.children[index]) {
98
+ // Create empty child object
99
+ // @ts-expect-error
100
+ secondInternalFieldStore.children[index] = {};
101
+
102
+ // Initialize field store for new child
103
+ initializeFieldStore(
104
+ internalFormStore,
105
+ secondInternalFieldStore.children[index],
106
+ // @ts-expect-error
107
+ secondInternalFieldStore.schema.item,
108
+ undefined,
109
+ [...secondInternalFieldStore.path, index],
110
+ );
111
+ }
112
+
113
+ // Recursively swap children
114
+ swapItemState(
115
+ internalFormStore,
116
+ firstInternalFieldStore.children[index],
117
+ secondInternalFieldStore.children[index],
118
+ );
119
+ }
120
+
121
+ // Otherwise, if both stores are objects, swap object children
122
+ } else if (
123
+ firstInternalFieldStore.kind === 'object' &&
124
+ secondInternalFieldStore.kind === 'object'
125
+ ) {
126
+ // Swap state for each object property
127
+ for (const key in firstInternalFieldStore.children) {
128
+ // Recursively swap children
129
+ swapItemState(
130
+ internalFormStore,
131
+ firstInternalFieldStore.children[key],
132
+ secondInternalFieldStore.children[key],
133
+ );
134
+ }
135
+ }
136
+ });
137
+ });
138
+ }
@@ -0,0 +1,32 @@
1
+ import type { InternalFieldStore } from '../../types/index.ts';
2
+
3
+ /**
4
+ * Focuses the first focusable element of a field store. The elements are tried
5
+ * in order and the first one that actually receives focus wins, so detached,
6
+ * disabled or hidden elements are skipped. The browser decides focusability,
7
+ * which is read back via the element's root `activeElement` so elements in a
8
+ * shadow root or another document are handled correctly.
9
+ *
10
+ * Hint: A `display: none` or `hidden` element is correctly skipped in real
11
+ * browsers, but jsdom has no layout and focuses it anyway, so that case cannot
12
+ * be covered by unit tests.
13
+ *
14
+ * @param internalFieldStore The field store to focus.
15
+ *
16
+ * @returns Whether an element was focused.
17
+ */
18
+ export function focusFieldElement(internalFieldStore: InternalFieldStore): boolean {
19
+ // Try to focus each element and stop at the first that actually receives
20
+ // focus, so the focus is not consumed by an element that cannot be focused
21
+ for (const element of internalFieldStore.elements) {
22
+ element.focus();
23
+ // Read focus back from the element's own root (shadow root or document)
24
+ // so elements in a shadow DOM or another document are handled correctly
25
+ if ((element.getRootNode() as Document | ShadowRoot).activeElement === element) {
26
+ return true;
27
+ }
28
+ }
29
+
30
+ // Otherwise, no element could be focused
31
+ return false;
32
+ }
@@ -0,0 +1 @@
1
+ export * from './focusFieldElement.ts';
@@ -0,0 +1,66 @@
1
+ import type { InternalFieldStore } from '../../types/index.ts';
2
+ import { getFieldBool } from '../getFieldBool/getFieldBool.ts';
3
+
4
+ /**
5
+ * Returns only the dirty input of the field store. Arrays are treated as
6
+ * atomic and returned in full if any item is dirty, while object keys without
7
+ * a dirty descendant are omitted. Returns `undefined` if no descendant is
8
+ * dirty.
9
+ *
10
+ * @param internalFieldStore The field store to get dirty input from.
11
+ * @param dirtyOnly Whether to only include dirty fields. Defaults to `true`.
12
+ *
13
+ * @returns The dirty input, or `undefined` if no descendant is dirty.
14
+ */
15
+ // @__NO_SIDE_EFFECTS__
16
+ export function getDirtyFieldInput(
17
+ internalFieldStore: InternalFieldStore,
18
+ dirtyOnly: boolean = true,
19
+ ): unknown {
20
+ // If field has no dirty descendant, return undefined
21
+ if (dirtyOnly && !getFieldBool(internalFieldStore, 'isDirty')) {
22
+ return undefined;
23
+ }
24
+
25
+ // If field store is array, collect input from children
26
+ if (internalFieldStore.kind === 'array') {
27
+ // If array input is not nullish, build full array from children
28
+ if (internalFieldStore.input.value) {
29
+ // Create output array
30
+ const value = [];
31
+
32
+ // Collect input from each array item
33
+ for (let index = 0; index < internalFieldStore.items.value.length; index++) {
34
+ value[index] = getDirtyFieldInput(internalFieldStore.children[index], false);
35
+ }
36
+ return value;
37
+ }
38
+
39
+ // Otherwise, return nullish input as-is
40
+ return internalFieldStore.input.value;
41
+ }
42
+
43
+ // If field store is object, recurse only into dirty children
44
+ if (internalFieldStore.kind === 'object') {
45
+ // If object input is not nullish, build object from children
46
+ if (internalFieldStore.input.value) {
47
+ // Create output object
48
+ const value: Record<string, unknown> = {};
49
+
50
+ // Collect input from each dirty object property
51
+ for (const key in internalFieldStore.children) {
52
+ const child = internalFieldStore.children[key];
53
+ if (!dirtyOnly || getFieldBool(child, 'isDirty')) {
54
+ value[key] = getDirtyFieldInput(child, dirtyOnly);
55
+ }
56
+ }
57
+ return value;
58
+ }
59
+
60
+ // Otherwise, return nullish input as-is
61
+ return internalFieldStore.input.value;
62
+ }
63
+
64
+ // Return primitive value input
65
+ return internalFieldStore.input.value;
66
+ }
@@ -0,0 +1 @@
1
+ export * from './getDirtyFieldInput.ts';
@@ -0,0 +1,78 @@
1
+ import { untrack } from '../../framework/index.ts';
2
+ import type { FieldElement, InternalFieldStore } from '../../types/index.ts';
3
+ import { getFieldInput } from '../getFieldInput/index.ts';
4
+
5
+ /**
6
+ * Returns the current input of the element. Handles special cases for select
7
+ * multiple, checkbox groups, radio groups, and file inputs.
8
+ *
9
+ * @param element The field element.
10
+ * @param internalFieldStore The internal field store.
11
+ *
12
+ * @returns The element input.
13
+ */
14
+ // @__NO_SIDE_EFFECTS__
15
+ export function getElementInput(
16
+ element: FieldElement,
17
+ internalFieldStore: InternalFieldStore,
18
+ ): unknown {
19
+ // If element is select with multiple option, return selected values
20
+ // @ts-expect-error
21
+ if (element.options && element.multiple) {
22
+ // @ts-expect-error
23
+ return [...element.options]
24
+ .filter((option) => option.selected && !option.disabled)
25
+ .map((option) => option.value);
26
+ }
27
+
28
+ // If element is checkbox, handle single or group
29
+ if (element.type === 'checkbox') {
30
+ // Get all checkboxes with same name
31
+ const options = document.getElementsByName(element.name);
32
+
33
+ // If checkbox group, return array of checked values
34
+ if (options.length > 1) {
35
+ return [...options]
36
+ .filter(
37
+ (option): option is HTMLInputElement =>
38
+ option instanceof HTMLInputElement && option.checked,
39
+ )
40
+ .map((option) => option.value);
41
+ }
42
+
43
+ // Return single checkbox checked state
44
+ // @ts-expect-error
45
+ return element.checked;
46
+ }
47
+
48
+ // If element is radio, handle as string
49
+ if (element.type === 'radio') {
50
+ // If radio is checked, return its value
51
+ // @ts-expect-error
52
+ if (element.checked) {
53
+ return element.value;
54
+ }
55
+
56
+ // Otherwise, return previous value
57
+ return untrack(() => getFieldInput(internalFieldStore));
58
+ }
59
+
60
+ // If element is file input, handle single or multiple
61
+ if (element.type === 'file') {
62
+ // If multiple files allowed, return files array
63
+ // @ts-expect-error
64
+ if (element.multiple) {
65
+ return [
66
+ // @ts-expect-error
67
+ ...element.files,
68
+ ];
69
+ }
70
+
71
+ // Return single file
72
+ // @ts-expect-error
73
+ return element.files[0];
74
+ }
75
+
76
+ // Return element value for all other input types
77
+ return element.value;
78
+ }
@@ -0,0 +1 @@
1
+ export * from './getElementInput.ts';
@@ -0,0 +1,22 @@
1
+ import type { InternalFieldStore } from '../../types/index.ts';
2
+ import { walkFieldStore } from '../walkFieldStore/index.ts';
3
+
4
+ /**
5
+ * Returns whether the specified boolean property is true for the field store
6
+ * or any of its nested children. Recursively checks arrays and objects.
7
+ *
8
+ * @param internalFieldStore The field store to check.
9
+ * @param type The boolean property type to check.
10
+ *
11
+ * @returns Whether the property is true.
12
+ */
13
+ // @__NO_SIDE_EFFECTS__
14
+ export function getFieldBool(
15
+ internalFieldStore: InternalFieldStore,
16
+ type: 'errors' | 'isTouched' | 'isEdited' | 'isDirty',
17
+ ): boolean {
18
+ // Stop walking as soon as a field has the property set to true
19
+ return walkFieldStore(internalFieldStore, (internalFieldStore) =>
20
+ Boolean(internalFieldStore[type].value),
21
+ );
22
+ }
@@ -0,0 +1 @@
1
+ export * from './getFieldBool.ts';
@@ -0,0 +1,52 @@
1
+ import type { InternalFieldStore } from '../../types/index.ts';
2
+
3
+ /**
4
+ * Returns the current input of the field store. For arrays and objects,
5
+ * recursively collects input from all children. Returns `null` or `undefined`
6
+ * for nullish array/object inputs, or the primitive value for value fields.
7
+ *
8
+ * @param internalFieldStore The field store to get input from.
9
+ *
10
+ * @returns The field input.
11
+ */
12
+ // @__NO_SIDE_EFFECTS__
13
+ export function getFieldInput(internalFieldStore: InternalFieldStore): unknown {
14
+ // If field store is array, collect input from children
15
+ if (internalFieldStore.kind === 'array') {
16
+ // If array input is not nullish, build array from children
17
+ if (internalFieldStore.input.value) {
18
+ // Create output array
19
+ const value = [];
20
+
21
+ // Collect input from each array item
22
+ for (let index = 0; index < internalFieldStore.items.value.length; index++) {
23
+ value[index] = getFieldInput(internalFieldStore.children[index]);
24
+ }
25
+ return value;
26
+ }
27
+
28
+ // Otherwise, return nullish input as-is
29
+ return internalFieldStore.input.value;
30
+ }
31
+
32
+ // If field store is object, collect input from children
33
+ if (internalFieldStore.kind === 'object') {
34
+ // If object input is not nullish, build object from children
35
+ if (internalFieldStore.input.value) {
36
+ // Create output object
37
+ const value: Record<string, unknown> = {};
38
+
39
+ // Collect input from each object property
40
+ for (const key in internalFieldStore.children) {
41
+ value[key] = getFieldInput(internalFieldStore.children[key]);
42
+ }
43
+ return value;
44
+ }
45
+
46
+ // Otherwise, return nullish input as-is
47
+ return internalFieldStore.input.value;
48
+ }
49
+
50
+ // Return primitive value input
51
+ return internalFieldStore.input.value;
52
+ }
@@ -0,0 +1 @@
1
+ export * from './getFieldInput.ts';
@@ -0,0 +1,34 @@
1
+ import type { InternalFieldStore, InternalFormStore, Path } from '../../types/index.ts';
2
+
3
+ /**
4
+ * Returns the field store at the specified path by traversing the form store's
5
+ * children hierarchy.
6
+ *
7
+ * @param internalFormStore The form store to traverse.
8
+ * @param path The path to the field store.
9
+ *
10
+ * @returns The field store.
11
+ */
12
+ // @__NO_SIDE_EFFECTS__
13
+ export function getFieldStore(
14
+ internalFormStore: InternalFormStore,
15
+ path: Path,
16
+ ): InternalFieldStore {
17
+ // Start at form store root
18
+ let internalFieldStore: InternalFieldStore = internalFormStore;
19
+
20
+ // Traverse path to find target field store
21
+ // TODO: This does not guard against paths that exist in the type but not at
22
+ // runtime (e.g. a not-yet-created dynamic array index), so navigating into a
23
+ // missing child returns `undefined` and crashes consumers like the deep error
24
+ // methods. A clean fix likely throws an error or returns `undefined` here
25
+ // and handles it in all callers.
26
+ for (const key of path) {
27
+ // Navigate to child at current path key
28
+ // @ts-expect-error
29
+ internalFieldStore = internalFieldStore.children[key];
30
+ }
31
+
32
+ // Return found field store
33
+ return internalFieldStore;
34
+ }
@@ -0,0 +1 @@
1
+ export * from './getFieldStore.ts';
@@ -0,0 +1,11 @@
1
+ export * from './focusFieldElement/index.ts';
2
+ export * from './getDirtyFieldInput/index.ts';
3
+ export * from './getElementInput/index.ts';
4
+ export * from './getFieldBool/index.ts';
5
+ export * from './getFieldInput/index.ts';
6
+ export * from './getFieldStore/index.ts';
7
+ export * from './initializeFieldStore/index.ts';
8
+ export * from './setFieldBool/index.ts';
9
+ export * from './setFieldInput/index.ts';
10
+ export * from './setInitialFieldInput/index.ts';
11
+ export * from './walkFieldStore/index.ts';
@@ -0,0 +1 @@
1
+ export * from './initializeFieldStore.ts';