@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.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/UPSTREAM.md +95 -0
- package/package.json +65 -0
- package/src/components/Field/Field.tsrx +22 -0
- package/src/components/Field/Field.tsrx.d.ts +17 -0
- package/src/components/Field/index.ts +1 -0
- package/src/components/FieldArray/FieldArray.tsrx +22 -0
- package/src/components/FieldArray/FieldArray.tsrx.d.ts +18 -0
- package/src/components/FieldArray/index.ts +1 -0
- package/src/components/Form/Form.tsrx +28 -0
- package/src/components/Form/Form.tsrx.d.ts +15 -0
- package/src/components/Form/index.ts +1 -0
- package/src/components/index.ts +3 -0
- package/src/core/array/copyItemState/copyItemState.ts +101 -0
- package/src/core/array/copyItemState/index.ts +1 -0
- package/src/core/array/index.ts +3 -0
- package/src/core/array/resetItemState/index.ts +1 -0
- package/src/core/array/resetItemState/resetItemState.ts +172 -0
- package/src/core/array/swapItemState/index.ts +1 -0
- package/src/core/array/swapItemState/swapItemState.ts +138 -0
- package/src/core/field/focusFieldElement/focusFieldElement.ts +32 -0
- package/src/core/field/focusFieldElement/index.ts +1 -0
- package/src/core/field/getDirtyFieldInput/getDirtyFieldInput.ts +66 -0
- package/src/core/field/getDirtyFieldInput/index.ts +1 -0
- package/src/core/field/getElementInput/getElementInput.ts +78 -0
- package/src/core/field/getElementInput/index.ts +1 -0
- package/src/core/field/getFieldBool/getFieldBool.ts +22 -0
- package/src/core/field/getFieldBool/index.ts +1 -0
- package/src/core/field/getFieldInput/getFieldInput.ts +52 -0
- package/src/core/field/getFieldInput/index.ts +1 -0
- package/src/core/field/getFieldStore/getFieldStore.ts +34 -0
- package/src/core/field/getFieldStore/index.ts +1 -0
- package/src/core/field/index.ts +11 -0
- package/src/core/field/initializeFieldStore/index.ts +1 -0
- package/src/core/field/initializeFieldStore/initializeFieldStore.ts +325 -0
- package/src/core/field/setFieldBool/index.ts +1 -0
- package/src/core/field/setFieldBool/setFieldBool.ts +29 -0
- package/src/core/field/setFieldInput/index.ts +1 -0
- package/src/core/field/setFieldInput/setFieldInput.ts +180 -0
- package/src/core/field/setInitialFieldInput/index.ts +1 -0
- package/src/core/field/setInitialFieldInput/setInitialFieldInput.ts +99 -0
- package/src/core/field/walkFieldStore/index.ts +1 -0
- package/src/core/field/walkFieldStore/walkFieldStore.ts +49 -0
- package/src/core/form/createFormStore/createFormStore.ts +56 -0
- package/src/core/form/createFormStore/index.ts +1 -0
- package/src/core/form/decodeFormData/decodeFormData.ts +436 -0
- package/src/core/form/decodeFormData/index.ts +1 -0
- package/src/core/form/index.ts +4 -0
- package/src/core/form/validateFormInput/index.ts +1 -0
- package/src/core/form/validateFormInput/validateFormInput.ts +138 -0
- package/src/core/form/validateIfRequired/index.ts +1 -0
- package/src/core/form/validateIfRequired/validateIfRequired.ts +31 -0
- package/src/core/framework/index.ts +80 -0
- package/src/core/index.ts +6 -0
- package/src/core/types/field/field.ts +201 -0
- package/src/core/types/field/index.ts +1 -0
- package/src/core/types/form/form.ts +140 -0
- package/src/core/types/form/index.ts +1 -0
- package/src/core/types/index.ts +6 -0
- package/src/core/types/path/index.ts +10 -0
- package/src/core/types/path/path.ts +301 -0
- package/src/core/types/schema/index.ts +1 -0
- package/src/core/types/schema/schema.ts +18 -0
- package/src/core/types/signal/index.ts +1 -0
- package/src/core/types/signal/signal.ts +23 -0
- package/src/core/types/utils/index.ts +1 -0
- package/src/core/types/utils/utils.ts +46 -0
- package/src/core/values.ts +4 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useField/index.ts +1 -0
- package/src/hooks/useField/useField.ts +114 -0
- package/src/hooks/useFieldArray/index.ts +1 -0
- package/src/hooks/useFieldArray/useFieldArray.ts +63 -0
- package/src/hooks/useForm/index.ts +1 -0
- package/src/hooks/useForm/useForm.ts +68 -0
- package/src/hooks/useSignals/index.ts +1 -0
- package/src/hooks/useSignals/useSignals.ts +31 -0
- package/src/index.ts +19 -0
- package/src/internal.ts +32 -0
- package/src/methods/focus/focus.ts +35 -0
- package/src/methods/focus/index.ts +1 -0
- package/src/methods/getDeepErrorEntries/getDeepErrorEntries.ts +108 -0
- package/src/methods/getDeepErrorEntries/index.ts +1 -0
- package/src/methods/getDeepErrors/getDeepErrors.ts +90 -0
- package/src/methods/getDeepErrors/index.ts +1 -0
- package/src/methods/getDirtyInput/getDirtyInput.ts +87 -0
- package/src/methods/getDirtyInput/index.ts +1 -0
- package/src/methods/getDirtyPaths/getDirtyPaths.ts +123 -0
- package/src/methods/getDirtyPaths/index.ts +1 -0
- package/src/methods/getErrors/getErrors.ts +70 -0
- package/src/methods/getErrors/index.ts +1 -0
- package/src/methods/getInput/getInput.ts +75 -0
- package/src/methods/getInput/index.ts +1 -0
- package/src/methods/handleSubmit/handleSubmit.ts +83 -0
- package/src/methods/handleSubmit/index.ts +1 -0
- package/src/methods/index.ts +23 -0
- package/src/methods/insert/index.ts +1 -0
- package/src/methods/insert/insert.ts +134 -0
- package/src/methods/isDirty/index.ts +1 -0
- package/src/methods/isDirty/isDirty.ts +72 -0
- package/src/methods/isEdited/index.ts +1 -0
- package/src/methods/isEdited/isEdited.ts +72 -0
- package/src/methods/isTouched/index.ts +1 -0
- package/src/methods/isTouched/isTouched.ts +72 -0
- package/src/methods/isValid/index.ts +1 -0
- package/src/methods/isValid/isValid.ts +74 -0
- package/src/methods/move/index.ts +1 -0
- package/src/methods/move/move.ts +124 -0
- package/src/methods/pickDirty/index.ts +1 -0
- package/src/methods/pickDirty/pickDirty.ts +87 -0
- package/src/methods/remove/index.ts +1 -0
- package/src/methods/remove/remove.ts +76 -0
- package/src/methods/replace/index.ts +1 -0
- package/src/methods/replace/replace.ts +80 -0
- package/src/methods/reset/index.ts +1 -0
- package/src/methods/reset/reset.ts +216 -0
- package/src/methods/setErrors/index.ts +1 -0
- package/src/methods/setErrors/setErrors.ts +63 -0
- package/src/methods/setInput/index.ts +1 -0
- package/src/methods/setInput/setInput.ts +87 -0
- package/src/methods/submit/index.ts +1 -0
- package/src/methods/submit/submit.ts +11 -0
- package/src/methods/swap/index.ts +1 -0
- package/src/methods/swap/swap.ts +85 -0
- package/src/methods/validate/index.ts +1 -0
- package/src/methods/validate/validate.ts +34 -0
- package/src/types/field.ts +48 -0
- package/src/types/form.ts +12 -0
- 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';
|