@formisch/svelte 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -68,11 +68,13 @@ function initializeFieldStore(internalFieldStore, schema, initialInput, path, nu
68
68
  else {
69
69
  internalFieldStore.schema = schema;
70
70
  internalFieldStore.name = JSON.stringify(path);
71
+ internalFieldStore.path = path;
71
72
  const initialElements = [];
72
73
  internalFieldStore.initialElements = initialElements;
73
74
  internalFieldStore.elements = initialElements;
74
75
  internalFieldStore.errors = /* @__PURE__ */ createSignal(null);
75
76
  internalFieldStore.isTouched = /* @__PURE__ */ createSignal(false);
77
+ internalFieldStore.isEdited = /* @__PURE__ */ createSignal(false);
76
78
  internalFieldStore.isDirty = /* @__PURE__ */ createSignal(false);
77
79
  if (schema.type === "array" || schema.type === "loose_tuple" || schema.type === "strict_tuple" || schema.type === "tuple") {
78
80
  if (internalFieldStore.kind && internalFieldStore.kind !== "array") throw new Error(`Store initialized as "${internalFieldStore.kind}" cannot be reinitialized as "array"`);
@@ -82,16 +84,13 @@ function initializeFieldStore(internalFieldStore, schema, initialInput, path, nu
82
84
  if (schema.type === "array") {
83
85
  if (initialInput) for (let index = 0; index < initialInput.length; index++) {
84
86
  internalFieldStore.children[index] = {};
85
- path.push(index);
86
- initializeFieldStore(internalFieldStore.children[index], schema.item, initialInput[index], path);
87
- path.pop();
87
+ initializeFieldStore(internalFieldStore.children[index], schema.item, initialInput[index], [...path, index]);
88
88
  }
89
89
  } else for (let index = 0; index < schema.items.length; index++) {
90
90
  internalFieldStore.children[index] = {};
91
- path.push(index);
92
- initializeFieldStore(internalFieldStore.children[index], schema.items[index], initialInput?.[index], path);
93
- path.pop();
91
+ initializeFieldStore(internalFieldStore.children[index], schema.items[index], initialInput?.[index], [...path, index]);
94
92
  }
93
+ internalFieldStore.isNullish = nullish;
95
94
  const arrayInput = nullish && initialInput == null ? initialInput : true;
96
95
  internalFieldStore.initialInput = /* @__PURE__ */ createSignal(arrayInput);
97
96
  internalFieldStore.startInput = /* @__PURE__ */ createSignal(arrayInput);
@@ -108,10 +107,9 @@ function initializeFieldStore(internalFieldStore, schema, initialInput, path, nu
108
107
  internalFieldStore.children ??= {};
109
108
  for (const key in schema.entries) {
110
109
  internalFieldStore.children[key] ??= {};
111
- path.push(key);
112
- initializeFieldStore(internalFieldStore.children[key], schema.entries[key], initialInput?.[key], path);
113
- path.pop();
110
+ initializeFieldStore(internalFieldStore.children[key], schema.entries[key], initialInput?.[key], [...path, key]);
114
111
  }
112
+ internalFieldStore.isNullish = nullish;
115
113
  const objectInput = nullish && initialInput == null ? initialInput : true;
116
114
  internalFieldStore.initialInput = /* @__PURE__ */ createSignal(objectInput);
117
115
  internalFieldStore.startInput = /* @__PURE__ */ createSignal(objectInput);
@@ -134,8 +132,9 @@ function initializeFieldStore(internalFieldStore, schema, initialInput, path, nu
134
132
  /**
135
133
  * Copies the deeply nested state (signal values) from one field store to
136
134
  * another. This includes the `elements`, `errors`, `startInput`, `input`,
137
- * `isTouched`, `isDirty`, and for arrays `startItems` and `items` properties.
138
- * Recursively walks through the field stores and copies all signal values.
135
+ * `isTouched`, `isEdited`, `isDirty`, and for arrays `startItems` and `items`
136
+ * properties. Recursively walks through the field stores and copies all signal
137
+ * values.
139
138
  *
140
139
  * @param fromInternalFieldStore The source field store to copy from.
141
140
  * @param toInternalFieldStore The destination field store to copy to.
@@ -148,19 +147,16 @@ function copyItemState(fromInternalFieldStore, toInternalFieldStore) {
148
147
  toInternalFieldStore.startInput.value = fromInternalFieldStore.startInput.value;
149
148
  toInternalFieldStore.input.value = fromInternalFieldStore.input.value;
150
149
  toInternalFieldStore.isTouched.value = fromInternalFieldStore.isTouched.value;
150
+ toInternalFieldStore.isEdited.value = fromInternalFieldStore.isEdited.value;
151
151
  toInternalFieldStore.isDirty.value = fromInternalFieldStore.isDirty.value;
152
152
  if (fromInternalFieldStore.kind === "array" && toInternalFieldStore.kind === "array") {
153
153
  const fromItems = fromInternalFieldStore.items.value;
154
154
  toInternalFieldStore.startItems.value = fromInternalFieldStore.startItems.value;
155
155
  toInternalFieldStore.items.value = fromItems;
156
- let path;
157
156
  for (let index = 0; index < fromItems.length; index++) {
158
157
  if (!toInternalFieldStore.children[index]) {
159
- path ??= JSON.parse(toInternalFieldStore.name);
160
158
  toInternalFieldStore.children[index] = {};
161
- path.push(index);
162
- initializeFieldStore(toInternalFieldStore.children[index], toInternalFieldStore.schema.item, void 0, path);
163
- path.pop();
159
+ initializeFieldStore(toInternalFieldStore.children[index], toInternalFieldStore.schema.item, void 0, [...toInternalFieldStore.path, index]);
164
160
  }
165
161
  copyItemState(fromInternalFieldStore.children[index], toInternalFieldStore.children[index]);
166
162
  }
@@ -173,37 +169,53 @@ function copyItemState(fromInternalFieldStore, toInternalFieldStore) {
173
169
  //#region src/array/resetItemState/resetItemState.ts
174
170
  /**
175
171
  * Resets the state of a field store (signal values) deeply nested. Sets
176
- * `elements` to empty array, `errors` to `null`, `isTouched` and `isDirty` to
177
- * `false`, and `startInput`, `input`, `startItems`, and `items` to the new
178
- * input value. Keeps the `initialInput` and `initialItems` state unchanged for
179
- * form reset functionality.
172
+ * `elements` to empty array, `errors` to `null`, `isTouched`, `isEdited` and
173
+ * `isDirty` to `false`, and `startInput`, `input`, `startItems`, and `items` to
174
+ * the new input value. Keeps the `initialInput` and `initialItems` state
175
+ * unchanged for form reset functionality.
180
176
  *
181
177
  * @param internalFieldStore The field store to reset.
182
- * @param initialInput The new input value (can be any type including array or object).
178
+ * @param input The new input value (can be any type including array or object).
179
+ * @param keepStart Whether to keep `startInput` and `startItems` as the dirty
180
+ * baseline instead of resetting them to the new input. Used when a field store
181
+ * is reused for an in-place edit so its dirty state is detected correctly.
183
182
  */
184
- function resetItemState(internalFieldStore, initialInput) {
183
+ function resetItemState(internalFieldStore, input, keepStart = false) {
185
184
  batch(() => {
186
- internalFieldStore.elements = [];
185
+ const elements = [];
186
+ if (internalFieldStore.elements === internalFieldStore.initialElements) internalFieldStore.initialElements = elements;
187
+ internalFieldStore.elements = elements;
187
188
  internalFieldStore.errors.value = null;
188
189
  internalFieldStore.isTouched.value = false;
190
+ internalFieldStore.isEdited.value = false;
189
191
  internalFieldStore.isDirty.value = false;
190
192
  if (internalFieldStore.kind === "array" || internalFieldStore.kind === "object") {
191
- const objectInput = initialInput == null ? initialInput : true;
192
- internalFieldStore.startInput.value = objectInput;
193
+ const objectInput = internalFieldStore.isNullish && input == null ? input : true;
194
+ if (!keepStart) internalFieldStore.startInput.value = objectInput;
193
195
  internalFieldStore.input.value = objectInput;
194
- if (internalFieldStore.kind === "array") if (initialInput) {
195
- const newItems = initialInput.map(createId);
196
- internalFieldStore.startItems.value = newItems;
197
- internalFieldStore.items.value = newItems;
198
- for (let index = 0; index < initialInput.length; index++) if (internalFieldStore.children[index]) resetItemState(internalFieldStore.children[index], initialInput[index]);
199
- } else {
200
- internalFieldStore.startItems.value = [];
201
- internalFieldStore.items.value = [];
202
- }
203
- else for (const key in internalFieldStore.children) resetItemState(internalFieldStore.children[key], initialInput?.[key]);
196
+ if (internalFieldStore.kind === "array") {
197
+ const isTuple = internalFieldStore.schema.type !== "array";
198
+ if (input || isTuple) {
199
+ const length = isTuple ? internalFieldStore.children.length : input.length;
200
+ const newItems = Array.from({ length }, createId);
201
+ if (!keepStart) internalFieldStore.startItems.value = newItems;
202
+ internalFieldStore.items.value = newItems;
203
+ for (let index = 0; index < length; index++) {
204
+ const itemInput = input?.[index];
205
+ if (internalFieldStore.children[index]) resetItemState(internalFieldStore.children[index], itemInput, keepStart);
206
+ else {
207
+ internalFieldStore.children[index] = {};
208
+ initializeFieldStore(internalFieldStore.children[index], internalFieldStore.schema.item, itemInput, [...internalFieldStore.path, index]);
209
+ }
210
+ }
211
+ } else {
212
+ if (!keepStart) internalFieldStore.startItems.value = [];
213
+ internalFieldStore.items.value = [];
214
+ }
215
+ } else for (const key in internalFieldStore.children) resetItemState(internalFieldStore.children[key], input?.[key], keepStart);
204
216
  } else {
205
- internalFieldStore.startInput.value = initialInput;
206
- internalFieldStore.input.value = initialInput;
217
+ if (!keepStart) internalFieldStore.startInput.value = input;
218
+ internalFieldStore.input.value = input;
207
219
  }
208
220
  });
209
221
  }
@@ -213,8 +225,8 @@ function resetItemState(internalFieldStore, initialInput) {
213
225
  /**
214
226
  * Swaps the deeply nested state (signal values) between two field stores. This
215
227
  * includes the `elements`, `errors`, `startInput`, `input`, `isTouched`,
216
- * `isDirty`, and for arrays `startItems` and `items` properties. Recursively
217
- * walks through the field stores and swaps all signal values.
228
+ * `isEdited`, `isDirty`, and for arrays `startItems` and `items` properties.
229
+ * Recursively walks through the field stores and swaps all signal values.
218
230
  *
219
231
  * @param firstInternalFieldStore The first field store to swap.
220
232
  * @param secondInternalFieldStore The second field store to swap.
@@ -237,6 +249,9 @@ function swapItemState(firstInternalFieldStore, secondInternalFieldStore) {
237
249
  const tempIsTouched = firstInternalFieldStore.isTouched.value;
238
250
  firstInternalFieldStore.isTouched.value = secondInternalFieldStore.isTouched.value;
239
251
  secondInternalFieldStore.isTouched.value = tempIsTouched;
252
+ const tempIsEdited = firstInternalFieldStore.isEdited.value;
253
+ firstInternalFieldStore.isEdited.value = secondInternalFieldStore.isEdited.value;
254
+ secondInternalFieldStore.isEdited.value = tempIsEdited;
240
255
  const tempIsDirty = firstInternalFieldStore.isDirty.value;
241
256
  firstInternalFieldStore.isDirty.value = secondInternalFieldStore.isDirty.value;
242
257
  secondInternalFieldStore.isDirty.value = tempIsDirty;
@@ -249,22 +264,14 @@ function swapItemState(firstInternalFieldStore, secondInternalFieldStore) {
249
264
  firstInternalFieldStore.items.value = secondItems;
250
265
  secondInternalFieldStore.items.value = firstItems;
251
266
  const maxLength = Math.max(firstItems.length, secondItems.length);
252
- let firstPath;
253
- let secondPath;
254
267
  for (let index = 0; index < maxLength; index++) {
255
268
  if (!firstInternalFieldStore.children[index]) {
256
- firstPath ??= JSON.parse(firstInternalFieldStore.name);
257
269
  firstInternalFieldStore.children[index] = {};
258
- firstPath.push(index);
259
- initializeFieldStore(firstInternalFieldStore.children[index], firstInternalFieldStore.schema.item, void 0, firstPath);
260
- firstPath.pop();
270
+ initializeFieldStore(firstInternalFieldStore.children[index], firstInternalFieldStore.schema.item, void 0, [...firstInternalFieldStore.path, index]);
261
271
  }
262
272
  if (!secondInternalFieldStore.children[index]) {
263
- secondPath ??= JSON.parse(secondInternalFieldStore.name);
264
273
  secondInternalFieldStore.children[index] = {};
265
- secondPath.push(index);
266
- initializeFieldStore(secondInternalFieldStore.children[index], secondInternalFieldStore.schema.item, void 0, secondPath);
267
- secondPath.pop();
274
+ initializeFieldStore(secondInternalFieldStore.children[index], secondInternalFieldStore.schema.item, void 0, [...secondInternalFieldStore.path, index]);
268
275
  }
269
276
  swapItemState(firstInternalFieldStore.children[index], secondInternalFieldStore.children[index]);
270
277
  }
@@ -273,6 +280,57 @@ function swapItemState(firstInternalFieldStore, secondInternalFieldStore) {
273
280
  });
274
281
  }
275
282
 
283
+ //#endregion
284
+ //#region src/field/focusFieldElement/focusFieldElement.ts
285
+ /**
286
+ * Focuses the first focusable element of a field store. The elements are tried
287
+ * in order and the first one that actually receives focus wins, so detached,
288
+ * disabled or hidden elements are skipped. The browser decides focusability,
289
+ * which is read back via the element's root `activeElement` so elements in a
290
+ * shadow root or another document are handled correctly.
291
+ *
292
+ * Hint: A `display: none` or `hidden` element is correctly skipped in real
293
+ * browsers, but jsdom has no layout and focuses it anyway, so that case cannot
294
+ * be covered by unit tests.
295
+ *
296
+ * @param internalFieldStore The field store to focus.
297
+ *
298
+ * @returns Whether an element was focused.
299
+ */
300
+ function focusFieldElement(internalFieldStore) {
301
+ for (const element of internalFieldStore.elements) {
302
+ element.focus();
303
+ if (element.getRootNode().activeElement === element) return true;
304
+ }
305
+ return false;
306
+ }
307
+
308
+ //#endregion
309
+ //#region src/field/walkFieldStore/walkFieldStore.ts
310
+ /**
311
+ * Walks through the field store and all nested children, calling the callback
312
+ * for each field store in depth-first order. The callback may return `true` to
313
+ * stop the walk early, in which case `walkFieldStore` returns `true` as well.
314
+ *
315
+ * The walk reads array `items` reactively, so a reactive caller subscribes to
316
+ * structural changes naturally. Imperative callers that must not subscribe
317
+ * (e.g. when invoked inside an effect) should wrap the call in `untrack`.
318
+ *
319
+ * @param internalFieldStore The field store to walk.
320
+ * @param callback The callback to invoke for each field store. Return `true` to stop the walk early.
321
+ *
322
+ * @returns Whether the walk was stopped early by the callback.
323
+ */
324
+ function walkFieldStore(internalFieldStore, callback) {
325
+ if (callback(internalFieldStore)) return true;
326
+ if (internalFieldStore.kind === "array") {
327
+ for (let index = 0; index < internalFieldStore.items.value.length; index++) if (walkFieldStore(internalFieldStore.children[index], callback)) return true;
328
+ } else if (internalFieldStore.kind === "object") {
329
+ for (const key in internalFieldStore.children) if (walkFieldStore(internalFieldStore.children[key], callback)) return true;
330
+ }
331
+ return false;
332
+ }
333
+
276
334
  //#endregion
277
335
  //#region src/field/getFieldBool/getFieldBool.ts
278
336
  /**
@@ -286,16 +344,7 @@ function swapItemState(firstInternalFieldStore, secondInternalFieldStore) {
286
344
  */
287
345
  /* @__NO_SIDE_EFFECTS__ */
288
346
  function getFieldBool(internalFieldStore, type) {
289
- if (internalFieldStore[type].value) return true;
290
- if (internalFieldStore.kind === "array") {
291
- for (let index = 0; index < internalFieldStore.items.value.length; index++) if (/* @__PURE__ */ getFieldBool(internalFieldStore.children[index], type)) return true;
292
- return false;
293
- }
294
- if (internalFieldStore.kind == "object") {
295
- for (const key in internalFieldStore.children) if (/* @__PURE__ */ getFieldBool(internalFieldStore.children[key], type)) return true;
296
- return false;
297
- }
298
- return false;
347
+ return walkFieldStore(internalFieldStore, (internalFieldStore$1) => Boolean(internalFieldStore$1[type].value));
299
348
  }
300
349
 
301
350
  //#endregion
@@ -428,11 +477,11 @@ function getFieldStore(internalFormStore, path) {
428
477
  */
429
478
  function setFieldBool(internalFieldStore, type, bool) {
430
479
  batch(() => {
431
- if (internalFieldStore.kind === "array") {
432
- internalFieldStore[type].value = bool;
433
- for (let index = 0; index < untrack(() => internalFieldStore.items.value).length; index++) setFieldBool(internalFieldStore.children[index], type, bool);
434
- } else if (internalFieldStore.kind == "object") for (const key in internalFieldStore.children) setFieldBool(internalFieldStore.children[key], type, bool);
435
- else internalFieldStore[type].value = bool;
480
+ untrack(() => {
481
+ walkFieldStore(internalFieldStore, (internalFieldStore$1) => {
482
+ internalFieldStore$1[type].value = bool;
483
+ });
484
+ });
436
485
  });
437
486
  }
438
487
 
@@ -447,23 +496,21 @@ function setFieldBool(internalFieldStore, type, bool) {
447
496
  */
448
497
  function setNestedInput(internalFieldStore, input) {
449
498
  internalFieldStore.isTouched.value = true;
499
+ internalFieldStore.isEdited.value = true;
450
500
  if (internalFieldStore.kind === "array") {
451
501
  const arrayInput = input ?? [];
452
502
  const items = internalFieldStore.items.value;
453
- if (arrayInput.length < items.length) internalFieldStore.items.value = items.slice(0, arrayInput.length);
454
- else if (arrayInput.length > items.length) {
455
- if (arrayInput.length > internalFieldStore.children.length) {
456
- const path = JSON.parse(internalFieldStore.name);
457
- for (let index = internalFieldStore.children.length; index < arrayInput.length; index++) {
458
- internalFieldStore.children[index] = {};
459
- path.push(index);
460
- initializeFieldStore(internalFieldStore.children[index], internalFieldStore.schema.item, arrayInput[index], path);
461
- path.pop();
462
- }
503
+ const length = internalFieldStore.schema.type === "array" ? arrayInput.length : internalFieldStore.children.length;
504
+ if (length < items.length) internalFieldStore.items.value = items.slice(0, length);
505
+ else if (length > items.length) {
506
+ for (let index = items.length; index < length; index++) if (internalFieldStore.children[index]) resetItemState(internalFieldStore.children[index], arrayInput[index], true);
507
+ else {
508
+ internalFieldStore.children[index] = {};
509
+ initializeFieldStore(internalFieldStore.children[index], internalFieldStore.schema.item, arrayInput[index], [...internalFieldStore.path, index]);
463
510
  }
464
- internalFieldStore.items.value = [...items, ...arrayInput.slice(items.length).map(createId)];
511
+ internalFieldStore.items.value = [...items, ...Array.from({ length: length - items.length }, createId)];
465
512
  }
466
- for (let index = 0; index < arrayInput.length; index++) setNestedInput(internalFieldStore.children[index], arrayInput[index]);
513
+ for (let index = 0; index < length; index++) setNestedInput(internalFieldStore.children[index], arrayInput[index]);
467
514
  internalFieldStore.input.value = input == null ? input : true;
468
515
  internalFieldStore.isDirty.value = internalFieldStore.startInput.value !== internalFieldStore.input.value || internalFieldStore.startItems.value.length !== internalFieldStore.items.value.length;
469
516
  } else if (internalFieldStore.kind === "object") {
@@ -510,41 +557,22 @@ function setFieldInput(internalFormStore, path, input) {
510
557
  function setInitialFieldInput(internalFieldStore, initialInput) {
511
558
  batch(() => {
512
559
  if (internalFieldStore.kind === "array") {
513
- internalFieldStore.input.value = initialInput == null ? initialInput : true;
560
+ internalFieldStore.initialInput.value = initialInput == null ? initialInput : true;
514
561
  const initialArrayInput = initialInput ?? [];
515
- if (initialArrayInput.length > internalFieldStore.children.length) {
516
- const path = JSON.parse(internalFieldStore.name);
517
- for (let index = internalFieldStore.children.length; index < initialArrayInput.length; index++) {
518
- internalFieldStore.children[index] = {};
519
- path.push(index);
520
- initializeFieldStore(internalFieldStore.children[index], internalFieldStore.schema.item, initialArrayInput[index], path);
521
- path.pop();
522
- }
562
+ const length = internalFieldStore.schema.type === "array" ? initialArrayInput.length : internalFieldStore.children.length;
563
+ if (length > internalFieldStore.children.length) for (let index = internalFieldStore.children.length; index < length; index++) {
564
+ internalFieldStore.children[index] = {};
565
+ initializeFieldStore(internalFieldStore.children[index], internalFieldStore.schema.item, initialArrayInput[index], [...internalFieldStore.path, index]);
523
566
  }
524
- internalFieldStore.initialItems.value = initialArrayInput.map(createId);
567
+ internalFieldStore.initialItems.value = Array.from({ length }, createId);
525
568
  for (let index = 0; index < internalFieldStore.children.length; index++) setInitialFieldInput(internalFieldStore.children[index], initialArrayInput[index]);
526
569
  } else if (internalFieldStore.kind === "object") {
527
- internalFieldStore.input.value = initialInput == null ? initialInput : true;
570
+ internalFieldStore.initialInput.value = initialInput == null ? initialInput : true;
528
571
  for (const key in internalFieldStore.children) setInitialFieldInput(internalFieldStore.children[key], initialInput?.[key]);
529
572
  } else internalFieldStore.initialInput.value = initialInput;
530
573
  });
531
574
  }
532
575
 
533
- //#endregion
534
- //#region src/field/walkFieldStore/walkFieldStore.ts
535
- /**
536
- * Walks through the field store and all nested children, calling the callback
537
- * for each field store in depth-first order.
538
- *
539
- * @param internalFieldStore The field store to walk.
540
- * @param callback The callback to invoke for each field store.
541
- */
542
- function walkFieldStore(internalFieldStore, callback) {
543
- callback(internalFieldStore);
544
- if (internalFieldStore.kind === "array") for (let index = 0; index < untrack(() => internalFieldStore.items.value).length; index++) walkFieldStore(internalFieldStore.children[index], callback);
545
- else if (internalFieldStore.kind === "object") for (const key in internalFieldStore.children) walkFieldStore(internalFieldStore.children[key], callback);
546
- }
547
-
548
576
  //#endregion
549
577
  //#region src/form/createFormStore/createFormStore.ts
550
578
  /**
@@ -570,6 +598,203 @@ function createFormStore(config, parse) {
570
598
  return store;
571
599
  }
572
600
 
601
+ //#endregion
602
+ //#region src/form/decodeFormData/decodeFormData.ts
603
+ const NUMBER_REGEX = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/u;
604
+ const ISO_DATE_TIME_REGEX = /^\d{4}-(?:0[1-9]|1[0-2])-(?:[12]\d|0[1-9]|3[01])T(?:0\d|1\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?$/u;
605
+ const MAX_ARRAY_LENGTH = 5e3;
606
+ /**
607
+ * Unwraps wrapper and lazy schemas until a concrete schema is reached.
608
+ *
609
+ * @param schema The schema to unwrap.
610
+ *
611
+ * @returns The unwrapped schema.
612
+ */
613
+ function unwrapSchema(schema) {
614
+ switch (schema.type) {
615
+ case "exact_optional":
616
+ case "nullable":
617
+ case "nullish":
618
+ case "optional":
619
+ case "undefinedable":
620
+ case "non_nullable":
621
+ case "non_nullish":
622
+ case "non_optional": return unwrapSchema(schema.wrapped);
623
+ case "lazy": return unwrapSchema(schema.getter(void 0));
624
+ default: return schema;
625
+ }
626
+ }
627
+ /**
628
+ * Returns the child schema for the given key by traversing objects, arrays,
629
+ * tuples and schema options. Returns `undefined` if no child schema is found.
630
+ *
631
+ * @param schema The parent schema.
632
+ * @param key The path key.
633
+ *
634
+ * @returns The child schema or `undefined`.
635
+ */
636
+ function getChildSchema(schema, key) {
637
+ if (schema) {
638
+ const unwrapped = unwrapSchema(schema);
639
+ if (unwrapped.type === "object" || unwrapped.type === "loose_object" || unwrapped.type === "strict_object") return unwrapped.entries[key];
640
+ if (unwrapped.type === "array") return unwrapped.item;
641
+ if (unwrapped.type === "tuple" || unwrapped.type === "loose_tuple" || unwrapped.type === "strict_tuple") return unwrapped.items[key];
642
+ if (unwrapped.type === "union" || unwrapped.type === "intersect" || unwrapped.type === "variant") for (const option of unwrapped.options) {
643
+ const childSchema = getChildSchema(option, key);
644
+ if (childSchema !== void 0) return childSchema;
645
+ }
646
+ }
647
+ }
648
+ /**
649
+ * Decodes a stringified date based on its format. Empty strings become `null`.
650
+ *
651
+ * @param value The stringified value.
652
+ *
653
+ * @returns The decoded date.
654
+ */
655
+ function decodeDate(value) {
656
+ if (!value || value === "null") return null;
657
+ if (value === "undefined") return;
658
+ if (ISO_DATE_TIME_REGEX.test(value)) return /* @__PURE__ */ new Date(`${value}Z`);
659
+ return new Date(value);
660
+ }
661
+ /**
662
+ * Decodes a stringified boolean. Empty strings become `null`.
663
+ *
664
+ * @param value The stringified value.
665
+ *
666
+ * @returns The decoded boolean.
667
+ */
668
+ function decodeBoolean(value) {
669
+ if (!value || value === "null") return null;
670
+ if (value === "undefined") return;
671
+ return !(value === "false" || value === "off" || value === "0");
672
+ }
673
+ /**
674
+ * Decodes a stringified number. Empty strings become `null` and non-numeric
675
+ * values become `NaN`.
676
+ *
677
+ * @param value The stringified value.
678
+ *
679
+ * @returns The decoded number.
680
+ */
681
+ function decodeNumber(value) {
682
+ if (!value || value === "null") return null;
683
+ if (value === "undefined") return;
684
+ if (NUMBER_REGEX.test(value)) return Number(value);
685
+ return NaN;
686
+ }
687
+ /**
688
+ * Decodes a stringified bigint. Empty strings become `null` and invalid values
689
+ * are returned unchanged.
690
+ *
691
+ * @param value The stringified value.
692
+ *
693
+ * @returns The decoded bigint.
694
+ */
695
+ function decodeBigint(value) {
696
+ if (!value || value === "null") return null;
697
+ if (value === "undefined") return;
698
+ try {
699
+ return BigInt(value);
700
+ } catch {
701
+ return value;
702
+ }
703
+ }
704
+ /**
705
+ * Decodes a single form data value based on the concrete schema type. Files
706
+ * and unknown types are returned unchanged.
707
+ *
708
+ * @param value The form data value.
709
+ * @param schema The schema of the value.
710
+ *
711
+ * @returns The decoded value.
712
+ */
713
+ function decodeValue(value, schema) {
714
+ if (typeof value !== "string" || !schema) return value;
715
+ switch (unwrapSchema(schema).type) {
716
+ case "number": return decodeNumber(value);
717
+ case "boolean": return decodeBoolean(value);
718
+ case "date": return decodeDate(value);
719
+ case "bigint": return decodeBigint(value);
720
+ default: return value;
721
+ }
722
+ }
723
+ /**
724
+ * Fills in default values that are lost during the form data transfer. Booleans
725
+ * of unchecked checkboxes become `false` and absent arrays become empty. Only
726
+ * containers that are present in the decoded data are completed.
727
+ *
728
+ * @param schema The schema of the value.
729
+ * @param parent The parent object or array holding the value.
730
+ * @param key The key of the value within its parent.
731
+ */
732
+ function fillDefaults(schema, parent, key) {
733
+ const unwrappedSchema = unwrapSchema(schema);
734
+ if (unwrappedSchema.type === "boolean") {
735
+ if (parent[key] === void 0) parent[key] = false;
736
+ } else if (unwrappedSchema.type === "array") if (Array.isArray(parent[key])) for (let index = 0; index < parent[key].length; index++) fillDefaults(unwrappedSchema.item, parent[key], index);
737
+ else parent[key] = [];
738
+ else if (unwrappedSchema.type === "tuple" || unwrappedSchema.type === "loose_tuple" || unwrappedSchema.type === "strict_tuple") {
739
+ if (Array.isArray(parent[key])) for (let index = 0; index < unwrappedSchema.items.length; index++) fillDefaults(unwrappedSchema.items[index], parent[key], index);
740
+ } else if (unwrappedSchema.type === "object" || unwrappedSchema.type === "loose_object" || unwrappedSchema.type === "strict_object") {
741
+ if (parent[key] && typeof parent[key] === "object") for (const entryKey in unwrappedSchema.entries) fillDefaults(unwrappedSchema.entries[entryKey], parent[key], entryKey);
742
+ } else if (unwrappedSchema.type === "union" || unwrappedSchema.type === "intersect" || unwrappedSchema.type === "variant") for (const option of unwrappedSchema.options) fillDefaults(option, parent, key);
743
+ }
744
+ /**
745
+ * Decodes the entries of a form data object into nested form values using the
746
+ * Valibot schema as the source of truth. Information that is lost during the
747
+ * transfer via HTTP, like numbers, booleans, dates and unchecked checkboxes,
748
+ * is restored based on the schema.
749
+ *
750
+ * The keys of the form data are expected to be the stringified field paths that
751
+ * Formisch assigns to its field elements (for example `["todos",0,"label"]`).
752
+ *
753
+ * @param schema The form schema.
754
+ * @param formData The form data object.
755
+ *
756
+ * @returns The decoded form values.
757
+ */
758
+ /* @__NO_SIDE_EFFECTS__ */
759
+ function decodeFormData(schema, formData) {
760
+ const values = {};
761
+ formData.forEach((value, key) => {
762
+ let path = null;
763
+ try {
764
+ path = JSON.parse(key);
765
+ } catch {}
766
+ if (Array.isArray(path) && path.length > 0 && (typeof value === "string" || value.size > 0 || value.name !== "")) {
767
+ let parentValue = values;
768
+ let parentSchema = schema;
769
+ for (let index = 0; index < path.length; index++) {
770
+ const segment = path[index];
771
+ if (typeof segment !== "string" && typeof segment !== "number" || segment === "" || segment === "__proto__" || segment === "prototype" || segment === "constructor") break;
772
+ if (Array.isArray(parentValue)) {
773
+ if (typeof segment === "string") break;
774
+ if (segment >= MAX_ARRAY_LENGTH) throw new Error(`Array exceeds the maximum length of ${MAX_ARRAY_LENGTH}`);
775
+ }
776
+ const childSchema = getChildSchema(parentSchema, segment);
777
+ if (index === path.length - 1) {
778
+ const unwrappedSchema = childSchema && unwrapSchema(childSchema);
779
+ if (unwrappedSchema && unwrappedSchema.type === "array") {
780
+ parentValue[segment] ??= [];
781
+ parentValue[segment].push(decodeValue(value, unwrappedSchema.item));
782
+ } else parentValue[segment] = decodeValue(value, childSchema);
783
+ } else {
784
+ if (parentValue[segment] == null) {
785
+ const schemaType = childSchema && unwrapSchema(childSchema).type;
786
+ parentValue[segment] = schemaType === "array" || schemaType === "tuple" || schemaType === "loose_tuple" || schemaType === "strict_tuple" ? [] : {};
787
+ } else if (typeof parentValue[segment] !== "object") break;
788
+ parentValue = parentValue[segment];
789
+ parentSchema = childSchema;
790
+ }
791
+ }
792
+ }
793
+ });
794
+ fillDefaults(schema, { values }, "values");
795
+ return values;
796
+ }
797
+
573
798
  //#endregion
574
799
  //#region src/form/validateFormInput/validateFormInput.ts
575
800
  /**
@@ -585,44 +810,51 @@ function createFormStore(config, parse) {
585
810
  async function validateFormInput(internalFormStore, config) {
586
811
  internalFormStore.validators++;
587
812
  internalFormStore.isValidating.value = true;
588
- const result = await internalFormStore.parse(untrack(() => /* @__PURE__ */ getFieldInput(internalFormStore)));
589
- let rootErrors;
590
- let nestedErrors;
591
- if (result.issues) {
592
- nestedErrors = {};
593
- for (const issue of result.issues) if (issue.path) {
594
- const path = [];
595
- for (const pathItem of issue.path) {
596
- const key = pathItem.key;
597
- const keyType = typeof key;
598
- const itemType = pathItem.type;
599
- if (keyType !== "string" && keyType !== "number" || itemType === "map" || itemType === "set") break;
600
- path.push(key);
601
- }
602
- const name = JSON.stringify(path);
603
- const fieldErrors = nestedErrors[name];
604
- if (fieldErrors) fieldErrors.push(issue.message);
605
- else nestedErrors[name] = [issue.message];
606
- } else if (rootErrors) rootErrors.push(issue.message);
607
- else rootErrors = [issue.message];
608
- }
609
- let shouldFocus = config?.shouldFocus ?? false;
610
- batch(() => {
611
- walkFieldStore(internalFormStore, (internalFieldStore) => {
612
- if (internalFieldStore.name === "[]") internalFieldStore.errors.value = rootErrors ?? null;
613
- else {
614
- const fieldErrors = nestedErrors?.[internalFieldStore.name] ?? null;
615
- internalFieldStore.errors.value = fieldErrors;
616
- if (shouldFocus && fieldErrors) {
617
- internalFieldStore.elements[0]?.focus();
618
- shouldFocus = false;
813
+ try {
814
+ const result = await internalFormStore.parse(untrack(() => /* @__PURE__ */ getFieldInput(internalFormStore)));
815
+ let rootErrors;
816
+ let nestedErrors;
817
+ if (result.issues) {
818
+ nestedErrors = {};
819
+ for (const issue of result.issues) if (issue.path) {
820
+ const path = [];
821
+ for (const pathItem of issue.path) {
822
+ const key = pathItem.key;
823
+ const keyType = typeof key;
824
+ const itemType = pathItem.type;
825
+ if (keyType !== "string" && keyType !== "number" || itemType === "map" || itemType === "set") break;
826
+ path.push(key);
619
827
  }
620
- }
828
+ const name = JSON.stringify(path);
829
+ const fieldErrors = nestedErrors[name];
830
+ if (fieldErrors) fieldErrors.push(issue.message);
831
+ else nestedErrors[name] = [issue.message];
832
+ } else if (rootErrors) rootErrors.push(issue.message);
833
+ else rootErrors = [issue.message];
834
+ }
835
+ let shouldFocus = config?.shouldFocus ?? false;
836
+ batch(() => {
837
+ untrack(() => {
838
+ walkFieldStore(internalFormStore, (internalFieldStore) => {
839
+ if (internalFieldStore.path.length === 0) internalFieldStore.errors.value = rootErrors ?? null;
840
+ else {
841
+ const fieldErrors = nestedErrors?.[internalFieldStore.name] ?? null;
842
+ internalFieldStore.errors.value = fieldErrors;
843
+ if (shouldFocus && fieldErrors && focusFieldElement(internalFieldStore)) shouldFocus = false;
844
+ }
845
+ });
846
+ });
847
+ internalFormStore.validators--;
848
+ internalFormStore.isValidating.value = internalFormStore.validators > 0;
621
849
  });
622
- internalFormStore.validators--;
623
- internalFormStore.isValidating.value = internalFormStore.validators > 0;
624
- });
625
- return result;
850
+ return result;
851
+ } catch (error) {
852
+ batch(() => {
853
+ internalFormStore.validators--;
854
+ internalFormStore.isValidating.value = internalFormStore.validators > 0;
855
+ });
856
+ throw error;
857
+ }
626
858
  }
627
859
 
628
860
  //#endregion
@@ -648,4 +880,4 @@ function validateIfRequired(internalFormStore, internalFieldStore, validationMod
648
880
  const INTERNAL = "~internal";
649
881
 
650
882
  //#endregion
651
- export { INTERNAL, batch, copyItemState, createFormStore, createId, createSignal, framework, getDirtyFieldInput, getElementInput, getFieldBool, getFieldInput, getFieldStore, initializeFieldStore, resetItemState, setFieldBool, setFieldInput, setInitialFieldInput, swapItemState, untrack, validateFormInput, validateIfRequired, walkFieldStore };
883
+ export { INTERNAL, batch, copyItemState, createFormStore, createId, createSignal, decodeFormData, focusFieldElement, framework, getDirtyFieldInput, getElementInput, getFieldBool, getFieldInput, getFieldStore, initializeFieldStore, resetItemState, setFieldBool, setFieldInput, setInitialFieldInput, swapItemState, untrack, validateFormInput, validateIfRequired, walkFieldStore };