@formisch/react 0.4.4 → 0.4.6

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
@@ -3,13 +3,13 @@ import { ChangeEventHandler, FocusEventHandler, FormEvent, FormHTMLAttributes, R
3
3
 
4
4
  //#region ../../packages/core/dist/index.react.d.ts
5
5
 
6
- //#region src/types/schema.d.ts
6
+ //#region src/types/schema/schema.d.ts
7
7
  /**
8
8
  * Schema type.
9
9
  */
10
10
  type Schema = v.GenericSchema | v.GenericSchemaAsync;
11
11
  //#endregion
12
- //#region src/types/signal.d.ts
12
+ //#region src/types/signal/signal.d.ts
13
13
  /**
14
14
  * Signal interface.
15
15
  */
@@ -24,7 +24,7 @@ interface Signal<T> {
24
24
  */
25
25
 
26
26
  //#endregion
27
- //#region src/types/field.d.ts
27
+ //#region src/types/field/field.d.ts
28
28
  /**
29
29
  * Field element type.
30
30
  */
@@ -184,7 +184,7 @@ interface InternalValueStore extends InternalBaseStore {
184
184
  */
185
185
  type InternalFieldStore = InternalArrayStore | InternalObjectStore | InternalValueStore;
186
186
  //#endregion
187
- //#region src/types/utils.d.ts
187
+ //#region src/types/utils/utils.d.ts
188
188
  /**
189
189
  * Checks if a type is `any`.
190
190
  */
@@ -216,7 +216,7 @@ type PartialValues<TValue> = TValue extends readonly (infer TItem)[] ? number ex
216
216
  */
217
217
  declare const INTERNAL: "~internal";
218
218
  //#endregion
219
- //#region src/types/form.d.ts
219
+ //#region src/types/form/form.d.ts
220
220
  /**
221
221
  * Validation mode type.
222
222
  */
@@ -291,7 +291,7 @@ interface BaseFormStore<TSchema extends Schema = Schema> {
291
291
  readonly [INTERNAL]: InternalFormStore<TSchema>;
292
292
  }
293
293
  //#endregion
294
- //#region src/types/form.react.d.ts
294
+ //#region src/types/form/form.react.d.ts
295
295
  /**
296
296
  * Submit handler type.
297
297
  */
@@ -301,7 +301,7 @@ type SubmitHandler<TSchema extends Schema> = (output: v.InferOutput<TSchema>) =>
301
301
  */
302
302
  type SubmitEventHandler<TSchema extends Schema> = (output: v.InferOutput<TSchema>, event: FormEvent<HTMLFormElement>) => MaybePromise<unknown>;
303
303
  //#endregion
304
- //#region src/types/path.d.ts
304
+ //#region src/types/path/path.d.ts
305
305
  /**
306
306
  * Path key type.
307
307
  */
@@ -315,42 +315,88 @@ type Path = readonly PathKey[];
315
315
  */
316
316
  type RequiredPath = readonly [PathKey, ...Path];
317
317
  /**
318
- * Extracts the exact keys of a tuple, array or object.
318
+ * Extracts the exact keys of a tuple, array or object. Tuples return their
319
+ * literal numeric indices, dynamic arrays return `number`, objects return
320
+ * their `keyof` keys, and any other input returns `never`.
319
321
  */
320
- type KeyOf<TValue> = IsAny<TValue> extends true ? never : TValue extends readonly unknown[] ? number extends TValue["length"] ? number : { [TKey in keyof TValue]: TKey extends `${infer TIndex extends number}` ? TIndex : never }[number] : TValue extends Record<string, unknown> ? keyof TValue & PathKey : never;
322
+ type ExactKeysOf<TValue> = IsAny<TValue> extends true ? never : TValue extends readonly unknown[] ? number extends TValue["length"] ? number : { [TKey in keyof TValue]: TKey extends `${infer TIndex extends number}` ? TIndex : never }[number] : TValue extends Record<PropertyKey, unknown> ? keyof TValue & PathKey : never;
321
323
  /**
322
- * Merges array and object unions into a single object.
324
+ * Returns the flat object of all indexable properties of `TValue`. For object
325
+ * unions, properties from every member are merged so that any single property
326
+ * is accessible. For primitives and other non-indexable types, the result is
327
+ * `{}`.
323
328
  *
324
- * Hint: This is necessary to make any property accessible. By default,
325
- * properties that do not exist in all union options are not accessible
326
- * and result in "any" when accessed.
329
+ * Hint: This is necessary to make properties accessible across union members.
330
+ * By default, properties that do not exist in all union options are not
331
+ * accessible and result in "any" when accessed.
327
332
  */
328
- type MergeUnion<TValue> = { [TKey in KeyOf<TValue>]: TValue extends Record<TKey, infer TItem> ? TItem : never };
333
+ type PropertiesOf<TValue> = { [TKey in ExactKeysOf<TValue>]: TValue extends Record<TKey, infer TItem> ? TItem : never };
329
334
  /**
330
335
  * Lazily evaluates only the first valid path segment based on the given value.
331
336
  */
332
- type LazyPath<TValue, TPathToCheck extends Path, TValidPath extends Path = readonly []> = TPathToCheck extends readonly [] ? TValidPath : TPathToCheck extends readonly [infer TFirstKey extends KeyOf<TValue>, ...infer TPathRest extends Path] ? LazyPath<Required<MergeUnion<TValue>[TFirstKey]>, TPathRest, readonly [...TValidPath, TFirstKey]> : IsNever<KeyOf<TValue>> extends false ? readonly [...TValidPath, KeyOf<TValue>] : TValidPath;
337
+ type LazyPath<TValue, TPathToCheck extends Path, TValidPath extends Path = readonly []> = TPathToCheck extends readonly [] ? TValidPath : TPathToCheck extends readonly [infer TFirstKey extends ExactKeysOf<TValue>, ...infer TPathRest extends Path] ? LazyPath<Required<PropertiesOf<TValue>[TFirstKey]>, TPathRest, readonly [...TValidPath, TFirstKey]> : IsNever<ExactKeysOf<TValue>> extends false ? readonly [...TValidPath, ExactKeysOf<TValue>] : TValidPath;
333
338
  /**
334
339
  * Returns the path if valid, otherwise the first possible valid path based on
335
340
  * the given value.
336
341
  */
337
342
  type ValidPath<TValue, TPath extends RequiredPath> = TPath extends LazyPath<Required<TValue>, TPath> ? TPath : LazyPath<Required<TValue>, TPath>;
338
343
  /**
344
+ * Detects whether the consuming project is configured with
345
+ * `exactOptionalPropertyTypes: true`.
346
+ *
347
+ * Hint: If `false` the built-in `Required<T>` strips `| undefined` from
348
+ * optional properties, so `Required<{ key?: undefined }>['key']` collapses
349
+ * to `never` — under strict mode the same expression yields `undefined`.
350
+ */
351
+ type IsExactOptionalProps = Required<{
352
+ key?: undefined;
353
+ }>["key"] extends never ? false : true;
354
+ /**
355
+ * Like the built-in `Required<T>`, but preserves `| undefined` in two
356
+ * places where `Required<T>` strips it:
357
+ *
358
+ * 1. Optional property values under `exactOptionalPropertyTypes: false`
359
+ * — without this, input typings for `v.optional`/`v.nullish` schemas
360
+ * narrow incorrectly (issue #15).
361
+ * 2. Array/tuple element types — e.g. `(string | undefined)[]` stays
362
+ * `(string | undefined)[]` instead of becoming `string[]`. Arrays
363
+ * fall through unchanged because they only have a numeric index
364
+ * signature and don't structurally extend `Record<PropertyKey,
365
+ * unknown>` (which requires string keys).
366
+ */
367
+ type ExactRequired<TValue> = TValue extends Record<PropertyKey, unknown> ? IsExactOptionalProps extends true ? Required<TValue> : { [TKey in keyof Required<TValue>]: TValue[TKey] } : TValue;
368
+ /**
339
369
  * Extracts the value type at the given path.
340
370
  */
341
- type PathValue<TValue, TPath extends Path> = TPath extends readonly [infer TKey, ...infer TRest extends Path] ? TKey extends KeyOf<Required<TValue>> ? PathValue<MergeUnion<Required<TValue>>[TKey], TRest> : unknown : TValue;
371
+ 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;
342
372
  /**
343
- * Checks if a value is an array or contains one.
373
+ * Checks whether a value is an array or contains one anywhere in its shape.
374
+ *
375
+ * Hint: The inner conditionals (`TValue extends readonly unknown[]` and
376
+ * `TValue extends Record<PropertyKey, unknown>`) distribute over union members,
377
+ * so the inner expression returns the union of each member's result (e.g.
378
+ * `true | false` when some members contain arrays and others don't).
379
+ * Downstream code uses `IsOrHasArray<T> extends true`, but
380
+ * `boolean extends true` is `false` — so we collapse the result via
381
+ * `true extends ...`, which is `true` whenever at least one union member
382
+ * contributed `true`.
344
383
  */
345
- type IsOrHasArray<TValue> = IsAny<TValue> extends true ? false : TValue extends readonly unknown[] ? true : TValue extends Record<string, unknown> ? true extends { [TKey in keyof TValue]: IsOrHasArray<TValue[TKey]> }[keyof TValue] ? true : false : false;
384
+ 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;
346
385
  /**
347
386
  * Extracts the exact keys of a tuple, array or object that contain arrays.
348
387
  */
349
- type KeyOfArrayPath<TValue> = IsAny<TValue> extends true ? never : TValue extends readonly (infer TItem)[] ? number extends TValue["length"] ? IsOrHasArray<TItem> extends true ? number : never : { [TKey in keyof TValue]: TKey extends `${infer TIndex extends number}` ? IsOrHasArray<NonNullable<TValue[TKey]>> extends true ? TIndex : never : never }[number] : TValue extends Record<string, unknown> ? { [TKey in keyof TValue]: IsOrHasArray<NonNullable<TValue[TKey]>> extends true ? TKey : never }[keyof TValue] & PathKey : never;
388
+ type ExactKeysOfArrayPath<TValue> = IsAny<TValue> extends true ? never : TValue extends readonly (infer TItem)[] ? number extends TValue["length"] ? IsOrHasArray<TItem> extends true ? number : never : { [TKey in keyof TValue]: TKey extends `${infer TIndex extends number}` ? IsOrHasArray<NonNullable<TValue[TKey]>> extends true ? TIndex : never : never }[number] : TValue extends Record<PropertyKey, unknown> ? { [TKey in keyof TValue]: IsOrHasArray<NonNullable<TValue[TKey]>> extends true ? TKey : never }[keyof TValue] & PathKey : never;
389
+ /**
390
+ * Returns the flat object of indexable properties of `TValue` whose values
391
+ * are or contain arrays. Mirrors `PropertiesOf` but keyed by
392
+ * `ExactKeysOfArrayPath` so the lookup is provably valid for array-path
393
+ * navigation in `LazyArrayPath`.
394
+ */
395
+ type PropertiesOfArrayPath<TValue> = { [TKey in ExactKeysOfArrayPath<TValue>]: TValue extends Record<TKey, infer TItem> ? TItem : never };
350
396
  /**
351
397
  * Lazily evaluates only the first valid array path segment based on the given value.
352
398
  */
353
- type LazyArrayPath<TValue, TPathToCheck extends Path, TValidPath extends Path = readonly []> = TPathToCheck extends readonly [] ? TValue extends readonly unknown[] ? TValidPath : readonly [...TValidPath, KeyOfArrayPath<TValue>] : TPathToCheck extends readonly [infer TFirstKey extends KeyOfArrayPath<TValue>, ...infer TPathRest extends Path] ? LazyArrayPath<Required<MergeUnion<TValue>[TFirstKey]>, TPathRest, readonly [...TValidPath, TFirstKey]> : IsNever<KeyOfArrayPath<TValue>> extends false ? readonly [...TValidPath, KeyOfArrayPath<TValue>] : never;
399
+ 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;
354
400
  /**
355
401
  * Returns the path if valid, otherwise the first possible valid array path
356
402
  * based on the given value.
package/dist/index.js CHANGED
@@ -162,7 +162,7 @@ function initializeFieldStore(internalFieldStore, schema, initialInput, path, nu
162
162
  if (internalFieldStore.kind === "object") {
163
163
  internalFieldStore.children ??= {};
164
164
  for (const key in schema.entries) {
165
- internalFieldStore.children[key] = {};
165
+ internalFieldStore.children[key] ??= {};
166
166
  path.push(key);
167
167
  initializeFieldStore(internalFieldStore.children[key], schema.entries[key], initialInput?.[key], path);
168
168
  path.pop();
@@ -173,6 +173,7 @@ function initializeFieldStore(internalFieldStore, schema, initialInput, path, nu
173
173
  internalFieldStore.input = /* @__PURE__ */ createSignal(objectInput);
174
174
  }
175
175
  } else {
176
+ if (internalFieldStore.kind && internalFieldStore.kind !== "value") throw new Error(`Store initialized as "${internalFieldStore.kind}" cannot be reinitialized as "value"`);
176
177
  internalFieldStore.kind = "value";
177
178
  if (internalFieldStore.kind === "value") {
178
179
  internalFieldStore.initialInput = /* @__PURE__ */ createSignal(initialInput);
@@ -644,6 +645,7 @@ function focus(form, config) {
644
645
  function getAllErrors(form) {
645
646
  let allErrors = null;
646
647
  walkFieldStore(form[INTERNAL], (internalFieldStore) => {
648
+ if (internalFieldStore.kind === "array") internalFieldStore.items.value;
647
649
  const errors = internalFieldStore.errors.value;
648
650
  if (errors) if (allErrors) allErrors.push(...errors);
649
651
  else allErrors = [...errors];
@@ -700,7 +702,15 @@ function insert(form, config) {
700
702
  const newItems = [...items];
701
703
  newItems.splice(insertIndex, 0, createId());
702
704
  internalArrayStore.items.value = newItems;
703
- for (let index = items.length; index > insertIndex; index--) copyItemState(internalArrayStore.children[index - 1], internalArrayStore.children[index]);
705
+ for (let index = items.length; index > insertIndex; index--) {
706
+ if (!internalArrayStore.children[index]) {
707
+ const path = JSON.parse(internalArrayStore.name);
708
+ internalArrayStore.children[index] = {};
709
+ path.push(index);
710
+ initializeFieldStore(internalArrayStore.children[index], internalArrayStore.schema.item, void 0, path);
711
+ }
712
+ copyItemState(internalArrayStore.children[index - 1], internalArrayStore.children[index]);
713
+ }
704
714
  if (!internalArrayStore.children[insertIndex]) {
705
715
  const path = JSON.parse(internalArrayStore.name);
706
716
  internalArrayStore.children[insertIndex] = {};
@@ -785,7 +795,7 @@ function reset(form, config) {
785
795
  untrack(() => {
786
796
  const internalFormStore = form[INTERNAL];
787
797
  const internalFieldStore = config?.path ? getFieldStore(internalFormStore, config.path) : internalFormStore;
788
- if (config?.initialInput) setInitialFieldInput(internalFieldStore, config.initialInput);
798
+ if (config && "initialInput" in config) setInitialFieldInput(internalFieldStore, config.initialInput);
789
799
  walkFieldStore(internalFieldStore, (internalFieldStore$1) => {
790
800
  internalFieldStore$1.elements = internalFieldStore$1.initialElements;
791
801
  if (!config?.keepErrors) internalFieldStore$1.errors.value = null;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@formisch/react",
3
- "description": "The modular and type-safe form library for React",
4
- "version": "0.4.4",
3
+ "description": "The lightweight, schema-first, and fully type-safe form library for React",
4
+ "version": "0.4.6",
5
5
  "license": "MIT",
6
6
  "author": "Fabian Hiller",
7
7
  "homepage": "https://formisch.dev",
@@ -34,6 +34,7 @@
34
34
  },
35
35
  "scripts": {
36
36
  "build": "tsdown",
37
+ "test": "vitest run --typecheck",
37
38
  "lint": "eslint \"src/**/*.ts*\" && tsc --noEmit",
38
39
  "lint.fix": "eslint \"src/**/*.ts*\" --fix",
39
40
  "format": "prettier --write ./src",
@@ -43,19 +44,25 @@
43
44
  "@formisch/core": "workspace:*",
44
45
  "@formisch/eslint-config": "workspace:*",
45
46
  "@formisch/methods": "workspace:*",
47
+ "@testing-library/dom": "^10.4.0",
48
+ "@testing-library/jest-dom": "^6.6.0",
49
+ "@testing-library/react": "^16.3.0",
46
50
  "@types/node": "^24.10.1",
47
51
  "@types/react": "^19.2.5",
48
52
  "@types/react-dom": "^19.2.3",
49
53
  "@vitejs/plugin-react": "^5.1.1",
54
+ "@vitest/coverage-v8": "^3.2.4",
50
55
  "eslint": "^9.39.1",
51
56
  "eslint-plugin-react-hooks": "^7.0.1",
52
57
  "eslint-plugin-react-refresh": "^0.4.24",
53
58
  "globals": "^16.5.0",
59
+ "jsdom": "^26.1.0",
54
60
  "react": "^19.2.1",
55
61
  "react-dom": "^19.2.1",
56
62
  "tsdown": "^0.16.8",
57
63
  "typescript": "~5.9.3",
58
- "vite": "^7.2.4"
64
+ "vite": "^7.2.4",
65
+ "vitest": "^3.2.4"
59
66
  },
60
67
  "peerDependencies": {
61
68
  "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",