@formisch/qwik 0.11.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.d.ts CHANGED
@@ -74,6 +74,17 @@ interface InternalBaseStore {
74
74
  */
75
75
  schema: NoSerialize<Schema>;
76
76
  /**
77
+ * The initial elements of the field.
78
+ *
79
+ * Hint: This may look unused, but do not remove it. `copyItemState` and
80
+ * `swapItemState` move the `elements` reference between field stores when
81
+ * array items are inserted, moved, removed or swapped, and `reset` restores
82
+ * each field's original element via `elements = initialElements`. Without it,
83
+ * focus and file reset target the wrong element after a reorder followed by a
84
+ * reset.
85
+ */
86
+ initialElements: FieldElement[];
87
+ /**
77
88
  * The elements of the field.
78
89
  */
79
90
  elements: FieldElement[];
@@ -202,14 +213,6 @@ interface InternalValueStore extends InternalBaseStore {
202
213
  * The input of the value field.
203
214
  */
204
215
  input: Signal<unknown>;
205
- /**
206
- * The touched state of the field.
207
- */
208
- isTouched: Signal<boolean>;
209
- /**
210
- * The dirty state of the field.
211
- */
212
- isDirty: Signal<boolean>;
213
216
  }
214
217
  /**
215
218
  * Internal field store type.
@@ -402,7 +405,10 @@ type ExactRequired<TValue> = TValue extends Record<PropertyKey, unknown> ? IsExa
402
405
  */
403
406
  type PathValue<TValue, TPath extends Path> = TPath extends readonly [infer TKey, ...infer TRest extends Path] ? TKey extends ExactKeysOf<ExactRequired<TValue>> ? PathValue<PropertiesOf<ExactRequired<TValue>>[TKey], TRest> : unknown : TValue;
404
407
  /**
405
- * Checks whether a value is an array or contains one anywhere in its shape.
408
+ * Checks whether a value is a dynamic array or contains one anywhere in its
409
+ * shape. A fixed-length tuple is not itself a dynamic array, but it counts when
410
+ * it contains one, so paths can still navigate through tuples to reach nested
411
+ * arrays.
406
412
  *
407
413
  * Hint: The inner conditionals (`TValue extends readonly unknown[]` and
408
414
  * `TValue extends Record<PropertyKey, unknown>`) distribute over union members,
@@ -413,7 +419,7 @@ type PathValue<TValue, TPath extends Path> = TPath extends readonly [infer TKey,
413
419
  * `true extends ...`, which is `true` whenever at least one union member
414
420
  * contributed `true`.
415
421
  */
416
- type IsOrHasArray<TValue> = true extends (IsAny<TValue> extends true ? false : TValue extends readonly unknown[] ? true : TValue extends Record<PropertyKey, unknown> ? { [TKey in keyof TValue]: IsOrHasArray<TValue[TKey]> }[keyof TValue] : false) ? true : false;
422
+ type IsOrHasArray<TValue> = true extends (IsAny<TValue> extends true ? false : TValue extends readonly unknown[] ? number extends TValue["length"] ? true : IsOrHasArray<TValue[number]> : TValue extends Record<PropertyKey, unknown> ? { [TKey in keyof TValue]: IsOrHasArray<TValue[TKey]> }[keyof TValue] : false) ? true : false;
417
423
  /**
418
424
  * Extracts the exact keys of a tuple, array or object that contain arrays.
419
425
  */
@@ -428,7 +434,7 @@ type PropertiesOfArrayPath<TValue> = { [TKey in ExactKeysOfArrayPath<TValue>]: T
428
434
  /**
429
435
  * Lazily evaluates only the first valid array path segment based on the given value.
430
436
  */
431
- type LazyArrayPath<TValue, TPathToCheck extends Path, TValidPath extends Path = readonly []> = TPathToCheck extends readonly [] ? TValue extends readonly unknown[] ? TValidPath : readonly [...TValidPath, ExactKeysOfArrayPath<TValue>] : TPathToCheck extends readonly [infer TFirstKey extends ExactKeysOfArrayPath<TValue>, ...infer TPathRest extends Path] ? LazyArrayPath<Required<PropertiesOfArrayPath<TValue>[TFirstKey]>, TPathRest, readonly [...TValidPath, TFirstKey]> : IsNever<ExactKeysOfArrayPath<TValue>> extends false ? readonly [...TValidPath, ExactKeysOfArrayPath<TValue>] : never;
437
+ type LazyArrayPath<TValue, TPathToCheck extends Path, TValidPath extends Path = readonly []> = TPathToCheck extends readonly [] ? TValue extends readonly unknown[] ? number extends TValue["length"] ? TValidPath : IsNever<ExactKeysOfArrayPath<TValue>> extends false ? readonly [...TValidPath, ExactKeysOfArrayPath<TValue>] : never : readonly [...TValidPath, ExactKeysOfArrayPath<TValue>] : TPathToCheck extends readonly [infer TFirstKey extends ExactKeysOfArrayPath<TValue>, ...infer TPathRest extends Path] ? LazyArrayPath<Required<PropertiesOfArrayPath<TValue>[TFirstKey]>, TPathRest, readonly [...TValidPath, TFirstKey]> : IsNever<ExactKeysOfArrayPath<TValue>> extends false ? readonly [...TValidPath, ExactKeysOfArrayPath<TValue>] : never;
432
438
  /**
433
439
  * Returns the path if valid, otherwise the first possible valid array path
434
440
  * based on the given value.
@@ -481,7 +487,7 @@ interface FocusFieldConfig<TSchema extends FormSchema, TFieldPath extends Requir
481
487
  readonly path: ValidPath<v.InferInput<TSchema>, TFieldPath>;
482
488
  }
483
489
  /**
484
- * Focuses the first input element of a field. This is useful for
490
+ * Focuses the first focusable input element of a field. This is useful for
485
491
  * programmatically setting focus to a specific field, such as after
486
492
  * validation errors or user interactions.
487
493
  *
@@ -152,31 +152,45 @@ function copyItemState(fromInternalFieldStore, toInternalFieldStore) {
152
152
  * form reset functionality.
153
153
  *
154
154
  * @param internalFieldStore The field store to reset.
155
- * @param initialInput The new input value (can be any type including array or object).
155
+ * @param input The new input value (can be any type including array or object).
156
+ * @param keepStart Whether to keep `startInput` and `startItems` as the dirty
157
+ * baseline instead of resetting them to the new input. Used when a field store
158
+ * is reused for an in-place edit so its dirty state is detected correctly.
156
159
  */
157
- function resetItemState(internalFieldStore, initialInput) {
160
+ function resetItemState(internalFieldStore, input, keepStart = false) {
158
161
  batch(() => {
159
- internalFieldStore.elements = [];
162
+ const elements = [];
163
+ if (internalFieldStore.elements === internalFieldStore.initialElements) internalFieldStore.initialElements = elements;
164
+ internalFieldStore.elements = elements;
160
165
  internalFieldStore.errors.value = null;
161
166
  internalFieldStore.isTouched.value = false;
162
167
  internalFieldStore.isDirty.value = false;
163
168
  if (internalFieldStore.kind === "array" || internalFieldStore.kind === "object") {
164
- const objectInput = initialInput == null ? initialInput : true;
165
- internalFieldStore.startInput.value = objectInput;
169
+ const objectInput = input == null ? input : true;
170
+ if (!keepStart) internalFieldStore.startInput.value = objectInput;
166
171
  internalFieldStore.input.value = objectInput;
167
- if (internalFieldStore.kind === "array") if (initialInput) {
168
- const newItems = initialInput.map(createId);
169
- internalFieldStore.startItems.value = newItems;
172
+ if (internalFieldStore.kind === "array") if (input) {
173
+ const length = internalFieldStore.schema.type === "array" ? input.length : internalFieldStore.children.length;
174
+ const newItems = Array.from({ length }, createId);
175
+ if (!keepStart) internalFieldStore.startItems.value = newItems;
170
176
  internalFieldStore.items.value = newItems;
171
- for (let index = 0; index < initialInput.length; index++) if (internalFieldStore.children[index]) resetItemState(internalFieldStore.children[index], initialInput[index]);
177
+ let path;
178
+ for (let index = 0; index < length; index++) if (internalFieldStore.children[index]) resetItemState(internalFieldStore.children[index], input[index], keepStart);
179
+ else {
180
+ path ??= JSON.parse(internalFieldStore.name);
181
+ internalFieldStore.children[index] = {};
182
+ path.push(index);
183
+ initializeFieldStore(internalFieldStore.children[index], internalFieldStore.schema.item, input[index], path);
184
+ path.pop();
185
+ }
172
186
  } else {
173
- internalFieldStore.startItems.value = [];
187
+ if (!keepStart) internalFieldStore.startItems.value = [];
174
188
  internalFieldStore.items.value = [];
175
189
  }
176
- else for (const key in internalFieldStore.children) resetItemState(internalFieldStore.children[key], initialInput?.[key]);
190
+ else for (const key in internalFieldStore.children) resetItemState(internalFieldStore.children[key], input?.[key], keepStart);
177
191
  } else {
178
- internalFieldStore.startInput.value = initialInput;
179
- internalFieldStore.input.value = initialInput;
192
+ if (!keepStart) internalFieldStore.startInput.value = input;
193
+ internalFieldStore.input.value = input;
180
194
  }
181
195
  });
182
196
  }
@@ -243,6 +257,28 @@ function swapItemState(firstInternalFieldStore, secondInternalFieldStore) {
243
257
  });
244
258
  }
245
259
  /**
260
+ * Focuses the first focusable element of a field store. The elements are tried
261
+ * in order and the first one that actually receives focus wins, so detached,
262
+ * disabled or hidden elements are skipped. The browser decides focusability,
263
+ * which is read back via the element's root `activeElement` so elements in a
264
+ * shadow root or another document are handled correctly.
265
+ *
266
+ * Hint: A `display: none` or `hidden` element is correctly skipped in real
267
+ * browsers, but jsdom has no layout and focuses it anyway, so that case cannot
268
+ * be covered by unit tests.
269
+ *
270
+ * @param internalFieldStore The field store to focus.
271
+ *
272
+ * @returns Whether an element was focused.
273
+ */
274
+ function focusFieldElement(internalFieldStore) {
275
+ for (const element of internalFieldStore.elements) {
276
+ element.focus();
277
+ if (element.getRootNode().activeElement === element) return true;
278
+ }
279
+ return false;
280
+ }
281
+ /**
246
282
  * Returns whether the specified boolean property is true for the field store
247
283
  * or any of its nested children. Recursively checks arrays and objects.
248
284
  *
@@ -380,11 +416,9 @@ function getFieldStore(internalFormStore, path) {
380
416
  */
381
417
  function setFieldBool(internalFieldStore, type, bool) {
382
418
  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;
419
+ internalFieldStore[type].value = bool;
420
+ if (internalFieldStore.kind === "array") for (let index = 0; index < untrack(() => internalFieldStore.items.value).length; index++) setFieldBool(internalFieldStore.children[index], type, bool);
421
+ else if (internalFieldStore.kind === "object") for (const key in internalFieldStore.children) setFieldBool(internalFieldStore.children[key], type, bool);
388
422
  });
389
423
  }
390
424
  /**
@@ -399,20 +433,20 @@ function setNestedInput(internalFieldStore, input) {
399
433
  if (internalFieldStore.kind === "array") {
400
434
  const arrayInput = input ?? [];
401
435
  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
- }
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
+ const path = JSON.parse(internalFieldStore.name);
440
+ for (let index = items.length; index < length; index++) if (internalFieldStore.children[index]) resetItemState(internalFieldStore.children[index], arrayInput[index], true);
441
+ else {
442
+ internalFieldStore.children[index] = {};
443
+ path.push(index);
444
+ initializeFieldStore(internalFieldStore.children[index], internalFieldStore.schema.item, arrayInput[index], path);
445
+ path.pop();
412
446
  }
413
- internalFieldStore.items.value = [...items, ...arrayInput.slice(items.length).map(createId)];
447
+ internalFieldStore.items.value = [...items, ...Array.from({ length: length - items.length }, createId)];
414
448
  }
415
- for (let index = 0; index < arrayInput.length; index++) setNestedInput(internalFieldStore.children[index], arrayInput[index]);
449
+ for (let index = 0; index < length; index++) setNestedInput(internalFieldStore.children[index], arrayInput[index]);
416
450
  internalFieldStore.input.value = input == null ? input : true;
417
451
  internalFieldStore.isDirty.value = internalFieldStore.startInput.value !== internalFieldStore.input.value || internalFieldStore.startItems.value.length !== internalFieldStore.items.value.length;
418
452
  } else if (internalFieldStore.kind === "object") {
@@ -456,21 +490,22 @@ function setFieldInput(internalFormStore, path, input) {
456
490
  function setInitialFieldInput(internalFieldStore, initialInput) {
457
491
  batch(() => {
458
492
  if (internalFieldStore.kind === "array") {
459
- internalFieldStore.input.value = initialInput == null ? initialInput : true;
493
+ internalFieldStore.initialInput.value = initialInput == null ? initialInput : true;
460
494
  const initialArrayInput = initialInput ?? [];
461
- if (initialArrayInput.length > internalFieldStore.children.length) {
495
+ const length = internalFieldStore.schema.type === "array" ? initialArrayInput.length : internalFieldStore.children.length;
496
+ if (length > internalFieldStore.children.length) {
462
497
  const path = JSON.parse(internalFieldStore.name);
463
- for (let index = internalFieldStore.children.length; index < initialArrayInput.length; index++) {
498
+ for (let index = internalFieldStore.children.length; index < length; index++) {
464
499
  internalFieldStore.children[index] = {};
465
500
  path.push(index);
466
501
  initializeFieldStore(internalFieldStore.children[index], internalFieldStore.schema.item, initialArrayInput[index], path);
467
502
  path.pop();
468
503
  }
469
504
  }
470
- internalFieldStore.initialItems.value = initialArrayInput.map(createId);
505
+ internalFieldStore.initialItems.value = Array.from({ length }, createId);
471
506
  for (let index = 0; index < internalFieldStore.children.length; index++) setInitialFieldInput(internalFieldStore.children[index], initialArrayInput[index]);
472
507
  } else if (internalFieldStore.kind === "object") {
473
- internalFieldStore.input.value = initialInput == null ? initialInput : true;
508
+ internalFieldStore.initialInput.value = initialInput == null ? initialInput : true;
474
509
  for (const key in internalFieldStore.children) setInitialFieldInput(internalFieldStore.children[key], initialInput?.[key]);
475
510
  } else internalFieldStore.initialInput.value = initialInput;
476
511
  });
@@ -522,44 +557,49 @@ function createFormStore(config, parse) {
522
557
  async function validateFormInput(internalFormStore, config) {
523
558
  internalFormStore.validators++;
524
559
  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;
560
+ try {
561
+ const result = await internalFormStore.parse(untrack(() => /* @__PURE__ */ getFieldInput(internalFormStore)));
562
+ let rootErrors;
563
+ let nestedErrors;
564
+ if (result.issues) {
565
+ nestedErrors = {};
566
+ for (const issue of result.issues) if (issue.path) {
567
+ const path = [];
568
+ for (const pathItem of issue.path) {
569
+ const key = pathItem.key;
570
+ const keyType = typeof key;
571
+ const itemType = pathItem.type;
572
+ if (keyType !== "string" && keyType !== "number" || itemType === "map" || itemType === "set") break;
573
+ path.push(key);
556
574
  }
557
- }
575
+ const name = JSON.stringify(path);
576
+ const fieldErrors = nestedErrors[name];
577
+ if (fieldErrors) fieldErrors.push(issue.message);
578
+ else nestedErrors[name] = [issue.message];
579
+ } else if (rootErrors) rootErrors.push(issue.message);
580
+ else rootErrors = [issue.message];
581
+ }
582
+ let shouldFocus = config?.shouldFocus ?? false;
583
+ batch(() => {
584
+ walkFieldStore(internalFormStore, (internalFieldStore) => {
585
+ if (internalFieldStore.name === "[]") internalFieldStore.errors.value = rootErrors ?? null;
586
+ else {
587
+ const fieldErrors = nestedErrors?.[internalFieldStore.name] ?? null;
588
+ internalFieldStore.errors.value = fieldErrors;
589
+ if (shouldFocus && fieldErrors && focusFieldElement(internalFieldStore)) shouldFocus = false;
590
+ }
591
+ });
592
+ internalFormStore.validators--;
593
+ internalFormStore.isValidating.value = internalFormStore.validators > 0;
558
594
  });
559
- internalFormStore.validators--;
560
- internalFormStore.isValidating.value = internalFormStore.validators > 0;
561
- });
562
- return result;
595
+ return result;
596
+ } catch (error) {
597
+ batch(() => {
598
+ internalFormStore.validators--;
599
+ internalFormStore.isValidating.value = internalFormStore.validators > 0;
600
+ });
601
+ throw error;
602
+ }
563
603
  }
564
604
  /**
565
605
  * Validates the form input if required based on the validation mode and form
@@ -581,7 +621,7 @@ const INTERNAL = "~internal";
581
621
  //#endregion
582
622
  //#region ../../packages/methods/dist/index.qwik.js
583
623
  /**
584
- * Focuses the first input element of a field. This is useful for
624
+ * Focuses the first focusable input element of a field. This is useful for
585
625
  * programmatically setting focus to a specific field, such as after
586
626
  * validation errors or user interactions.
587
627
  *
@@ -589,7 +629,7 @@ const INTERNAL = "~internal";
589
629
  * @param config The focus field configuration.
590
630
  */
591
631
  function focus(form, config) {
592
- getFieldStore(form[INTERNAL], config.path).elements[0]?.focus();
632
+ focusFieldElement(getFieldStore(form[INTERNAL], config.path));
593
633
  }
594
634
  /**
595
635
  * Retrieves all error messages from all fields in the form by walking through
@@ -917,7 +957,10 @@ function useField(form, config) {
917
957
  useTask$(({ track, cleanup }) => {
918
958
  track(internalFieldStore);
919
959
  cleanup(() => {
920
- internalFieldStore.value.elements = internalFieldStore.value.elements.filter((element) => element.isConnected);
960
+ const internalFieldStoreValue = internalFieldStore.value;
961
+ const elements = internalFieldStoreValue.elements.filter((element) => element.isConnected);
962
+ if (internalFieldStoreValue.elements === internalFieldStoreValue.initialElements) internalFieldStoreValue.initialElements = elements;
963
+ internalFieldStoreValue.elements = elements;
921
964
  });
922
965
  });
923
966
  return useConstant(() => ({
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.12.0",
5
5
  "license": "MIT",
6
6
  "author": "Fabian Hiller",
7
7
  "homepage": "https://formisch.dev",