@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,325 @@
1
+ import * as v from 'valibot';
2
+ import { createId, createSignal, framework } from '../../framework/index.ts';
3
+ import type {
4
+ EmptyInput,
5
+ FieldElement,
6
+ InternalFieldStore,
7
+ InternalFormStore,
8
+ Path,
9
+ } from '../../types/index.ts';
10
+
11
+ /* eslint-disable @typescript-eslint/no-explicit-any */
12
+ export type FieldSchema =
13
+ | v.ArraySchema<
14
+ v.BaseSchema<unknown, unknown, v.BaseIssue<unknown>>,
15
+ v.ErrorMessage<v.ArrayIssue> | undefined
16
+ >
17
+ | v.BooleanSchema<any>
18
+ | v.DateSchema<any>
19
+ | v.ExactOptionalSchema<any, any>
20
+ | v.IntersectSchema<any, any>
21
+ | v.LazySchema<any>
22
+ | v.LooseObjectSchema<any, any>
23
+ | v.LooseTupleSchema<any, any>
24
+ | v.NonNullableSchema<any, any>
25
+ | v.NonNullishSchema<any, any>
26
+ | v.NonOptionalSchema<any, any>
27
+ | v.NullableSchema<any, any>
28
+ | v.NullishSchema<any, any>
29
+ | v.NumberSchema<any>
30
+ | v.ObjectSchema<v.ObjectEntries, v.ErrorMessage<v.ObjectIssue> | undefined>
31
+ | v.ObjectWithRestSchema<
32
+ v.ObjectEntries,
33
+ v.BaseSchema<unknown, unknown, v.BaseIssue<unknown>>,
34
+ v.ErrorMessage<v.ObjectWithRestIssue> | undefined
35
+ >
36
+ | v.OptionalSchema<any, any>
37
+ | v.PromiseSchema<any>
38
+ | v.RecordSchema<any, any, any>
39
+ | v.StrictObjectSchema<any, any>
40
+ | v.StrictTupleSchema<any, any>
41
+ | v.StringSchema<any>
42
+ | v.TupleSchema<v.TupleItems, v.ErrorMessage<v.TupleIssue> | undefined>
43
+ | v.TupleWithRestSchema<any, any, any>
44
+ | v.UndefinedableSchema<any, any>
45
+ | v.UnionSchema<any, any>
46
+ | v.VariantSchema<any, any, any>;
47
+
48
+ /**
49
+ * Initializes a field store recursively based on the schema structure. Handles
50
+ * array, object, and value schemas, setting up all necessary signals and
51
+ * children. Supports wrapped schemas and schema options.
52
+ *
53
+ * @param internalFormStore The form store providing the empty input config.
54
+ * @param internalFieldStore The partial field store to initialize.
55
+ * @param schema The Valibot schema defining the field structure.
56
+ * @param initialInput The initial input value.
57
+ * @param path The path to the field in the form.
58
+ * @param nullish Whether the schema is wrapped in a nullish schema.
59
+ */
60
+ export function initializeFieldStore(
61
+ internalFormStore: InternalFormStore,
62
+ internalFieldStore: Partial<InternalFieldStore>,
63
+ schema: FieldSchema,
64
+ initialInput: unknown,
65
+ path: Path,
66
+ nullish = false,
67
+ ): void {
68
+ // If schema is unsupported, throw error
69
+ if (
70
+ (framework === 'qwik' && schema.type === 'lazy') ||
71
+ schema.type === 'object_with_rest' ||
72
+ schema.type === 'record' ||
73
+ schema.type === 'tuple_with_rest' ||
74
+ schema.type === 'promise'
75
+ ) {
76
+ throw new Error(`"${schema.type}" schema is not supported`);
77
+
78
+ // Otherwise, if schema is lazy, unwrap and initialize
79
+ } else if (schema.type === 'lazy') {
80
+ initializeFieldStore(
81
+ internalFormStore,
82
+ internalFieldStore,
83
+ schema.getter(undefined),
84
+ initialInput,
85
+ path,
86
+ nullish,
87
+ );
88
+
89
+ // Otherwise, if schema is nullish wrapper, unwrap and initialize
90
+ } else if (
91
+ schema.type === 'exact_optional' ||
92
+ schema.type === 'nullable' ||
93
+ schema.type === 'nullish' ||
94
+ schema.type === 'optional' ||
95
+ schema.type === 'undefinedable'
96
+ ) {
97
+ initializeFieldStore(
98
+ internalFormStore,
99
+ internalFieldStore,
100
+ schema.wrapped,
101
+ initialInput === undefined ? v.getDefault(schema) : initialInput,
102
+ path,
103
+ true,
104
+ );
105
+
106
+ // Otherwise, if schema is non-nullish wrapper, unwrap and initialize
107
+ } else if (
108
+ schema.type === 'non_nullable' ||
109
+ schema.type === 'non_nullish' ||
110
+ schema.type === 'non_optional'
111
+ ) {
112
+ // Forward the nullish flag so an outer optional or nullable wrapper still
113
+ // keeps the field at `undefined`/`null` instead of its empty input (e.g.
114
+ // `v.optional(v.nonOptional(v.string()))`)
115
+ initializeFieldStore(
116
+ internalFormStore,
117
+ internalFieldStore,
118
+ schema.wrapped,
119
+ initialInput,
120
+ path,
121
+ nullish,
122
+ );
123
+
124
+ // Otherwise, if schema has options, initialize for each option
125
+ } else if (schema.type === 'intersect' || schema.type === 'union' || schema.type === 'variant') {
126
+ // Initialize field store for each schema option
127
+ // Hint: Options share a single field store per key, so per-branch metadata
128
+ // (`schema`, `kind`, `isNullish`) is approximated last-write-wins. A key
129
+ // that differs across branches (e.g. nullish in one, required in another)
130
+ // is therefore not fully represented. See #95 for the long-term fix.
131
+ for (const schemaOption of schema.options) {
132
+ initializeFieldStore(
133
+ internalFormStore,
134
+ internalFieldStore,
135
+ schemaOption,
136
+ initialInput,
137
+ path,
138
+ nullish,
139
+ );
140
+ }
141
+
142
+ // Otherwise, initialize as concrete schema
143
+ } else {
144
+ // Set basic properties
145
+ internalFieldStore.schema = schema;
146
+ internalFieldStore.name = JSON.stringify(path);
147
+ // Hint: Each field store receives its own freshly built path array (see the
148
+ // `[...path, key]` calls below), so it can be stored directly.
149
+ internalFieldStore.path = path;
150
+
151
+ // Store whether property is nullish so resetting can stay consistent
152
+ internalFieldStore.isNullish = nullish;
153
+
154
+ // Initialize elements array
155
+ // Hint: `initialElements` and `elements` start as the same array so that
156
+ // `reset` can restore elements that array methods move between field stores
157
+ // (see `initialElements` in the `InternalBaseStore` interface).
158
+ const initialElements: FieldElement[] = [];
159
+ internalFieldStore.initialElements = initialElements;
160
+ internalFieldStore.elements = initialElements;
161
+
162
+ // Initialize common signals
163
+ internalFieldStore.errors = createSignal(null);
164
+ internalFieldStore.isTouched = createSignal(false);
165
+ internalFieldStore.isEdited = createSignal(false);
166
+ internalFieldStore.isDirty = createSignal(false);
167
+
168
+ // If schema is array or tuple, initialize as array field
169
+ if (
170
+ schema.type === 'array' ||
171
+ schema.type === 'loose_tuple' ||
172
+ schema.type === 'strict_tuple' ||
173
+ schema.type === 'tuple'
174
+ ) {
175
+ // If already initialized as different kind, throw error
176
+ if (internalFieldStore.kind && internalFieldStore.kind !== 'array') {
177
+ throw new Error(
178
+ `Store initialized as "${internalFieldStore.kind}" cannot be reinitialized as "array"`,
179
+ );
180
+ }
181
+
182
+ // Set kind to array
183
+ internalFieldStore.kind = 'array';
184
+
185
+ // Initialize array-specific properties
186
+ if (internalFieldStore.kind === 'array') {
187
+ // Initialize children array if not exists
188
+ internalFieldStore.children ??= [];
189
+
190
+ // If schema is dynamic array, initialize children from input
191
+ if (schema.type === 'array') {
192
+ // If initial input provided, initialize children
193
+ if (initialInput) {
194
+ // Initialize child for each input item
195
+ for (
196
+ let index = 0;
197
+ // @ts-expect-error
198
+ index < initialInput.length;
199
+ index++
200
+ ) {
201
+ // Create empty child object
202
+ // @ts-expect-error
203
+ internalFieldStore.children[index] = {};
204
+
205
+ // Initialize field store for child
206
+ initializeFieldStore(
207
+ internalFormStore,
208
+ internalFieldStore.children[index],
209
+ schema.item as FieldSchema,
210
+ // @ts-expect-error
211
+ initialInput[index],
212
+ [...path, index],
213
+ );
214
+ }
215
+ }
216
+
217
+ // Otherwise, if schema is fixed tuple, initialize children from schema
218
+ } else {
219
+ // Initialize child for each tuple item
220
+ for (let index = 0; index < schema.items.length; index++) {
221
+ // Create empty child object
222
+ // @ts-expect-error
223
+ internalFieldStore.children[index] = {};
224
+
225
+ // Initialize field store for child
226
+ initializeFieldStore(
227
+ internalFormStore,
228
+ internalFieldStore.children[index],
229
+ schema.items[index] as FieldSchema,
230
+ // @ts-expect-error
231
+ initialInput?.[index],
232
+ [...path, index],
233
+ );
234
+ }
235
+ }
236
+
237
+ // Set array input (nullish or true)
238
+ const arrayInput = nullish && initialInput == null ? initialInput : true;
239
+ internalFieldStore.initialInput = createSignal(arrayInput);
240
+ internalFieldStore.startInput = createSignal(arrayInput);
241
+ internalFieldStore.input = createSignal(arrayInput);
242
+
243
+ // Set items with unique IDs for each child
244
+ const initialItems = internalFieldStore.children.map(createId);
245
+ internalFieldStore.initialItems = createSignal(initialItems);
246
+ internalFieldStore.startItems = createSignal(initialItems);
247
+ internalFieldStore.items = createSignal(initialItems);
248
+ }
249
+
250
+ // Otherwise, if schema is object, initialize as object field
251
+ } else if (
252
+ schema.type === 'loose_object' ||
253
+ schema.type === 'object' ||
254
+ schema.type === 'strict_object'
255
+ ) {
256
+ // If already initialized as different kind, throw error
257
+ if (internalFieldStore.kind && internalFieldStore.kind !== 'object') {
258
+ throw new Error(
259
+ `Store initialized as "${internalFieldStore.kind}" cannot be reinitialized as "object"`,
260
+ );
261
+ }
262
+
263
+ // Set kind to object
264
+ internalFieldStore.kind = 'object';
265
+
266
+ // Initialize object-specific properties
267
+ if (internalFieldStore.kind === 'object') {
268
+ // Initialize children object if not exists
269
+ internalFieldStore.children ??= {};
270
+
271
+ // Initialize child for each object entry
272
+ for (const key in schema.entries) {
273
+ // Create empty child object if not exists
274
+ // @ts-expect-error
275
+ internalFieldStore.children[key] ??= {};
276
+
277
+ // Initialize field store for child
278
+ initializeFieldStore(
279
+ internalFormStore,
280
+ internalFieldStore.children[key],
281
+ schema.entries[key] as FieldSchema,
282
+ // @ts-expect-error
283
+ initialInput?.[key],
284
+ [...path, key],
285
+ );
286
+ }
287
+
288
+ // Set object input (nullish or true)
289
+ const objectInput = nullish && initialInput == null ? initialInput : true;
290
+ internalFieldStore.initialInput = createSignal(objectInput);
291
+ internalFieldStore.startInput = createSignal(objectInput);
292
+ internalFieldStore.input = createSignal(objectInput);
293
+ }
294
+
295
+ // Otherwise, initialize as value field (leaf node)
296
+ } else {
297
+ // If already initialized as different kind, throw error
298
+ if (internalFieldStore.kind && internalFieldStore.kind !== 'value') {
299
+ throw new Error(
300
+ `Store initialized as "${internalFieldStore.kind}" cannot be reinitialized as "value"`,
301
+ );
302
+ }
303
+
304
+ // Set kind to value
305
+ internalFieldStore.kind = 'value';
306
+
307
+ // Initialize value-specific properties
308
+ if (internalFieldStore.kind === 'value') {
309
+ // Resolve the empty input for this field's type from the configured map
310
+ // (e.g. `''` for a string), so an untouched empty field matches the DOM
311
+ // and validates with its own message instead of a type mismatch.
312
+ // Optional and nullable fields stay `undefined` as they accept it.
313
+ const valueInput =
314
+ initialInput === undefined && !nullish
315
+ ? internalFormStore.emptyInput[schema.type as keyof EmptyInput]
316
+ : initialInput;
317
+
318
+ // Set initial, start and current input
319
+ internalFieldStore.initialInput = createSignal(valueInput);
320
+ internalFieldStore.startInput = createSignal(valueInput);
321
+ internalFieldStore.input = createSignal(valueInput);
322
+ }
323
+ }
324
+ }
325
+ }
@@ -0,0 +1 @@
1
+ export * from './setFieldBool.ts';
@@ -0,0 +1,29 @@
1
+ import { batch, untrack } from '../../framework/index.ts';
2
+ import type { InternalFieldStore } from '../../types/index.ts';
3
+ import { walkFieldStore } from '../walkFieldStore/index.ts';
4
+
5
+ /**
6
+ * Sets the specified boolean property for the field store and all nested
7
+ * children. Recursively updates arrays and objects.
8
+ *
9
+ * @param internalFieldStore The field store to update.
10
+ * @param type The boolean property type to set.
11
+ * @param bool The boolean value to set.
12
+ */
13
+ export function setFieldBool(
14
+ internalFieldStore: InternalFieldStore,
15
+ type: 'isTouched' | 'isDirty',
16
+ bool: boolean,
17
+ ): void {
18
+ // Batch all state updates for optimal reactivity performance
19
+ batch(() => {
20
+ // Untracked to avoid subscribing a surrounding reactive scope to the
21
+ // form structure
22
+ untrack(() => {
23
+ // Set property on each field store
24
+ walkFieldStore(internalFieldStore, (internalFieldStore) => {
25
+ internalFieldStore[type].value = bool;
26
+ });
27
+ });
28
+ });
29
+ }
@@ -0,0 +1 @@
1
+ export * from './setFieldInput.ts';
@@ -0,0 +1,180 @@
1
+ import { resetItemState } from '../../array/resetItemState/index.ts';
2
+ import { batch, createId, untrack } from '../../framework/index.ts';
3
+ import type { InternalFieldStore, InternalFormStore, Path } from '../../types/index.ts';
4
+ import { initializeFieldStore } from '../initializeFieldStore/index.ts';
5
+
6
+ /**
7
+ * Sets the input for a nested field store and all its children, updating
8
+ * touched and dirty states accordingly. Handles dynamic array resizing.
9
+ *
10
+ * @param internalFormStore The form store providing the empty input config.
11
+ * @param internalFieldStore The field store to update.
12
+ * @param input The new input value.
13
+ */
14
+ function setNestedInput(
15
+ internalFormStore: InternalFormStore,
16
+ internalFieldStore: InternalFieldStore,
17
+ input: unknown,
18
+ ): void {
19
+ // Mark field as touched and edited
20
+ internalFieldStore.isTouched.value = true;
21
+ internalFieldStore.isEdited.value = true;
22
+
23
+ // If field store is array, handle array input
24
+ if (internalFieldStore.kind === 'array') {
25
+ // Normalize input to empty array if nullish
26
+ const arrayInput = input ?? [];
27
+ const items = internalFieldStore.items.value;
28
+
29
+ // Tuples have a fixed number of children, so ignore any extra input items
30
+ // instead of growing them (they have no `item` schema to initialize
31
+ // additional children, unlike dynamic arrays)
32
+ const length =
33
+ internalFieldStore.schema.type === 'array'
34
+ ? (arrayInput as unknown[]).length
35
+ : internalFieldStore.children.length;
36
+
37
+ // If new array is shorter, truncate items
38
+ if (length < items.length) {
39
+ internalFieldStore.items.value = items.slice(0, length);
40
+
41
+ // Otherwise, if new array is longer, extend items
42
+ } else if (length > items.length) {
43
+ // Initialize or reset each newly visible child
44
+ for (let index = items.length; index < length; index++) {
45
+ // Reset the reused stale child but keep its start input as baseline
46
+ // Hint: A child store from a previously longer array still holds stale
47
+ // errors and nested values that must be cleared, but its `startInput`
48
+ // and `startItems` are the dirty baseline. Passing `keepStart`
49
+ // preserves them so editing a regrown index is detected as dirty, just
50
+ // like a direct edit on a never-shrunk array would be.
51
+ if (internalFieldStore.children[index]) {
52
+ resetItemState(
53
+ internalFormStore,
54
+ internalFieldStore.children[index],
55
+ // @ts-expect-error
56
+ arrayInput[index],
57
+ true,
58
+ );
59
+
60
+ // Otherwise, create and initialize a brand-new child
61
+ } else {
62
+ // Create empty child object
63
+ // @ts-expect-error
64
+ internalFieldStore.children[index] = {};
65
+
66
+ // Initialize field store for new child
67
+ initializeFieldStore(
68
+ internalFormStore,
69
+ internalFieldStore.children[index],
70
+ // @ts-expect-error
71
+ internalFieldStore.schema.item,
72
+ // @ts-expect-error
73
+ arrayInput[index],
74
+ [...internalFieldStore.path, index],
75
+ );
76
+ }
77
+ }
78
+
79
+ // Extend items array with new items, capped to the clamped length so a
80
+ // tuple never grows beyond its fixed number of children
81
+ internalFieldStore.items.value = [
82
+ ...items,
83
+ ...Array.from({ length: length - items.length }, createId),
84
+ ];
85
+ }
86
+
87
+ // Set input for each array item
88
+ for (let index = 0; index < length; index++) {
89
+ // Recursively set nested input
90
+ setNestedInput(
91
+ internalFormStore,
92
+ internalFieldStore.children[index],
93
+ // @ts-expect-error
94
+ arrayInput[index],
95
+ );
96
+ }
97
+
98
+ // Set array input
99
+ internalFieldStore.input.value = input == null ? input : true;
100
+
101
+ // Update dirty state based on input or items length change
102
+ internalFieldStore.isDirty.value =
103
+ internalFieldStore.startInput.value !== internalFieldStore.input.value ||
104
+ internalFieldStore.startItems.value.length !== internalFieldStore.items.value.length;
105
+
106
+ // Otherwise, if field store is object, handle object input
107
+ } else if (internalFieldStore.kind === 'object') {
108
+ // Set input for each object property
109
+ for (const key in internalFieldStore.children) {
110
+ // Recursively set nested input
111
+ setNestedInput(
112
+ internalFormStore,
113
+ internalFieldStore.children[key],
114
+ // @ts-expect-error
115
+ input?.[key],
116
+ );
117
+ }
118
+
119
+ // Set object input
120
+ internalFieldStore.input.value = input == null ? input : true;
121
+
122
+ // Update dirty state based on input change
123
+ internalFieldStore.isDirty.value =
124
+ internalFieldStore.startInput.value !== internalFieldStore.input.value;
125
+
126
+ // Otherwise, handle value field input
127
+ } else {
128
+ // Set value input
129
+ internalFieldStore.input.value = input;
130
+
131
+ // TODO: Should we add support for Dates and Files?
132
+ // Get start input for comparison
133
+ const startInput = internalFieldStore.startInput.value;
134
+
135
+ // Update dirty state with special handling for empty string and NaN
136
+ internalFieldStore.isDirty.value =
137
+ startInput !== input &&
138
+ // Hint: This check ensures that an empty string or `NaN` does not mark
139
+ // the field as dirty if the start input was `undefined` or `null`.
140
+ (startInput != null || (input !== '' && !Number.isNaN(input)));
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Sets the input for a field at the specified path in the form store,
146
+ * traversing the path and updating all parent fields along the way.
147
+ *
148
+ * @param internalFormStore The form store containing the field.
149
+ * @param path The path to the field.
150
+ * @param input The new input value.
151
+ */
152
+ export function setFieldInput(
153
+ internalFormStore: InternalFormStore,
154
+ path: Path,
155
+ input: unknown,
156
+ ): void {
157
+ // Batch all state updates for optimal reactivity performance
158
+ batch(() => {
159
+ // Untrack to avoid creating reactive dependencies during update
160
+ untrack(() => {
161
+ // Start at form store root
162
+ let internalFieldStore: InternalFieldStore = internalFormStore;
163
+
164
+ // Traverse path to target field
165
+ for (let index = 0; index < path.length; index++) {
166
+ // Navigate to child at current path key
167
+ // @ts-expect-error
168
+ internalFieldStore = internalFieldStore.children[path[index]];
169
+
170
+ // If not at target field, mark parent input as truthy
171
+ if (index < path.length - 1) {
172
+ internalFieldStore.input.value = true;
173
+ }
174
+ }
175
+
176
+ // Set nested input on target field
177
+ setNestedInput(internalFormStore, internalFieldStore, input);
178
+ });
179
+ });
180
+ }
@@ -0,0 +1 @@
1
+ export * from './setInitialFieldInput.ts';
@@ -0,0 +1,99 @@
1
+ import { batch, createId } from '../../framework/index.ts';
2
+ import type { EmptyInput, InternalFieldStore, InternalFormStore } from '../../types/index.ts';
3
+ import { initializeFieldStore } from '../initializeFieldStore/index.ts';
4
+
5
+ /**
6
+ * Sets the initial input for a field store and all its children recursively.
7
+ * For arrays, initializes missing children if needed. Updates `initialInput`
8
+ * and `initialItems` properties.
9
+ *
10
+ * @param internalFormStore The form store providing the empty input config.
11
+ * @param internalFieldStore The field store to update.
12
+ * @param initialInput The initial input value.
13
+ */
14
+ export function setInitialFieldInput(
15
+ internalFormStore: InternalFormStore,
16
+ internalFieldStore: InternalFieldStore,
17
+ initialInput: unknown,
18
+ ): void {
19
+ // Batch all state updates for optimal reactivity performance
20
+ batch(() => {
21
+ // If field store is array, handle array initial input
22
+ if (internalFieldStore.kind === 'array') {
23
+ // Set initial array input
24
+ internalFieldStore.initialInput.value = initialInput == null ? initialInput : true;
25
+
26
+ // Normalize input to empty array if nullish
27
+ const initialArrayInput = initialInput ?? [];
28
+
29
+ // Tuples have a fixed number of children, so ignore any extra input items
30
+ // instead of growing them (they have no `item` schema to initialize
31
+ // additional children, unlike dynamic arrays)
32
+ const length =
33
+ internalFieldStore.schema.type === 'array'
34
+ ? (initialArrayInput as unknown[]).length
35
+ : internalFieldStore.children.length;
36
+
37
+ // If initial input exceeds children capacity, initialize new children
38
+ if (length > internalFieldStore.children.length) {
39
+ // Initialize missing children
40
+ for (let index = internalFieldStore.children.length; index < length; index++) {
41
+ // Create empty child object
42
+ // @ts-expect-error
43
+ internalFieldStore.children[index] = {};
44
+
45
+ // Initialize field store for new child
46
+ initializeFieldStore(
47
+ internalFormStore,
48
+ internalFieldStore.children[index],
49
+ // @ts-expect-error
50
+ internalFieldStore.schema.item,
51
+ // @ts-expect-error
52
+ initialArrayInput[index],
53
+ [...internalFieldStore.path, index],
54
+ );
55
+ }
56
+ }
57
+
58
+ // Set initial items with unique IDs
59
+ internalFieldStore.initialItems.value = Array.from({ length }, createId);
60
+
61
+ // Set initial input for each array item
62
+ for (let index = 0; index < internalFieldStore.children.length; index++) {
63
+ // Recursively set initial input for child
64
+ setInitialFieldInput(
65
+ internalFormStore,
66
+ internalFieldStore.children[index],
67
+ // @ts-expect-error
68
+ initialArrayInput[index],
69
+ );
70
+ }
71
+
72
+ // Otherwise, if field store is object, handle object initial input
73
+ } else if (internalFieldStore.kind === 'object') {
74
+ // Set initial object input
75
+ internalFieldStore.initialInput.value = initialInput == null ? initialInput : true;
76
+
77
+ // Set initial input for each object property
78
+ for (const key in internalFieldStore.children) {
79
+ // Recursively set initial input for child
80
+ setInitialFieldInput(
81
+ internalFormStore,
82
+ internalFieldStore.children[key],
83
+ // @ts-expect-error
84
+ initialInput?.[key],
85
+ );
86
+ }
87
+
88
+ // Otherwise, handle value field initial input
89
+ } else {
90
+ // Fall back to the empty input for this field's type when no input is
91
+ // provided so the initial input stays consistent with form
92
+ // initialization. Optional and nullable fields stay `undefined`.
93
+ internalFieldStore.initialInput.value =
94
+ initialInput === undefined && !internalFieldStore.isNullish
95
+ ? internalFormStore.emptyInput[internalFieldStore.schema.type as keyof EmptyInput]
96
+ : initialInput;
97
+ }
98
+ });
99
+ }
@@ -0,0 +1 @@
1
+ export * from './walkFieldStore.ts';
@@ -0,0 +1,49 @@
1
+ import type { InternalFieldStore } from '../../types/index.ts';
2
+
3
+ /**
4
+ * Walks through the field store and all nested children, calling the callback
5
+ * for each field store in depth-first order. The callback may return `true` to
6
+ * stop the walk early, in which case `walkFieldStore` returns `true` as well.
7
+ *
8
+ * The walk reads array `items` reactively, so a reactive caller subscribes to
9
+ * structural changes naturally. Imperative callers that must not subscribe
10
+ * (e.g. when invoked inside an effect) should wrap the call in `untrack`.
11
+ *
12
+ * @param internalFieldStore The field store to walk.
13
+ * @param callback The callback to invoke for each field store. Return `true` to stop the walk early.
14
+ *
15
+ * @returns Whether the walk was stopped early by the callback.
16
+ */
17
+ export function walkFieldStore(
18
+ internalFieldStore: InternalFieldStore,
19
+ callback: (internalFieldStore: InternalFieldStore) => boolean | void,
20
+ ): boolean {
21
+ // Invoke callback for current field store and stop early if requested
22
+ if (callback(internalFieldStore)) {
23
+ return true;
24
+ }
25
+
26
+ // If field store is array, walk all children
27
+ if (internalFieldStore.kind === 'array') {
28
+ // Walk each array item
29
+ for (let index = 0; index < internalFieldStore.items.value.length; index++) {
30
+ // Recursively walk child and stop early if requested
31
+ if (walkFieldStore(internalFieldStore.children[index], callback)) {
32
+ return true;
33
+ }
34
+ }
35
+
36
+ // Otherwise, if field store is object, walk all children
37
+ } else if (internalFieldStore.kind === 'object') {
38
+ // Walk each object property
39
+ for (const key in internalFieldStore.children) {
40
+ // Recursively walk child and stop early if requested
41
+ if (walkFieldStore(internalFieldStore.children[key], callback)) {
42
+ return true;
43
+ }
44
+ }
45
+ }
46
+
47
+ // Otherwise, return that the walk completed without stopping early
48
+ return false;
49
+ }