@formisch/qwik 0.11.0 → 0.13.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.
@@ -47,11 +47,13 @@ function initializeFieldStore(internalFieldStore, schema, initialInput, path, nu
47
47
  else {
48
48
  internalFieldStore.schema = schema;
49
49
  internalFieldStore.name = JSON.stringify(path);
50
+ internalFieldStore.path = path;
50
51
  const initialElements = [];
51
52
  internalFieldStore.initialElements = initialElements;
52
53
  internalFieldStore.elements = initialElements;
53
54
  internalFieldStore.errors = createSignal(null);
54
55
  internalFieldStore.isTouched = createSignal(false);
56
+ internalFieldStore.isEdited = createSignal(false);
55
57
  internalFieldStore.isDirty = createSignal(false);
56
58
  if (schema.type === "array" || schema.type === "loose_tuple" || schema.type === "strict_tuple" || schema.type === "tuple") {
57
59
  if (internalFieldStore.kind && internalFieldStore.kind !== "array") throw new Error(`Store initialized as "${internalFieldStore.kind}" cannot be reinitialized as "array"`);
@@ -61,16 +63,13 @@ function initializeFieldStore(internalFieldStore, schema, initialInput, path, nu
61
63
  if (schema.type === "array") {
62
64
  if (initialInput) for (let index = 0; index < initialInput.length; index++) {
63
65
  internalFieldStore.children[index] = {};
64
- path.push(index);
65
- initializeFieldStore(internalFieldStore.children[index], schema.item, initialInput[index], path);
66
- path.pop();
66
+ initializeFieldStore(internalFieldStore.children[index], schema.item, initialInput[index], [...path, index]);
67
67
  }
68
68
  } else for (let index = 0; index < schema.items.length; index++) {
69
69
  internalFieldStore.children[index] = {};
70
- path.push(index);
71
- initializeFieldStore(internalFieldStore.children[index], schema.items[index], initialInput?.[index], path);
72
- path.pop();
70
+ initializeFieldStore(internalFieldStore.children[index], schema.items[index], initialInput?.[index], [...path, index]);
73
71
  }
72
+ internalFieldStore.isNullish = nullish;
74
73
  const arrayInput = nullish && initialInput == null ? initialInput : true;
75
74
  internalFieldStore.initialInput = createSignal(arrayInput);
76
75
  internalFieldStore.startInput = createSignal(arrayInput);
@@ -87,10 +86,9 @@ function initializeFieldStore(internalFieldStore, schema, initialInput, path, nu
87
86
  internalFieldStore.children ??= {};
88
87
  for (const key in schema.entries) {
89
88
  internalFieldStore.children[key] ??= {};
90
- path.push(key);
91
- initializeFieldStore(internalFieldStore.children[key], schema.entries[key], initialInput?.[key], path);
92
- path.pop();
89
+ initializeFieldStore(internalFieldStore.children[key], schema.entries[key], initialInput?.[key], [...path, key]);
93
90
  }
91
+ internalFieldStore.isNullish = nullish;
94
92
  const objectInput = nullish && initialInput == null ? initialInput : true;
95
93
  internalFieldStore.initialInput = createSignal(objectInput);
96
94
  internalFieldStore.startInput = createSignal(objectInput);
@@ -110,8 +108,9 @@ function initializeFieldStore(internalFieldStore, schema, initialInput, path, nu
110
108
  /**
111
109
  * Copies the deeply nested state (signal values) from one field store to
112
110
  * another. This includes the `elements`, `errors`, `startInput`, `input`,
113
- * `isTouched`, `isDirty`, and for arrays `startItems` and `items` properties.
114
- * Recursively walks through the field stores and copies all signal values.
111
+ * `isTouched`, `isEdited`, `isDirty`, and for arrays `startItems` and `items`
112
+ * properties. Recursively walks through the field stores and copies all signal
113
+ * values.
115
114
  *
116
115
  * @param fromInternalFieldStore The source field store to copy from.
117
116
  * @param toInternalFieldStore The destination field store to copy to.
@@ -124,19 +123,16 @@ function copyItemState(fromInternalFieldStore, toInternalFieldStore) {
124
123
  toInternalFieldStore.startInput.value = fromInternalFieldStore.startInput.value;
125
124
  toInternalFieldStore.input.value = fromInternalFieldStore.input.value;
126
125
  toInternalFieldStore.isTouched.value = fromInternalFieldStore.isTouched.value;
126
+ toInternalFieldStore.isEdited.value = fromInternalFieldStore.isEdited.value;
127
127
  toInternalFieldStore.isDirty.value = fromInternalFieldStore.isDirty.value;
128
128
  if (fromInternalFieldStore.kind === "array" && toInternalFieldStore.kind === "array") {
129
129
  const fromItems = fromInternalFieldStore.items.value;
130
130
  toInternalFieldStore.startItems.value = fromInternalFieldStore.startItems.value;
131
131
  toInternalFieldStore.items.value = fromItems;
132
- let path;
133
132
  for (let index = 0; index < fromItems.length; index++) {
134
133
  if (!toInternalFieldStore.children[index]) {
135
- path ??= JSON.parse(toInternalFieldStore.name);
136
134
  toInternalFieldStore.children[index] = {};
137
- path.push(index);
138
- initializeFieldStore(toInternalFieldStore.children[index], toInternalFieldStore.schema.item, void 0, path);
139
- path.pop();
135
+ initializeFieldStore(toInternalFieldStore.children[index], toInternalFieldStore.schema.item, void 0, [...toInternalFieldStore.path, index]);
140
136
  }
141
137
  copyItemState(fromInternalFieldStore.children[index], toInternalFieldStore.children[index]);
142
138
  }
@@ -146,45 +142,61 @@ function copyItemState(fromInternalFieldStore, toInternalFieldStore) {
146
142
  }
147
143
  /**
148
144
  * Resets the state of a field store (signal values) deeply nested. Sets
149
- * `elements` to empty array, `errors` to `null`, `isTouched` and `isDirty` to
150
- * `false`, and `startInput`, `input`, `startItems`, and `items` to the new
151
- * input value. Keeps the `initialInput` and `initialItems` state unchanged for
152
- * form reset functionality.
145
+ * `elements` to empty array, `errors` to `null`, `isTouched`, `isEdited` and
146
+ * `isDirty` to `false`, and `startInput`, `input`, `startItems`, and `items` to
147
+ * the new input value. Keeps the `initialInput` and `initialItems` state
148
+ * unchanged for form reset functionality.
153
149
  *
154
150
  * @param internalFieldStore The field store to reset.
155
- * @param initialInput The new input value (can be any type including array or object).
151
+ * @param input The new input value (can be any type including array or object).
152
+ * @param keepStart Whether to keep `startInput` and `startItems` as the dirty
153
+ * baseline instead of resetting them to the new input. Used when a field store
154
+ * is reused for an in-place edit so its dirty state is detected correctly.
156
155
  */
157
- function resetItemState(internalFieldStore, initialInput) {
156
+ function resetItemState(internalFieldStore, input, keepStart = false) {
158
157
  batch(() => {
159
- internalFieldStore.elements = [];
158
+ const elements = [];
159
+ if (internalFieldStore.elements === internalFieldStore.initialElements) internalFieldStore.initialElements = elements;
160
+ internalFieldStore.elements = elements;
160
161
  internalFieldStore.errors.value = null;
161
162
  internalFieldStore.isTouched.value = false;
163
+ internalFieldStore.isEdited.value = false;
162
164
  internalFieldStore.isDirty.value = false;
163
165
  if (internalFieldStore.kind === "array" || internalFieldStore.kind === "object") {
164
- const objectInput = initialInput == null ? initialInput : true;
165
- internalFieldStore.startInput.value = objectInput;
166
+ const objectInput = internalFieldStore.isNullish && input == null ? input : true;
167
+ if (!keepStart) internalFieldStore.startInput.value = objectInput;
166
168
  internalFieldStore.input.value = objectInput;
167
- if (internalFieldStore.kind === "array") if (initialInput) {
168
- const newItems = initialInput.map(createId);
169
- internalFieldStore.startItems.value = newItems;
170
- internalFieldStore.items.value = newItems;
171
- for (let index = 0; index < initialInput.length; index++) if (internalFieldStore.children[index]) resetItemState(internalFieldStore.children[index], initialInput[index]);
172
- } else {
173
- internalFieldStore.startItems.value = [];
174
- internalFieldStore.items.value = [];
175
- }
176
- else for (const key in internalFieldStore.children) resetItemState(internalFieldStore.children[key], initialInput?.[key]);
169
+ if (internalFieldStore.kind === "array") {
170
+ const isTuple = internalFieldStore.schema.type !== "array";
171
+ if (input || isTuple) {
172
+ const length = isTuple ? internalFieldStore.children.length : input.length;
173
+ const newItems = Array.from({ length }, createId);
174
+ if (!keepStart) internalFieldStore.startItems.value = newItems;
175
+ internalFieldStore.items.value = newItems;
176
+ for (let index = 0; index < length; index++) {
177
+ const itemInput = input?.[index];
178
+ if (internalFieldStore.children[index]) resetItemState(internalFieldStore.children[index], itemInput, keepStart);
179
+ else {
180
+ internalFieldStore.children[index] = {};
181
+ initializeFieldStore(internalFieldStore.children[index], internalFieldStore.schema.item, itemInput, [...internalFieldStore.path, index]);
182
+ }
183
+ }
184
+ } else {
185
+ if (!keepStart) internalFieldStore.startItems.value = [];
186
+ internalFieldStore.items.value = [];
187
+ }
188
+ } else for (const key in internalFieldStore.children) resetItemState(internalFieldStore.children[key], input?.[key], keepStart);
177
189
  } else {
178
- internalFieldStore.startInput.value = initialInput;
179
- internalFieldStore.input.value = initialInput;
190
+ if (!keepStart) internalFieldStore.startInput.value = input;
191
+ internalFieldStore.input.value = input;
180
192
  }
181
193
  });
182
194
  }
183
195
  /**
184
196
  * Swaps the deeply nested state (signal values) between two field stores. This
185
197
  * includes the `elements`, `errors`, `startInput`, `input`, `isTouched`,
186
- * `isDirty`, and for arrays `startItems` and `items` properties. Recursively
187
- * walks through the field stores and swaps all signal values.
198
+ * `isEdited`, `isDirty`, and for arrays `startItems` and `items` properties.
199
+ * Recursively walks through the field stores and swaps all signal values.
188
200
  *
189
201
  * @param firstInternalFieldStore The first field store to swap.
190
202
  * @param secondInternalFieldStore The second field store to swap.
@@ -207,6 +219,9 @@ function swapItemState(firstInternalFieldStore, secondInternalFieldStore) {
207
219
  const tempIsTouched = firstInternalFieldStore.isTouched.value;
208
220
  firstInternalFieldStore.isTouched.value = secondInternalFieldStore.isTouched.value;
209
221
  secondInternalFieldStore.isTouched.value = tempIsTouched;
222
+ const tempIsEdited = firstInternalFieldStore.isEdited.value;
223
+ firstInternalFieldStore.isEdited.value = secondInternalFieldStore.isEdited.value;
224
+ secondInternalFieldStore.isEdited.value = tempIsEdited;
210
225
  const tempIsDirty = firstInternalFieldStore.isDirty.value;
211
226
  firstInternalFieldStore.isDirty.value = secondInternalFieldStore.isDirty.value;
212
227
  secondInternalFieldStore.isDirty.value = tempIsDirty;
@@ -219,22 +234,14 @@ function swapItemState(firstInternalFieldStore, secondInternalFieldStore) {
219
234
  firstInternalFieldStore.items.value = secondItems;
220
235
  secondInternalFieldStore.items.value = firstItems;
221
236
  const maxLength = Math.max(firstItems.length, secondItems.length);
222
- let firstPath;
223
- let secondPath;
224
237
  for (let index = 0; index < maxLength; index++) {
225
238
  if (!firstInternalFieldStore.children[index]) {
226
- firstPath ??= JSON.parse(firstInternalFieldStore.name);
227
239
  firstInternalFieldStore.children[index] = {};
228
- firstPath.push(index);
229
- initializeFieldStore(firstInternalFieldStore.children[index], firstInternalFieldStore.schema.item, void 0, firstPath);
230
- firstPath.pop();
240
+ initializeFieldStore(firstInternalFieldStore.children[index], firstInternalFieldStore.schema.item, void 0, [...firstInternalFieldStore.path, index]);
231
241
  }
232
242
  if (!secondInternalFieldStore.children[index]) {
233
- secondPath ??= JSON.parse(secondInternalFieldStore.name);
234
243
  secondInternalFieldStore.children[index] = {};
235
- secondPath.push(index);
236
- initializeFieldStore(secondInternalFieldStore.children[index], secondInternalFieldStore.schema.item, void 0, secondPath);
237
- secondPath.pop();
244
+ initializeFieldStore(secondInternalFieldStore.children[index], secondInternalFieldStore.schema.item, void 0, [...secondInternalFieldStore.path, index]);
238
245
  }
239
246
  swapItemState(firstInternalFieldStore.children[index], secondInternalFieldStore.children[index]);
240
247
  }
@@ -243,6 +250,51 @@ function swapItemState(firstInternalFieldStore, secondInternalFieldStore) {
243
250
  });
244
251
  }
245
252
  /**
253
+ * Focuses the first focusable element of a field store. The elements are tried
254
+ * in order and the first one that actually receives focus wins, so detached,
255
+ * disabled or hidden elements are skipped. The browser decides focusability,
256
+ * which is read back via the element's root `activeElement` so elements in a
257
+ * shadow root or another document are handled correctly.
258
+ *
259
+ * Hint: A `display: none` or `hidden` element is correctly skipped in real
260
+ * browsers, but jsdom has no layout and focuses it anyway, so that case cannot
261
+ * be covered by unit tests.
262
+ *
263
+ * @param internalFieldStore The field store to focus.
264
+ *
265
+ * @returns Whether an element was focused.
266
+ */
267
+ function focusFieldElement(internalFieldStore) {
268
+ for (const element of internalFieldStore.elements) {
269
+ element.focus();
270
+ if (element.getRootNode().activeElement === element) return true;
271
+ }
272
+ return false;
273
+ }
274
+ /**
275
+ * Walks through the field store and all nested children, calling the callback
276
+ * for each field store in depth-first order. The callback may return `true` to
277
+ * stop the walk early, in which case `walkFieldStore` returns `true` as well.
278
+ *
279
+ * The walk reads array `items` reactively, so a reactive caller subscribes to
280
+ * structural changes naturally. Imperative callers that must not subscribe
281
+ * (e.g. when invoked inside an effect) should wrap the call in `untrack`.
282
+ *
283
+ * @param internalFieldStore The field store to walk.
284
+ * @param callback The callback to invoke for each field store. Return `true` to stop the walk early.
285
+ *
286
+ * @returns Whether the walk was stopped early by the callback.
287
+ */
288
+ function walkFieldStore(internalFieldStore, callback) {
289
+ if (callback(internalFieldStore)) return true;
290
+ if (internalFieldStore.kind === "array") {
291
+ for (let index = 0; index < internalFieldStore.items.value.length; index++) if (walkFieldStore(internalFieldStore.children[index], callback)) return true;
292
+ } else if (internalFieldStore.kind === "object") {
293
+ for (const key in internalFieldStore.children) if (walkFieldStore(internalFieldStore.children[key], callback)) return true;
294
+ }
295
+ return false;
296
+ }
297
+ /**
246
298
  * Returns whether the specified boolean property is true for the field store
247
299
  * or any of its nested children. Recursively checks arrays and objects.
248
300
  *
@@ -253,16 +305,7 @@ function swapItemState(firstInternalFieldStore, secondInternalFieldStore) {
253
305
  */
254
306
  /* @__NO_SIDE_EFFECTS__ */
255
307
  function getFieldBool(internalFieldStore, type) {
256
- if (internalFieldStore[type].value) return true;
257
- if (internalFieldStore.kind === "array") {
258
- for (let index = 0; index < internalFieldStore.items.value.length; index++) if (/* @__PURE__ */ getFieldBool(internalFieldStore.children[index], type)) return true;
259
- return false;
260
- }
261
- if (internalFieldStore.kind == "object") {
262
- for (const key in internalFieldStore.children) if (/* @__PURE__ */ getFieldBool(internalFieldStore.children[key], type)) return true;
263
- return false;
264
- }
265
- return false;
308
+ return walkFieldStore(internalFieldStore, (internalFieldStore$1) => Boolean(internalFieldStore$1[type].value));
266
309
  }
267
310
  /**
268
311
  * Returns only the dirty input of the field store. Arrays are treated as
@@ -380,11 +423,11 @@ function getFieldStore(internalFormStore, path) {
380
423
  */
381
424
  function setFieldBool(internalFieldStore, type, bool) {
382
425
  batch(() => {
383
- if (internalFieldStore.kind === "array") {
384
- internalFieldStore[type].value = bool;
385
- for (let index = 0; index < untrack(() => internalFieldStore.items.value).length; index++) setFieldBool(internalFieldStore.children[index], type, bool);
386
- } else if (internalFieldStore.kind == "object") for (const key in internalFieldStore.children) setFieldBool(internalFieldStore.children[key], type, bool);
387
- else internalFieldStore[type].value = bool;
426
+ untrack(() => {
427
+ walkFieldStore(internalFieldStore, (internalFieldStore$1) => {
428
+ internalFieldStore$1[type].value = bool;
429
+ });
430
+ });
388
431
  });
389
432
  }
390
433
  /**
@@ -396,23 +439,21 @@ function setFieldBool(internalFieldStore, type, bool) {
396
439
  */
397
440
  function setNestedInput(internalFieldStore, input) {
398
441
  internalFieldStore.isTouched.value = true;
442
+ internalFieldStore.isEdited.value = true;
399
443
  if (internalFieldStore.kind === "array") {
400
444
  const arrayInput = input ?? [];
401
445
  const items = internalFieldStore.items.value;
402
- if (arrayInput.length < items.length) internalFieldStore.items.value = items.slice(0, arrayInput.length);
403
- else if (arrayInput.length > items.length) {
404
- if (arrayInput.length > internalFieldStore.children.length) {
405
- const path = JSON.parse(internalFieldStore.name);
406
- for (let index = internalFieldStore.children.length; index < arrayInput.length; index++) {
407
- internalFieldStore.children[index] = {};
408
- path.push(index);
409
- initializeFieldStore(internalFieldStore.children[index], internalFieldStore.schema.item, arrayInput[index], path);
410
- path.pop();
411
- }
446
+ const length = internalFieldStore.schema.type === "array" ? arrayInput.length : internalFieldStore.children.length;
447
+ if (length < items.length) internalFieldStore.items.value = items.slice(0, length);
448
+ else if (length > items.length) {
449
+ for (let index = items.length; index < length; index++) if (internalFieldStore.children[index]) resetItemState(internalFieldStore.children[index], arrayInput[index], true);
450
+ else {
451
+ internalFieldStore.children[index] = {};
452
+ initializeFieldStore(internalFieldStore.children[index], internalFieldStore.schema.item, arrayInput[index], [...internalFieldStore.path, index]);
412
453
  }
413
- internalFieldStore.items.value = [...items, ...arrayInput.slice(items.length).map(createId)];
454
+ internalFieldStore.items.value = [...items, ...Array.from({ length: length - items.length }, createId)];
414
455
  }
415
- for (let index = 0; index < arrayInput.length; index++) setNestedInput(internalFieldStore.children[index], arrayInput[index]);
456
+ for (let index = 0; index < length; index++) setNestedInput(internalFieldStore.children[index], arrayInput[index]);
416
457
  internalFieldStore.input.value = input == null ? input : true;
417
458
  internalFieldStore.isDirty.value = internalFieldStore.startInput.value !== internalFieldStore.input.value || internalFieldStore.startItems.value.length !== internalFieldStore.items.value.length;
418
459
  } else if (internalFieldStore.kind === "object") {
@@ -456,38 +497,22 @@ function setFieldInput(internalFormStore, path, input) {
456
497
  function setInitialFieldInput(internalFieldStore, initialInput) {
457
498
  batch(() => {
458
499
  if (internalFieldStore.kind === "array") {
459
- internalFieldStore.input.value = initialInput == null ? initialInput : true;
500
+ internalFieldStore.initialInput.value = initialInput == null ? initialInput : true;
460
501
  const initialArrayInput = initialInput ?? [];
461
- if (initialArrayInput.length > internalFieldStore.children.length) {
462
- const path = JSON.parse(internalFieldStore.name);
463
- for (let index = internalFieldStore.children.length; index < initialArrayInput.length; index++) {
464
- internalFieldStore.children[index] = {};
465
- path.push(index);
466
- initializeFieldStore(internalFieldStore.children[index], internalFieldStore.schema.item, initialArrayInput[index], path);
467
- path.pop();
468
- }
502
+ const length = internalFieldStore.schema.type === "array" ? initialArrayInput.length : internalFieldStore.children.length;
503
+ if (length > internalFieldStore.children.length) for (let index = internalFieldStore.children.length; index < length; index++) {
504
+ internalFieldStore.children[index] = {};
505
+ initializeFieldStore(internalFieldStore.children[index], internalFieldStore.schema.item, initialArrayInput[index], [...internalFieldStore.path, index]);
469
506
  }
470
- internalFieldStore.initialItems.value = initialArrayInput.map(createId);
507
+ internalFieldStore.initialItems.value = Array.from({ length }, createId);
471
508
  for (let index = 0; index < internalFieldStore.children.length; index++) setInitialFieldInput(internalFieldStore.children[index], initialArrayInput[index]);
472
509
  } else if (internalFieldStore.kind === "object") {
473
- internalFieldStore.input.value = initialInput == null ? initialInput : true;
510
+ internalFieldStore.initialInput.value = initialInput == null ? initialInput : true;
474
511
  for (const key in internalFieldStore.children) setInitialFieldInput(internalFieldStore.children[key], initialInput?.[key]);
475
512
  } else internalFieldStore.initialInput.value = initialInput;
476
513
  });
477
514
  }
478
515
  /**
479
- * Walks through the field store and all nested children, calling the callback
480
- * for each field store in depth-first order.
481
- *
482
- * @param internalFieldStore The field store to walk.
483
- * @param callback The callback to invoke for each field store.
484
- */
485
- function walkFieldStore(internalFieldStore, callback) {
486
- callback(internalFieldStore);
487
- if (internalFieldStore.kind === "array") for (let index = 0; index < untrack(() => internalFieldStore.items.value).length; index++) walkFieldStore(internalFieldStore.children[index], callback);
488
- else if (internalFieldStore.kind === "object") for (const key in internalFieldStore.children) walkFieldStore(internalFieldStore.children[key], callback);
489
- }
490
- /**
491
516
  * Creates a new internal form store from the provided configuration.
492
517
  * Initializes the field store hierarchy, sets validation modes, and
493
518
  * creates form state signals.
@@ -522,44 +547,51 @@ function createFormStore(config, parse) {
522
547
  async function validateFormInput(internalFormStore, config) {
523
548
  internalFormStore.validators++;
524
549
  internalFormStore.isValidating.value = true;
525
- const result = await internalFormStore.parse(untrack(() => /* @__PURE__ */ getFieldInput(internalFormStore)));
526
- let rootErrors;
527
- let nestedErrors;
528
- if (result.issues) {
529
- nestedErrors = {};
530
- for (const issue of result.issues) if (issue.path) {
531
- const path = [];
532
- for (const pathItem of issue.path) {
533
- const key = pathItem.key;
534
- const keyType = typeof key;
535
- const itemType = pathItem.type;
536
- if (keyType !== "string" && keyType !== "number" || itemType === "map" || itemType === "set") break;
537
- path.push(key);
538
- }
539
- const name = JSON.stringify(path);
540
- const fieldErrors = nestedErrors[name];
541
- if (fieldErrors) fieldErrors.push(issue.message);
542
- else nestedErrors[name] = [issue.message];
543
- } else if (rootErrors) rootErrors.push(issue.message);
544
- else rootErrors = [issue.message];
545
- }
546
- let shouldFocus = config?.shouldFocus ?? false;
547
- batch(() => {
548
- walkFieldStore(internalFormStore, (internalFieldStore) => {
549
- if (internalFieldStore.name === "[]") internalFieldStore.errors.value = rootErrors ?? null;
550
- else {
551
- const fieldErrors = nestedErrors?.[internalFieldStore.name] ?? null;
552
- internalFieldStore.errors.value = fieldErrors;
553
- if (shouldFocus && fieldErrors) {
554
- internalFieldStore.elements[0]?.focus();
555
- shouldFocus = false;
550
+ try {
551
+ const result = await internalFormStore.parse(untrack(() => /* @__PURE__ */ getFieldInput(internalFormStore)));
552
+ let rootErrors;
553
+ let nestedErrors;
554
+ if (result.issues) {
555
+ nestedErrors = {};
556
+ for (const issue of result.issues) if (issue.path) {
557
+ const path = [];
558
+ for (const pathItem of issue.path) {
559
+ const key = pathItem.key;
560
+ const keyType = typeof key;
561
+ const itemType = pathItem.type;
562
+ if (keyType !== "string" && keyType !== "number" || itemType === "map" || itemType === "set") break;
563
+ path.push(key);
556
564
  }
557
- }
565
+ const name = JSON.stringify(path);
566
+ const fieldErrors = nestedErrors[name];
567
+ if (fieldErrors) fieldErrors.push(issue.message);
568
+ else nestedErrors[name] = [issue.message];
569
+ } else if (rootErrors) rootErrors.push(issue.message);
570
+ else rootErrors = [issue.message];
571
+ }
572
+ let shouldFocus = config?.shouldFocus ?? false;
573
+ batch(() => {
574
+ untrack(() => {
575
+ walkFieldStore(internalFormStore, (internalFieldStore) => {
576
+ if (internalFieldStore.path.length === 0) internalFieldStore.errors.value = rootErrors ?? null;
577
+ else {
578
+ const fieldErrors = nestedErrors?.[internalFieldStore.name] ?? null;
579
+ internalFieldStore.errors.value = fieldErrors;
580
+ if (shouldFocus && fieldErrors && focusFieldElement(internalFieldStore)) shouldFocus = false;
581
+ }
582
+ });
583
+ });
584
+ internalFormStore.validators--;
585
+ internalFormStore.isValidating.value = internalFormStore.validators > 0;
558
586
  });
559
- internalFormStore.validators--;
560
- internalFormStore.isValidating.value = internalFormStore.validators > 0;
561
- });
562
- return result;
587
+ return result;
588
+ } catch (error) {
589
+ batch(() => {
590
+ internalFormStore.validators--;
591
+ internalFormStore.isValidating.value = internalFormStore.validators > 0;
592
+ });
593
+ throw error;
594
+ }
563
595
  }
564
596
  /**
565
597
  * Validates the form input if required based on the validation mode and form
@@ -581,7 +613,7 @@ const INTERNAL = "~internal";
581
613
  //#endregion
582
614
  //#region ../../packages/methods/dist/index.qwik.js
583
615
  /**
584
- * Focuses the first input element of a field. This is useful for
616
+ * Focuses the first focusable input element of a field. This is useful for
585
617
  * programmatically setting focus to a specific field, such as after
586
618
  * validation errors or user interactions.
587
619
  *
@@ -589,27 +621,29 @@ const INTERNAL = "~internal";
589
621
  * @param config The focus field configuration.
590
622
  */
591
623
  function focus(form, config) {
592
- getFieldStore(form[INTERNAL], config.path).elements[0]?.focus();
624
+ focusFieldElement(getFieldStore(form[INTERNAL], config.path));
625
+ }
626
+ /* @__NO_SIDE_EFFECTS__ */
627
+ function getDeepErrorEntries(form, config) {
628
+ const entries = [];
629
+ walkFieldStore(config?.path ? getFieldStore(form[INTERNAL], config.path) : form[INTERNAL], (internalFieldStore) => {
630
+ const errors = internalFieldStore.errors.value;
631
+ if (errors) entries.push({
632
+ path: internalFieldStore.path,
633
+ errors
634
+ });
635
+ });
636
+ return entries;
593
637
  }
594
- /**
595
- * Retrieves all error messages from all fields in the form by walking through
596
- * the entire field store tree. This is useful for displaying a summary of all
597
- * validation errors across the form.
598
- *
599
- * @param form The form store to retrieve errors from.
600
- *
601
- * @returns A non-empty array of error messages, or null if no errors exist.
602
- */
603
638
  /* @__NO_SIDE_EFFECTS__ */
604
- function getAllErrors(form) {
605
- let allErrors = null;
606
- walkFieldStore(form[INTERNAL], (internalFieldStore) => {
607
- if (internalFieldStore.kind === "array") internalFieldStore.items.value;
639
+ function getDeepErrors(form, config) {
640
+ let deepErrors = null;
641
+ walkFieldStore(config?.path ? getFieldStore(form[INTERNAL], config.path) : form[INTERNAL], (internalFieldStore) => {
608
642
  const errors = internalFieldStore.errors.value;
609
- if (errors) if (allErrors) allErrors.push(...errors);
610
- else allErrors = [...errors];
643
+ if (errors) if (deepErrors) deepErrors.push(...errors);
644
+ else deepErrors = [...errors];
611
645
  });
612
- return allErrors;
646
+ return deepErrors;
613
647
  }
614
648
  /* @__NO_SIDE_EFFECTS__ */
615
649
  function getDirtyInput(form, config) {
@@ -617,9 +651,8 @@ function getDirtyInput(form, config) {
617
651
  }
618
652
  /* @__NO_SIDE_EFFECTS__ */
619
653
  function getDirtyPaths(form, config) {
620
- config?.path ? getFieldStore(form[INTERNAL], config.path) : form[INTERNAL];
621
654
  const paths = [];
622
- config?.path && [...config.path];
655
+ config?.path ? getFieldStore(form[INTERNAL], config.path) : form[INTERNAL];
623
656
  return paths;
624
657
  }
625
658
  /* @__NO_SIDE_EFFECTS__ */
@@ -670,25 +703,38 @@ function insert(form, config) {
670
703
  internalArrayStore.items.value = newItems;
671
704
  for (let index = items.length; index > insertIndex; index--) {
672
705
  if (!internalArrayStore.children[index]) {
673
- const path = JSON.parse(internalArrayStore.name);
674
706
  internalArrayStore.children[index] = {};
675
- path.push(index);
676
- initializeFieldStore(internalArrayStore.children[index], internalArrayStore.schema.item, void 0, path);
707
+ initializeFieldStore(internalArrayStore.children[index], internalArrayStore.schema.item, void 0, [...internalArrayStore.path, index]);
677
708
  }
678
709
  copyItemState(internalArrayStore.children[index - 1], internalArrayStore.children[index]);
679
710
  }
680
711
  if (!internalArrayStore.children[insertIndex]) {
681
- const path = JSON.parse(internalArrayStore.name);
682
712
  internalArrayStore.children[insertIndex] = {};
683
- path.push(insertIndex);
684
- initializeFieldStore(internalArrayStore.children[insertIndex], internalArrayStore.schema.item, config.initialInput, path);
713
+ initializeFieldStore(internalArrayStore.children[insertIndex], internalArrayStore.schema.item, config.initialInput, [...internalArrayStore.path, insertIndex]);
685
714
  } else resetItemState(internalArrayStore.children[insertIndex], config.initialInput);
686
715
  internalArrayStore.input.value = true;
687
716
  internalArrayStore.isTouched.value = true;
717
+ internalArrayStore.isEdited.value = true;
688
718
  internalArrayStore.isDirty.value = true;
689
719
  validateIfRequired(internalFormStore, internalArrayStore, "input");
690
720
  });
691
721
  }
722
+ /* @__NO_SIDE_EFFECTS__ */
723
+ function isDirty(form, config) {
724
+ return getFieldBool(config?.path ? getFieldStore(form[INTERNAL], config.path) : form[INTERNAL], "isDirty");
725
+ }
726
+ /* @__NO_SIDE_EFFECTS__ */
727
+ function isEdited(form, config) {
728
+ return getFieldBool(config?.path ? getFieldStore(form[INTERNAL], config.path) : form[INTERNAL], "isEdited");
729
+ }
730
+ /* @__NO_SIDE_EFFECTS__ */
731
+ function isTouched(form, config) {
732
+ return getFieldBool(config?.path ? getFieldStore(form[INTERNAL], config.path) : form[INTERNAL], "isTouched");
733
+ }
734
+ /* @__NO_SIDE_EFFECTS__ */
735
+ function isValid(form, config) {
736
+ return !getFieldBool(config?.path ? getFieldStore(form[INTERNAL], config.path) : form[INTERNAL], "errors");
737
+ }
692
738
  /**
693
739
  * Moves an item from one index to another within a field array. All items
694
740
  * between the source and destination indices are shifted accordingly.
@@ -711,6 +757,7 @@ function move(form, config) {
711
757
  else for (let index = config.from; index > config.to; index--) copyItemState(internalArrayStore.children[index - 1], internalArrayStore.children[index]);
712
758
  copyItemState(tempInternalFieldStore, internalArrayStore.children[config.to]);
713
759
  internalArrayStore.isTouched.value = true;
760
+ internalArrayStore.isEdited.value = true;
714
761
  internalArrayStore.isDirty.value = internalArrayStore.startItems.value.join() !== newItems.join();
715
762
  validateIfRequired(internalFormStore, internalArrayStore, "input");
716
763
  });
@@ -774,6 +821,7 @@ function remove(form, config) {
774
821
  internalArrayStore.items.value = newItems;
775
822
  for (let index = config.at; index < items.length - 1; index++) copyItemState(internalArrayStore.children[index + 1], internalArrayStore.children[index]);
776
823
  internalArrayStore.isTouched.value = true;
824
+ internalArrayStore.isEdited.value = true;
777
825
  internalArrayStore.isDirty.value = internalArrayStore.startItems.value.join() !== newItems.join();
778
826
  validateIfRequired(internalFormStore, internalArrayStore, "input");
779
827
  });
@@ -794,6 +842,7 @@ function replace(form, config) {
794
842
  internalArrayStore.items.value = newItems;
795
843
  resetItemState(internalArrayStore.children[config.at], config.initialInput);
796
844
  internalArrayStore.isTouched.value = true;
845
+ internalArrayStore.isEdited.value = true;
797
846
  internalArrayStore.isDirty.value = true;
798
847
  validateIfRequired(internalFormStore, internalArrayStore, "input");
799
848
  });
@@ -808,6 +857,7 @@ function reset(form, config) {
808
857
  internalFieldStore$1.elements = internalFieldStore$1.initialElements;
809
858
  if (!config?.keepErrors) internalFieldStore$1.errors.value = null;
810
859
  if (!config?.keepTouched) internalFieldStore$1.isTouched.value = false;
860
+ if (!config?.keepEdited) internalFieldStore$1.isEdited.value = false;
811
861
  internalFieldStore$1.startInput.value = internalFieldStore$1.initialInput.value;
812
862
  if (!config?.keepInput) internalFieldStore$1.input.value = internalFieldStore$1.initialInput.value;
813
863
  if (internalFieldStore$1.kind === "array") {
@@ -866,6 +916,7 @@ function swap(form, config) {
866
916
  internalArrayStore.items.value = newItems;
867
917
  swapItemState(internalArrayStore.children[config.at], internalArrayStore.children[config.and]);
868
918
  internalArrayStore.isTouched.value = true;
919
+ internalArrayStore.isEdited.value = true;
869
920
  internalArrayStore.isDirty.value = internalArrayStore.startItems.value.join() !== newItems.join();
870
921
  validateIfRequired(internalFormStore, internalArrayStore, "input");
871
922
  });
@@ -917,7 +968,10 @@ function useField(form, config) {
917
968
  useTask$(({ track, cleanup }) => {
918
969
  track(internalFieldStore);
919
970
  cleanup(() => {
920
- internalFieldStore.value.elements = internalFieldStore.value.elements.filter((element) => element.isConnected);
971
+ const internalFieldStoreValue = internalFieldStore.value;
972
+ const elements = internalFieldStoreValue.elements.filter((element) => element.isConnected);
973
+ if (internalFieldStoreValue.elements === internalFieldStoreValue.initialElements) internalFieldStoreValue.initialElements = elements;
974
+ internalFieldStoreValue.elements = elements;
921
975
  });
922
976
  });
923
977
  return useConstant(() => ({
@@ -925,6 +979,7 @@ function useField(form, config) {
925
979
  input: createComputed$(() => getFieldInput(internalFieldStore.value)),
926
980
  errors: createComputed$(() => internalFieldStore.value.errors.value),
927
981
  isTouched: createComputed$(() => getFieldBool(internalFieldStore.value, "isTouched")),
982
+ isEdited: createComputed$(() => getFieldBool(internalFieldStore.value, "isEdited")),
928
983
  isDirty: createComputed$(() => getFieldBool(internalFieldStore.value, "isDirty")),
929
984
  isValid: createComputed$(() => !getFieldBool(internalFieldStore.value, "errors")),
930
985
  onInput: $((value) => {
@@ -968,6 +1023,7 @@ function useFieldArray(form, config) {
968
1023
  items: createComputed$(() => internalFieldStore.value.items.value),
969
1024
  errors: createComputed$(() => internalFieldStore.value.errors.value),
970
1025
  isTouched: createComputed$(() => getFieldBool(internalFieldStore.value, "isTouched")),
1026
+ isEdited: createComputed$(() => getFieldBool(internalFieldStore.value, "isEdited")),
971
1027
  isDirty: createComputed$(() => getFieldBool(internalFieldStore.value, "isDirty")),
972
1028
  isValid: createComputed$(() => !getFieldBool(internalFieldStore.value, "errors"))
973
1029
  }));
@@ -1010,6 +1066,7 @@ function useFormQrl(configQrl) {
1010
1066
  isSubmitted: internalFormStore.isSubmitted,
1011
1067
  isValidating: internalFormStore.isValidating,
1012
1068
  isTouched: createComputed$(() => getFieldBool(internalFormStore, "isTouched")),
1069
+ isEdited: createComputed$(() => getFieldBool(internalFormStore, "isEdited")),
1013
1070
  isDirty: createComputed$(() => getFieldBool(internalFormStore, "isDirty")),
1014
1071
  isValid: createComputed$(() => !getFieldBool(internalFormStore, "errors")),
1015
1072
  errors: internalFormStore.errors
@@ -1084,4 +1141,4 @@ const Form = component$(({ of, onSubmit$,...other }) => {
1084
1141
  });
1085
1142
 
1086
1143
  //#endregion
1087
- export { Field, FieldArray, Form, focus, getAllErrors, getDirtyInput, getDirtyPaths, getErrors, getInput, handleSubmit, insert, move, pickDirty, remove, replace, reset, setErrors, setInput, submit, swap, useField, useFieldArray, useForm$, useFormQrl, validate };
1144
+ 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$, useFormQrl, validate };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@formisch/qwik",
3
3
  "description": "The lightweight, schema-first, and fully type-safe form library for Qwik",
4
- "version": "0.11.0",
4
+ "version": "0.13.0",
5
5
  "license": "MIT",
6
6
  "author": "Fabian Hiller",
7
7
  "homepage": "https://formisch.dev",