@formisch/preact 0.10.0 → 0.12.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.
package/dist/index.js CHANGED
@@ -37,11 +37,13 @@ function initializeFieldStore(internalFieldStore, schema, initialInput, path, nu
37
37
  else {
38
38
  internalFieldStore.schema = schema;
39
39
  internalFieldStore.name = JSON.stringify(path);
40
+ internalFieldStore.path = path;
40
41
  const initialElements = [];
41
42
  internalFieldStore.initialElements = initialElements;
42
43
  internalFieldStore.elements = initialElements;
43
44
  internalFieldStore.errors = createSignal(null);
44
45
  internalFieldStore.isTouched = createSignal(false);
46
+ internalFieldStore.isEdited = createSignal(false);
45
47
  internalFieldStore.isDirty = createSignal(false);
46
48
  if (schema.type === "array" || schema.type === "loose_tuple" || schema.type === "strict_tuple" || schema.type === "tuple") {
47
49
  if (internalFieldStore.kind && internalFieldStore.kind !== "array") throw new Error(`Store initialized as "${internalFieldStore.kind}" cannot be reinitialized as "array"`);
@@ -51,16 +53,13 @@ function initializeFieldStore(internalFieldStore, schema, initialInput, path, nu
51
53
  if (schema.type === "array") {
52
54
  if (initialInput) for (let index = 0; index < initialInput.length; index++) {
53
55
  internalFieldStore.children[index] = {};
54
- path.push(index);
55
- initializeFieldStore(internalFieldStore.children[index], schema.item, initialInput[index], path);
56
- path.pop();
56
+ initializeFieldStore(internalFieldStore.children[index], schema.item, initialInput[index], [...path, index]);
57
57
  }
58
58
  } else for (let index = 0; index < schema.items.length; index++) {
59
59
  internalFieldStore.children[index] = {};
60
- path.push(index);
61
- initializeFieldStore(internalFieldStore.children[index], schema.items[index], initialInput?.[index], path);
62
- path.pop();
60
+ initializeFieldStore(internalFieldStore.children[index], schema.items[index], initialInput?.[index], [...path, index]);
63
61
  }
62
+ internalFieldStore.isNullish = nullish;
64
63
  const arrayInput = nullish && initialInput == null ? initialInput : true;
65
64
  internalFieldStore.initialInput = createSignal(arrayInput);
66
65
  internalFieldStore.startInput = createSignal(arrayInput);
@@ -77,10 +76,9 @@ function initializeFieldStore(internalFieldStore, schema, initialInput, path, nu
77
76
  internalFieldStore.children ??= {};
78
77
  for (const key in schema.entries) {
79
78
  internalFieldStore.children[key] ??= {};
80
- path.push(key);
81
- initializeFieldStore(internalFieldStore.children[key], schema.entries[key], initialInput?.[key], path);
82
- path.pop();
79
+ initializeFieldStore(internalFieldStore.children[key], schema.entries[key], initialInput?.[key], [...path, key]);
83
80
  }
81
+ internalFieldStore.isNullish = nullish;
84
82
  const objectInput = nullish && initialInput == null ? initialInput : true;
85
83
  internalFieldStore.initialInput = createSignal(objectInput);
86
84
  internalFieldStore.startInput = createSignal(objectInput);
@@ -100,8 +98,9 @@ function initializeFieldStore(internalFieldStore, schema, initialInput, path, nu
100
98
  /**
101
99
  * Copies the deeply nested state (signal values) from one field store to
102
100
  * another. This includes the `elements`, `errors`, `startInput`, `input`,
103
- * `isTouched`, `isDirty`, and for arrays `startItems` and `items` properties.
104
- * Recursively walks through the field stores and copies all signal values.
101
+ * `isTouched`, `isEdited`, `isDirty`, and for arrays `startItems` and `items`
102
+ * properties. Recursively walks through the field stores and copies all signal
103
+ * values.
105
104
  *
106
105
  * @param fromInternalFieldStore The source field store to copy from.
107
106
  * @param toInternalFieldStore The destination field store to copy to.
@@ -114,19 +113,16 @@ function copyItemState(fromInternalFieldStore, toInternalFieldStore) {
114
113
  toInternalFieldStore.startInput.value = fromInternalFieldStore.startInput.value;
115
114
  toInternalFieldStore.input.value = fromInternalFieldStore.input.value;
116
115
  toInternalFieldStore.isTouched.value = fromInternalFieldStore.isTouched.value;
116
+ toInternalFieldStore.isEdited.value = fromInternalFieldStore.isEdited.value;
117
117
  toInternalFieldStore.isDirty.value = fromInternalFieldStore.isDirty.value;
118
118
  if (fromInternalFieldStore.kind === "array" && toInternalFieldStore.kind === "array") {
119
119
  const fromItems = fromInternalFieldStore.items.value;
120
120
  toInternalFieldStore.startItems.value = fromInternalFieldStore.startItems.value;
121
121
  toInternalFieldStore.items.value = fromItems;
122
- let path;
123
122
  for (let index = 0; index < fromItems.length; index++) {
124
123
  if (!toInternalFieldStore.children[index]) {
125
- path ??= JSON.parse(toInternalFieldStore.name);
126
124
  toInternalFieldStore.children[index] = {};
127
- path.push(index);
128
- initializeFieldStore(toInternalFieldStore.children[index], toInternalFieldStore.schema.item, void 0, path);
129
- path.pop();
125
+ initializeFieldStore(toInternalFieldStore.children[index], toInternalFieldStore.schema.item, void 0, [...toInternalFieldStore.path, index]);
130
126
  }
131
127
  copyItemState(fromInternalFieldStore.children[index], toInternalFieldStore.children[index]);
132
128
  }
@@ -136,45 +132,61 @@ function copyItemState(fromInternalFieldStore, toInternalFieldStore) {
136
132
  }
137
133
  /**
138
134
  * Resets the state of a field store (signal values) deeply nested. Sets
139
- * `elements` to empty array, `errors` to `null`, `isTouched` and `isDirty` to
140
- * `false`, and `startInput`, `input`, `startItems`, and `items` to the new
141
- * input value. Keeps the `initialInput` and `initialItems` state unchanged for
142
- * form reset functionality.
135
+ * `elements` to empty array, `errors` to `null`, `isTouched`, `isEdited` and
136
+ * `isDirty` to `false`, and `startInput`, `input`, `startItems`, and `items` to
137
+ * the new input value. Keeps the `initialInput` and `initialItems` state
138
+ * unchanged for form reset functionality.
143
139
  *
144
140
  * @param internalFieldStore The field store to reset.
145
- * @param initialInput The new input value (can be any type including array or object).
141
+ * @param input The new input value (can be any type including array or object).
142
+ * @param keepStart Whether to keep `startInput` and `startItems` as the dirty
143
+ * baseline instead of resetting them to the new input. Used when a field store
144
+ * is reused for an in-place edit so its dirty state is detected correctly.
146
145
  */
147
- function resetItemState(internalFieldStore, initialInput) {
146
+ function resetItemState(internalFieldStore, input, keepStart = false) {
148
147
  batch(() => {
149
- internalFieldStore.elements = [];
148
+ const elements = [];
149
+ if (internalFieldStore.elements === internalFieldStore.initialElements) internalFieldStore.initialElements = elements;
150
+ internalFieldStore.elements = elements;
150
151
  internalFieldStore.errors.value = null;
151
152
  internalFieldStore.isTouched.value = false;
153
+ internalFieldStore.isEdited.value = false;
152
154
  internalFieldStore.isDirty.value = false;
153
155
  if (internalFieldStore.kind === "array" || internalFieldStore.kind === "object") {
154
- const objectInput = initialInput == null ? initialInput : true;
155
- internalFieldStore.startInput.value = objectInput;
156
+ const objectInput = internalFieldStore.isNullish && input == null ? input : true;
157
+ if (!keepStart) internalFieldStore.startInput.value = objectInput;
156
158
  internalFieldStore.input.value = objectInput;
157
- if (internalFieldStore.kind === "array") if (initialInput) {
158
- const newItems = initialInput.map(createId);
159
- internalFieldStore.startItems.value = newItems;
160
- internalFieldStore.items.value = newItems;
161
- for (let index = 0; index < initialInput.length; index++) if (internalFieldStore.children[index]) resetItemState(internalFieldStore.children[index], initialInput[index]);
162
- } else {
163
- internalFieldStore.startItems.value = [];
164
- internalFieldStore.items.value = [];
165
- }
166
- else for (const key in internalFieldStore.children) resetItemState(internalFieldStore.children[key], initialInput?.[key]);
159
+ if (internalFieldStore.kind === "array") {
160
+ const isTuple = internalFieldStore.schema.type !== "array";
161
+ if (input || isTuple) {
162
+ const length = isTuple ? internalFieldStore.children.length : input.length;
163
+ const newItems = Array.from({ length }, createId);
164
+ if (!keepStart) internalFieldStore.startItems.value = newItems;
165
+ internalFieldStore.items.value = newItems;
166
+ for (let index = 0; index < length; index++) {
167
+ const itemInput = input?.[index];
168
+ if (internalFieldStore.children[index]) resetItemState(internalFieldStore.children[index], itemInput, keepStart);
169
+ else {
170
+ internalFieldStore.children[index] = {};
171
+ initializeFieldStore(internalFieldStore.children[index], internalFieldStore.schema.item, itemInput, [...internalFieldStore.path, index]);
172
+ }
173
+ }
174
+ } else {
175
+ if (!keepStart) internalFieldStore.startItems.value = [];
176
+ internalFieldStore.items.value = [];
177
+ }
178
+ } else for (const key in internalFieldStore.children) resetItemState(internalFieldStore.children[key], input?.[key], keepStart);
167
179
  } else {
168
- internalFieldStore.startInput.value = initialInput;
169
- internalFieldStore.input.value = initialInput;
180
+ if (!keepStart) internalFieldStore.startInput.value = input;
181
+ internalFieldStore.input.value = input;
170
182
  }
171
183
  });
172
184
  }
173
185
  /**
174
186
  * Swaps the deeply nested state (signal values) between two field stores. This
175
187
  * includes the `elements`, `errors`, `startInput`, `input`, `isTouched`,
176
- * `isDirty`, and for arrays `startItems` and `items` properties. Recursively
177
- * walks through the field stores and swaps all signal values.
188
+ * `isEdited`, `isDirty`, and for arrays `startItems` and `items` properties.
189
+ * Recursively walks through the field stores and swaps all signal values.
178
190
  *
179
191
  * @param firstInternalFieldStore The first field store to swap.
180
192
  * @param secondInternalFieldStore The second field store to swap.
@@ -197,6 +209,9 @@ function swapItemState(firstInternalFieldStore, secondInternalFieldStore) {
197
209
  const tempIsTouched = firstInternalFieldStore.isTouched.value;
198
210
  firstInternalFieldStore.isTouched.value = secondInternalFieldStore.isTouched.value;
199
211
  secondInternalFieldStore.isTouched.value = tempIsTouched;
212
+ const tempIsEdited = firstInternalFieldStore.isEdited.value;
213
+ firstInternalFieldStore.isEdited.value = secondInternalFieldStore.isEdited.value;
214
+ secondInternalFieldStore.isEdited.value = tempIsEdited;
200
215
  const tempIsDirty = firstInternalFieldStore.isDirty.value;
201
216
  firstInternalFieldStore.isDirty.value = secondInternalFieldStore.isDirty.value;
202
217
  secondInternalFieldStore.isDirty.value = tempIsDirty;
@@ -209,22 +224,14 @@ function swapItemState(firstInternalFieldStore, secondInternalFieldStore) {
209
224
  firstInternalFieldStore.items.value = secondItems;
210
225
  secondInternalFieldStore.items.value = firstItems;
211
226
  const maxLength = Math.max(firstItems.length, secondItems.length);
212
- let firstPath;
213
- let secondPath;
214
227
  for (let index = 0; index < maxLength; index++) {
215
228
  if (!firstInternalFieldStore.children[index]) {
216
- firstPath ??= JSON.parse(firstInternalFieldStore.name);
217
229
  firstInternalFieldStore.children[index] = {};
218
- firstPath.push(index);
219
- initializeFieldStore(firstInternalFieldStore.children[index], firstInternalFieldStore.schema.item, void 0, firstPath);
220
- firstPath.pop();
230
+ initializeFieldStore(firstInternalFieldStore.children[index], firstInternalFieldStore.schema.item, void 0, [...firstInternalFieldStore.path, index]);
221
231
  }
222
232
  if (!secondInternalFieldStore.children[index]) {
223
- secondPath ??= JSON.parse(secondInternalFieldStore.name);
224
233
  secondInternalFieldStore.children[index] = {};
225
- secondPath.push(index);
226
- initializeFieldStore(secondInternalFieldStore.children[index], secondInternalFieldStore.schema.item, void 0, secondPath);
227
- secondPath.pop();
234
+ initializeFieldStore(secondInternalFieldStore.children[index], secondInternalFieldStore.schema.item, void 0, [...secondInternalFieldStore.path, index]);
228
235
  }
229
236
  swapItemState(firstInternalFieldStore.children[index], secondInternalFieldStore.children[index]);
230
237
  }
@@ -233,6 +240,51 @@ function swapItemState(firstInternalFieldStore, secondInternalFieldStore) {
233
240
  });
234
241
  }
235
242
  /**
243
+ * Focuses the first focusable element of a field store. The elements are tried
244
+ * in order and the first one that actually receives focus wins, so detached,
245
+ * disabled or hidden elements are skipped. The browser decides focusability,
246
+ * which is read back via the element's root `activeElement` so elements in a
247
+ * shadow root or another document are handled correctly.
248
+ *
249
+ * Hint: A `display: none` or `hidden` element is correctly skipped in real
250
+ * browsers, but jsdom has no layout and focuses it anyway, so that case cannot
251
+ * be covered by unit tests.
252
+ *
253
+ * @param internalFieldStore The field store to focus.
254
+ *
255
+ * @returns Whether an element was focused.
256
+ */
257
+ function focusFieldElement(internalFieldStore) {
258
+ for (const element of internalFieldStore.elements) {
259
+ element.focus();
260
+ if (element.getRootNode().activeElement === element) return true;
261
+ }
262
+ return false;
263
+ }
264
+ /**
265
+ * Walks through the field store and all nested children, calling the callback
266
+ * for each field store in depth-first order. The callback may return `true` to
267
+ * stop the walk early, in which case `walkFieldStore` returns `true` as well.
268
+ *
269
+ * The walk reads array `items` reactively, so a reactive caller subscribes to
270
+ * structural changes naturally. Imperative callers that must not subscribe
271
+ * (e.g. when invoked inside an effect) should wrap the call in `untrack`.
272
+ *
273
+ * @param internalFieldStore The field store to walk.
274
+ * @param callback The callback to invoke for each field store. Return `true` to stop the walk early.
275
+ *
276
+ * @returns Whether the walk was stopped early by the callback.
277
+ */
278
+ function walkFieldStore(internalFieldStore, callback) {
279
+ if (callback(internalFieldStore)) return true;
280
+ if (internalFieldStore.kind === "array") {
281
+ for (let index = 0; index < internalFieldStore.items.value.length; index++) if (walkFieldStore(internalFieldStore.children[index], callback)) return true;
282
+ } else if (internalFieldStore.kind === "object") {
283
+ for (const key in internalFieldStore.children) if (walkFieldStore(internalFieldStore.children[key], callback)) return true;
284
+ }
285
+ return false;
286
+ }
287
+ /**
236
288
  * Returns whether the specified boolean property is true for the field store
237
289
  * or any of its nested children. Recursively checks arrays and objects.
238
290
  *
@@ -243,16 +295,7 @@ function swapItemState(firstInternalFieldStore, secondInternalFieldStore) {
243
295
  */
244
296
  /* @__NO_SIDE_EFFECTS__ */
245
297
  function getFieldBool(internalFieldStore, type) {
246
- if (internalFieldStore[type].value) return true;
247
- if (internalFieldStore.kind === "array") {
248
- for (let index = 0; index < internalFieldStore.items.value.length; index++) if (/* @__PURE__ */ getFieldBool(internalFieldStore.children[index], type)) return true;
249
- return false;
250
- }
251
- if (internalFieldStore.kind == "object") {
252
- for (const key in internalFieldStore.children) if (/* @__PURE__ */ getFieldBool(internalFieldStore.children[key], type)) return true;
253
- return false;
254
- }
255
- return false;
298
+ return walkFieldStore(internalFieldStore, (internalFieldStore$1) => Boolean(internalFieldStore$1[type].value));
256
299
  }
257
300
  /**
258
301
  * Returns only the dirty input of the field store. Arrays are treated as
@@ -370,11 +413,11 @@ function getFieldStore(internalFormStore, path) {
370
413
  */
371
414
  function setFieldBool(internalFieldStore, type, bool) {
372
415
  batch(() => {
373
- if (internalFieldStore.kind === "array") {
374
- internalFieldStore[type].value = bool;
375
- for (let index = 0; index < untrack(() => internalFieldStore.items.value).length; index++) setFieldBool(internalFieldStore.children[index], type, bool);
376
- } else if (internalFieldStore.kind == "object") for (const key in internalFieldStore.children) setFieldBool(internalFieldStore.children[key], type, bool);
377
- else internalFieldStore[type].value = bool;
416
+ untrack(() => {
417
+ walkFieldStore(internalFieldStore, (internalFieldStore$1) => {
418
+ internalFieldStore$1[type].value = bool;
419
+ });
420
+ });
378
421
  });
379
422
  }
380
423
  /**
@@ -386,23 +429,21 @@ function setFieldBool(internalFieldStore, type, bool) {
386
429
  */
387
430
  function setNestedInput(internalFieldStore, input) {
388
431
  internalFieldStore.isTouched.value = true;
432
+ internalFieldStore.isEdited.value = true;
389
433
  if (internalFieldStore.kind === "array") {
390
434
  const arrayInput = input ?? [];
391
435
  const items = internalFieldStore.items.value;
392
- if (arrayInput.length < items.length) internalFieldStore.items.value = items.slice(0, arrayInput.length);
393
- else if (arrayInput.length > items.length) {
394
- if (arrayInput.length > internalFieldStore.children.length) {
395
- const path = JSON.parse(internalFieldStore.name);
396
- for (let index = internalFieldStore.children.length; index < arrayInput.length; index++) {
397
- internalFieldStore.children[index] = {};
398
- path.push(index);
399
- initializeFieldStore(internalFieldStore.children[index], internalFieldStore.schema.item, arrayInput[index], path);
400
- path.pop();
401
- }
436
+ const length = internalFieldStore.schema.type === "array" ? arrayInput.length : internalFieldStore.children.length;
437
+ if (length < items.length) internalFieldStore.items.value = items.slice(0, length);
438
+ else if (length > items.length) {
439
+ for (let index = items.length; index < length; index++) if (internalFieldStore.children[index]) resetItemState(internalFieldStore.children[index], arrayInput[index], true);
440
+ else {
441
+ internalFieldStore.children[index] = {};
442
+ initializeFieldStore(internalFieldStore.children[index], internalFieldStore.schema.item, arrayInput[index], [...internalFieldStore.path, index]);
402
443
  }
403
- internalFieldStore.items.value = [...items, ...arrayInput.slice(items.length).map(createId)];
444
+ internalFieldStore.items.value = [...items, ...Array.from({ length: length - items.length }, createId)];
404
445
  }
405
- for (let index = 0; index < arrayInput.length; index++) setNestedInput(internalFieldStore.children[index], arrayInput[index]);
446
+ for (let index = 0; index < length; index++) setNestedInput(internalFieldStore.children[index], arrayInput[index]);
406
447
  internalFieldStore.input.value = input == null ? input : true;
407
448
  internalFieldStore.isDirty.value = internalFieldStore.startInput.value !== internalFieldStore.input.value || internalFieldStore.startItems.value.length !== internalFieldStore.items.value.length;
408
449
  } else if (internalFieldStore.kind === "object") {
@@ -446,38 +487,22 @@ function setFieldInput(internalFormStore, path, input) {
446
487
  function setInitialFieldInput(internalFieldStore, initialInput) {
447
488
  batch(() => {
448
489
  if (internalFieldStore.kind === "array") {
449
- internalFieldStore.input.value = initialInput == null ? initialInput : true;
490
+ internalFieldStore.initialInput.value = initialInput == null ? initialInput : true;
450
491
  const initialArrayInput = initialInput ?? [];
451
- if (initialArrayInput.length > internalFieldStore.children.length) {
452
- const path = JSON.parse(internalFieldStore.name);
453
- for (let index = internalFieldStore.children.length; index < initialArrayInput.length; index++) {
454
- internalFieldStore.children[index] = {};
455
- path.push(index);
456
- initializeFieldStore(internalFieldStore.children[index], internalFieldStore.schema.item, initialArrayInput[index], path);
457
- path.pop();
458
- }
492
+ const length = internalFieldStore.schema.type === "array" ? initialArrayInput.length : internalFieldStore.children.length;
493
+ if (length > internalFieldStore.children.length) for (let index = internalFieldStore.children.length; index < length; index++) {
494
+ internalFieldStore.children[index] = {};
495
+ initializeFieldStore(internalFieldStore.children[index], internalFieldStore.schema.item, initialArrayInput[index], [...internalFieldStore.path, index]);
459
496
  }
460
- internalFieldStore.initialItems.value = initialArrayInput.map(createId);
497
+ internalFieldStore.initialItems.value = Array.from({ length }, createId);
461
498
  for (let index = 0; index < internalFieldStore.children.length; index++) setInitialFieldInput(internalFieldStore.children[index], initialArrayInput[index]);
462
499
  } else if (internalFieldStore.kind === "object") {
463
- internalFieldStore.input.value = initialInput == null ? initialInput : true;
500
+ internalFieldStore.initialInput.value = initialInput == null ? initialInput : true;
464
501
  for (const key in internalFieldStore.children) setInitialFieldInput(internalFieldStore.children[key], initialInput?.[key]);
465
502
  } else internalFieldStore.initialInput.value = initialInput;
466
503
  });
467
504
  }
468
505
  /**
469
- * Walks through the field store and all nested children, calling the callback
470
- * for each field store in depth-first order.
471
- *
472
- * @param internalFieldStore The field store to walk.
473
- * @param callback The callback to invoke for each field store.
474
- */
475
- function walkFieldStore(internalFieldStore, callback) {
476
- callback(internalFieldStore);
477
- if (internalFieldStore.kind === "array") for (let index = 0; index < untrack(() => internalFieldStore.items.value).length; index++) walkFieldStore(internalFieldStore.children[index], callback);
478
- else if (internalFieldStore.kind === "object") for (const key in internalFieldStore.children) walkFieldStore(internalFieldStore.children[key], callback);
479
- }
480
- /**
481
506
  * Creates a new internal form store from the provided configuration.
482
507
  * Initializes the field store hierarchy, sets validation modes, and
483
508
  * creates form state signals.
@@ -512,44 +537,51 @@ function createFormStore(config, parse) {
512
537
  async function validateFormInput(internalFormStore, config) {
513
538
  internalFormStore.validators++;
514
539
  internalFormStore.isValidating.value = true;
515
- const result = await internalFormStore.parse(untrack(() => /* @__PURE__ */ getFieldInput(internalFormStore)));
516
- let rootErrors;
517
- let nestedErrors;
518
- if (result.issues) {
519
- nestedErrors = {};
520
- for (const issue of result.issues) if (issue.path) {
521
- const path = [];
522
- for (const pathItem of issue.path) {
523
- const key = pathItem.key;
524
- const keyType = typeof key;
525
- const itemType = pathItem.type;
526
- if (keyType !== "string" && keyType !== "number" || itemType === "map" || itemType === "set") break;
527
- path.push(key);
528
- }
529
- const name = JSON.stringify(path);
530
- const fieldErrors = nestedErrors[name];
531
- if (fieldErrors) fieldErrors.push(issue.message);
532
- else nestedErrors[name] = [issue.message];
533
- } else if (rootErrors) rootErrors.push(issue.message);
534
- else rootErrors = [issue.message];
535
- }
536
- let shouldFocus = config?.shouldFocus ?? false;
537
- batch(() => {
538
- walkFieldStore(internalFormStore, (internalFieldStore) => {
539
- if (internalFieldStore.name === "[]") internalFieldStore.errors.value = rootErrors ?? null;
540
- else {
541
- const fieldErrors = nestedErrors?.[internalFieldStore.name] ?? null;
542
- internalFieldStore.errors.value = fieldErrors;
543
- if (shouldFocus && fieldErrors) {
544
- internalFieldStore.elements[0]?.focus();
545
- shouldFocus = false;
540
+ try {
541
+ const result = await internalFormStore.parse(untrack(() => /* @__PURE__ */ getFieldInput(internalFormStore)));
542
+ let rootErrors;
543
+ let nestedErrors;
544
+ if (result.issues) {
545
+ nestedErrors = {};
546
+ for (const issue of result.issues) if (issue.path) {
547
+ const path = [];
548
+ for (const pathItem of issue.path) {
549
+ const key = pathItem.key;
550
+ const keyType = typeof key;
551
+ const itemType = pathItem.type;
552
+ if (keyType !== "string" && keyType !== "number" || itemType === "map" || itemType === "set") break;
553
+ path.push(key);
546
554
  }
547
- }
555
+ const name = JSON.stringify(path);
556
+ const fieldErrors = nestedErrors[name];
557
+ if (fieldErrors) fieldErrors.push(issue.message);
558
+ else nestedErrors[name] = [issue.message];
559
+ } else if (rootErrors) rootErrors.push(issue.message);
560
+ else rootErrors = [issue.message];
561
+ }
562
+ let shouldFocus = config?.shouldFocus ?? false;
563
+ batch(() => {
564
+ untrack(() => {
565
+ walkFieldStore(internalFormStore, (internalFieldStore) => {
566
+ if (internalFieldStore.path.length === 0) internalFieldStore.errors.value = rootErrors ?? null;
567
+ else {
568
+ const fieldErrors = nestedErrors?.[internalFieldStore.name] ?? null;
569
+ internalFieldStore.errors.value = fieldErrors;
570
+ if (shouldFocus && fieldErrors && focusFieldElement(internalFieldStore)) shouldFocus = false;
571
+ }
572
+ });
573
+ });
574
+ internalFormStore.validators--;
575
+ internalFormStore.isValidating.value = internalFormStore.validators > 0;
548
576
  });
549
- internalFormStore.validators--;
550
- internalFormStore.isValidating.value = internalFormStore.validators > 0;
551
- });
552
- return result;
577
+ return result;
578
+ } catch (error) {
579
+ batch(() => {
580
+ internalFormStore.validators--;
581
+ internalFormStore.isValidating.value = internalFormStore.validators > 0;
582
+ });
583
+ throw error;
584
+ }
553
585
  }
554
586
  /**
555
587
  * Validates the form input if required based on the validation mode and form
@@ -571,7 +603,7 @@ const INTERNAL = "~internal";
571
603
  //#endregion
572
604
  //#region ../../packages/methods/dist/index.preact.js
573
605
  /**
574
- * Focuses the first input element of a field. This is useful for
606
+ * Focuses the first focusable input element of a field. This is useful for
575
607
  * programmatically setting focus to a specific field, such as after
576
608
  * validation errors or user interactions.
577
609
  *
@@ -579,27 +611,29 @@ const INTERNAL = "~internal";
579
611
  * @param config The focus field configuration.
580
612
  */
581
613
  function focus(form, config) {
582
- getFieldStore(form[INTERNAL], config.path).elements[0]?.focus();
614
+ focusFieldElement(getFieldStore(form[INTERNAL], config.path));
615
+ }
616
+ /* @__NO_SIDE_EFFECTS__ */
617
+ function getDeepErrorEntries(form, config) {
618
+ const entries = [];
619
+ walkFieldStore(config?.path ? getFieldStore(form[INTERNAL], config.path) : form[INTERNAL], (internalFieldStore) => {
620
+ const errors = internalFieldStore.errors.value;
621
+ if (errors) entries.push({
622
+ path: internalFieldStore.path,
623
+ errors
624
+ });
625
+ });
626
+ return entries;
583
627
  }
584
- /**
585
- * Retrieves all error messages from all fields in the form by walking through
586
- * the entire field store tree. This is useful for displaying a summary of all
587
- * validation errors across the form.
588
- *
589
- * @param form The form store to retrieve errors from.
590
- *
591
- * @returns A non-empty array of error messages, or null if no errors exist.
592
- */
593
628
  /* @__NO_SIDE_EFFECTS__ */
594
- function getAllErrors(form) {
595
- let allErrors = null;
596
- walkFieldStore(form[INTERNAL], (internalFieldStore) => {
597
- if (internalFieldStore.kind === "array") internalFieldStore.items.value;
629
+ function getDeepErrors(form, config) {
630
+ let deepErrors = null;
631
+ walkFieldStore(config?.path ? getFieldStore(form[INTERNAL], config.path) : form[INTERNAL], (internalFieldStore) => {
598
632
  const errors = internalFieldStore.errors.value;
599
- if (errors) if (allErrors) allErrors.push(...errors);
600
- else allErrors = [...errors];
633
+ if (errors) if (deepErrors) deepErrors.push(...errors);
634
+ else deepErrors = [...errors];
601
635
  });
602
- return allErrors;
636
+ return deepErrors;
603
637
  }
604
638
  /* @__NO_SIDE_EFFECTS__ */
605
639
  function getDirtyInput(form, config) {
@@ -607,9 +641,8 @@ function getDirtyInput(form, config) {
607
641
  }
608
642
  /* @__NO_SIDE_EFFECTS__ */
609
643
  function getDirtyPaths(form, config) {
610
- config?.path ? getFieldStore(form[INTERNAL], config.path) : form[INTERNAL];
611
644
  const paths = [];
612
- config?.path && [...config.path];
645
+ config?.path ? getFieldStore(form[INTERNAL], config.path) : form[INTERNAL];
613
646
  return paths;
614
647
  }
615
648
  /* @__NO_SIDE_EFFECTS__ */
@@ -660,25 +693,38 @@ function insert(form, config) {
660
693
  internalArrayStore.items.value = newItems;
661
694
  for (let index = items.length; index > insertIndex; index--) {
662
695
  if (!internalArrayStore.children[index]) {
663
- const path = JSON.parse(internalArrayStore.name);
664
696
  internalArrayStore.children[index] = {};
665
- path.push(index);
666
- initializeFieldStore(internalArrayStore.children[index], internalArrayStore.schema.item, void 0, path);
697
+ initializeFieldStore(internalArrayStore.children[index], internalArrayStore.schema.item, void 0, [...internalArrayStore.path, index]);
667
698
  }
668
699
  copyItemState(internalArrayStore.children[index - 1], internalArrayStore.children[index]);
669
700
  }
670
701
  if (!internalArrayStore.children[insertIndex]) {
671
- const path = JSON.parse(internalArrayStore.name);
672
702
  internalArrayStore.children[insertIndex] = {};
673
- path.push(insertIndex);
674
- initializeFieldStore(internalArrayStore.children[insertIndex], internalArrayStore.schema.item, config.initialInput, path);
703
+ initializeFieldStore(internalArrayStore.children[insertIndex], internalArrayStore.schema.item, config.initialInput, [...internalArrayStore.path, insertIndex]);
675
704
  } else resetItemState(internalArrayStore.children[insertIndex], config.initialInput);
676
705
  internalArrayStore.input.value = true;
677
706
  internalArrayStore.isTouched.value = true;
707
+ internalArrayStore.isEdited.value = true;
678
708
  internalArrayStore.isDirty.value = true;
679
709
  validateIfRequired(internalFormStore, internalArrayStore, "input");
680
710
  });
681
711
  }
712
+ /* @__NO_SIDE_EFFECTS__ */
713
+ function isDirty(form, config) {
714
+ return getFieldBool(config?.path ? getFieldStore(form[INTERNAL], config.path) : form[INTERNAL], "isDirty");
715
+ }
716
+ /* @__NO_SIDE_EFFECTS__ */
717
+ function isEdited(form, config) {
718
+ return getFieldBool(config?.path ? getFieldStore(form[INTERNAL], config.path) : form[INTERNAL], "isEdited");
719
+ }
720
+ /* @__NO_SIDE_EFFECTS__ */
721
+ function isTouched(form, config) {
722
+ return getFieldBool(config?.path ? getFieldStore(form[INTERNAL], config.path) : form[INTERNAL], "isTouched");
723
+ }
724
+ /* @__NO_SIDE_EFFECTS__ */
725
+ function isValid(form, config) {
726
+ return !getFieldBool(config?.path ? getFieldStore(form[INTERNAL], config.path) : form[INTERNAL], "errors");
727
+ }
682
728
  /**
683
729
  * Moves an item from one index to another within a field array. All items
684
730
  * between the source and destination indices are shifted accordingly.
@@ -701,6 +747,7 @@ function move(form, config) {
701
747
  else for (let index = config.from; index > config.to; index--) copyItemState(internalArrayStore.children[index - 1], internalArrayStore.children[index]);
702
748
  copyItemState(tempInternalFieldStore, internalArrayStore.children[config.to]);
703
749
  internalArrayStore.isTouched.value = true;
750
+ internalArrayStore.isEdited.value = true;
704
751
  internalArrayStore.isDirty.value = internalArrayStore.startItems.value.join() !== newItems.join();
705
752
  validateIfRequired(internalFormStore, internalArrayStore, "input");
706
753
  });
@@ -764,6 +811,7 @@ function remove(form, config) {
764
811
  internalArrayStore.items.value = newItems;
765
812
  for (let index = config.at; index < items.length - 1; index++) copyItemState(internalArrayStore.children[index + 1], internalArrayStore.children[index]);
766
813
  internalArrayStore.isTouched.value = true;
814
+ internalArrayStore.isEdited.value = true;
767
815
  internalArrayStore.isDirty.value = internalArrayStore.startItems.value.join() !== newItems.join();
768
816
  validateIfRequired(internalFormStore, internalArrayStore, "input");
769
817
  });
@@ -784,6 +832,7 @@ function replace(form, config) {
784
832
  internalArrayStore.items.value = newItems;
785
833
  resetItemState(internalArrayStore.children[config.at], config.initialInput);
786
834
  internalArrayStore.isTouched.value = true;
835
+ internalArrayStore.isEdited.value = true;
787
836
  internalArrayStore.isDirty.value = true;
788
837
  validateIfRequired(internalFormStore, internalArrayStore, "input");
789
838
  });
@@ -798,6 +847,7 @@ function reset(form, config) {
798
847
  internalFieldStore$1.elements = internalFieldStore$1.initialElements;
799
848
  if (!config?.keepErrors) internalFieldStore$1.errors.value = null;
800
849
  if (!config?.keepTouched) internalFieldStore$1.isTouched.value = false;
850
+ if (!config?.keepEdited) internalFieldStore$1.isEdited.value = false;
801
851
  internalFieldStore$1.startInput.value = internalFieldStore$1.initialInput.value;
802
852
  if (!config?.keepInput) internalFieldStore$1.input.value = internalFieldStore$1.initialInput.value;
803
853
  if (internalFieldStore$1.kind === "array") {
@@ -856,6 +906,7 @@ function swap(form, config) {
856
906
  internalArrayStore.items.value = newItems;
857
907
  swapItemState(internalArrayStore.children[config.at], internalArrayStore.children[config.and]);
858
908
  internalArrayStore.isTouched.value = true;
909
+ internalArrayStore.isEdited.value = true;
859
910
  internalArrayStore.isDirty.value = internalArrayStore.startItems.value.join() !== newItems.join();
860
911
  validateIfRequired(internalFormStore, internalArrayStore, "input");
861
912
  });
@@ -913,7 +964,10 @@ function useField(form, config) {
913
964
  const internalFieldStore = useComputed(() => getFieldStore(internalFormStore, pathSignal.value));
914
965
  useSignalEffect(() => {
915
966
  return () => {
916
- internalFieldStore.value.elements = internalFieldStore.value.elements.filter((element) => element.isConnected);
967
+ const internalFieldStoreValue = internalFieldStore.value;
968
+ const elements = internalFieldStoreValue.elements.filter((element) => element.isConnected);
969
+ if (internalFieldStoreValue.elements === internalFieldStoreValue.initialElements) internalFieldStoreValue.initialElements = elements;
970
+ internalFieldStoreValue.elements = elements;
917
971
  };
918
972
  });
919
973
  return useMemo(() => ({
@@ -921,6 +975,7 @@ function useField(form, config) {
921
975
  input: computed(() => getFieldInput(internalFieldStore.value)),
922
976
  errors: computed(() => internalFieldStore.value.errors.value),
923
977
  isTouched: computed(() => getFieldBool(internalFieldStore.value, "isTouched")),
978
+ isEdited: computed(() => getFieldBool(internalFieldStore.value, "isEdited")),
924
979
  isDirty: computed(() => getFieldBool(internalFieldStore.value, "isDirty")),
925
980
  isValid: computed(() => !getFieldBool(internalFieldStore.value, "errors")),
926
981
  onInput(value) {
@@ -964,6 +1019,7 @@ function useFieldArray(form, config) {
964
1019
  items: computed(() => internalFieldStore.value.items.value),
965
1020
  errors: computed(() => internalFieldStore.value.errors.value),
966
1021
  isTouched: computed(() => getFieldBool(internalFieldStore.value, "isTouched")),
1022
+ isEdited: computed(() => getFieldBool(internalFieldStore.value, "isEdited")),
967
1023
  isDirty: computed(() => getFieldBool(internalFieldStore.value, "isDirty")),
968
1024
  isValid: computed(() => !getFieldBool(internalFieldStore.value, "errors"))
969
1025
  }), []);
@@ -981,6 +1037,7 @@ function useForm(config) {
981
1037
  isSubmitted: internalFormStore.isSubmitted,
982
1038
  isValidating: internalFormStore.isValidating,
983
1039
  isTouched: computed(() => getFieldBool(internalFormStore, "isTouched")),
1040
+ isEdited: computed(() => getFieldBool(internalFormStore, "isEdited")),
984
1041
  isDirty: computed(() => getFieldBool(internalFormStore, "isDirty")),
985
1042
  isValid: computed(() => !getFieldBool(internalFormStore, "errors")),
986
1043
  errors: internalFormStore.errors
@@ -1040,4 +1097,4 @@ function Form({ of, onSubmit, ...other }) {
1040
1097
  }
1041
1098
 
1042
1099
  //#endregion
1043
- export { Field, FieldArray, Form, focus, getAllErrors, getDirtyInput, getDirtyPaths, getErrors, getInput, handleSubmit, insert, move, pickDirty, remove, replace, reset, setErrors, setInput, submit, swap, useField, useFieldArray, useForm, validate };
1100
+ export { Field, FieldArray, Form, focus, getDeepErrorEntries, getDeepErrors, getDirtyInput, getDirtyPaths, getErrors, getInput, handleSubmit, insert, isDirty, isEdited, isTouched, isValid, move, pickDirty, remove, replace, reset, setErrors, setInput, submit, swap, useField, useFieldArray, useForm, validate };