@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,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
|
+
}
|