formstand 0.2.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,8 +1,90 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 — 2026-07-08
4
+
5
+ ### Breaking
6
+
7
+ - The deprecated `useFormState` / `useFormStateShallow` aliases are removed —
8
+ use `useFormSelector` / `useFormSelectorShallow` (renamed in 0.2.0 because
9
+ React DOM ships its own, deprecated, `useFormState`).
10
+ - Bound components' `path` prop is schema-typed when `form` is a
11
+ `Form<TSchema>` (`PathsOf<F>`): typo'd paths are now compile errors, and
12
+ runtime-built strings need the documented cast. A structural `FieldFormApi`
13
+ form keeps plain `string`.
14
+ - `focusFirstError`'s root-`""` fallback no longer fires under the default
15
+ `document` scope when the page holds more than one `<form>` — "first
16
+ control" would be a guess, so it returns `false`. Pass the form element
17
+ (e.g. via a ref) as `root`.
18
+ - `SubmitResult` gained `{ kind: "error", error }`: when `onValid` throws or
19
+ rejects, `submit` now **resolves** with that result instead of rejecting
20
+ (so `handleSubmit` never leaves an unhandled rejection). Code that relied
21
+ on catching the rejection must check `result.kind === "error"`.
22
+ - `adoptValues` now clears the in-flight validation flags
23
+ (`isValidating` / `isValidatingForm`) along with the errors it already
24
+ cleared — the rebase disowns in-flight passes.
25
+ - `submit` skips its error/touched state writes when `values` changed while
26
+ validation was in flight (a `reset` during an in-flight submit no longer
27
+ gets stale errors committed over the fresh form); `onValid`/`onInvalid`
28
+ still run and the result still reports the outcome.
29
+
30
+ ### Added
31
+
32
+ - `focusField(path, root?)` — imperative focus by path (the `setFocus` of
33
+ react-hook-form), sharing `focusFirstError`'s focusability rules.
34
+ - `emptyValueForSchema(schema)` is exported — the schema-introspection rule
35
+ behind `useField().emptyValue`, alongside its adapter siblings
36
+ `numberToInputText` / `parseNumberText`.
37
+ - `field.setError` (from `useField`) accepts a single string, matching
38
+ `form.setError`.
39
+ - Dev-mode warning (once per path) when `validateField` /
40
+ `validateFieldAsync` target a path the schema provably cannot contain —
41
+ protects the docs-sanctioned dynamic-path casts from silent always-valid
42
+ results.
43
+ - Docs: a migrating-from-react-hook-form guide with the full API mapping
44
+ table.
45
+
46
+ ### Fixed
47
+
48
+ - Array ops no longer strand in-flight `isValidating` flags: flags under the
49
+ path are dropped rather than re-keyed (the completing pass clears the
50
+ original key, so a re-keyed flag could never be cleared).
51
+ - The validation sequence counter is globally monotonic and never resets, so
52
+ a superseded pre-reset validation pass can no longer collide with a
53
+ post-reset one and clobber its state.
54
+ - `validateFieldAsync("")` delegates to the whole-form pass, so its pending
55
+ state lives in `isValidatingForm` instead of `isValidating[""]`.
56
+ - `focusFirstError` / `focusField` report success only when a control
57
+ actually **holds** focus — hidden, disabled, closed-`<dialog>`, and
58
+ focus-refusing matches are passed over for the next candidate in DOM
59
+ order.
60
+ - `dirtyFields()` / `diff()` report an object that diverges only by an
61
+ `undefined`-valued key, agreeing with `useIsDirty`.
62
+ - `SelectField` tolerates duplicate option values (React keys no longer
63
+ collide).
64
+ - `useField` typo errors blame the path argument against the full
65
+ `FieldPath` union instead of blaming the form argument.
66
+
67
+ ## 0.3.0 — 2026-07-04
68
+
69
+ ### Added
70
+
71
+ - `numberToInputText`, `parseNumberText` (and the `ParsedNumberText` type)
72
+ are exported — the number-text rules the built-in bindings use, so
73
+ adapters for third-party UI kits can share them instead of re-deriving.
74
+ This is the minimum formstand version for `formstand-gen --ui mui` output.
75
+
76
+ ### Docs & examples (no package changes)
77
+
78
+ - Five Material UI 9 playground demos plus the ~60-line formstand→MUI
79
+ adapter pattern they showcase.
80
+ - `formstand-cli` (`formstand-gen`) lives in the repo: generates form
81
+ components from a zod schema or TypeScript type. Published separately.
82
+
3
83
  ## 0.2.0 — 2026-07-02
4
84
 
5
- The result of a full-repo review pass (see `TODO.md` items 1–34).
85
+ The result of a full-repo review pass (37 items across correctness, API
86
+ gaps, robustness, testing, tooling, and docs — the working log lived in
87
+ `TODO.md`, retired after release; see the git history for the play-by-play).
6
88
 
7
89
  ### Breaking
8
90
 
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # formstand
2
2
 
3
+ [![CI](https://github.com/Scrumrot/formstand/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Scrumrot/formstand/actions/workflows/ci.yml) [![npm version](https://img.shields.io/npm/v/formstand)](https://www.npmjs.com/package/formstand) [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Scrumrot/formstand/blob/main/LICENSE)
4
+
3
5
  Zod-schema-first form state for React 19, backed by zustand.
4
6
 
5
7
  **[Documentation](https://scrumrot.github.io/formstand/)** · **[Live playground](https://scrumrot.github.io/formstand/examples/)** · **[API reference](https://scrumrot.github.io/formstand/api/)**
@@ -84,11 +86,11 @@ const form = createForm(schema, {
84
86
  | `setMode` / `setReValidateMode` | switch modes at runtime |
85
87
  | `reset(nextInitial?, options?)` | reset to initial; optional partial overrides (shallow-merged for record roots, replaced wholesale otherwise) and `{ keepErrors, keepTouched, keepSubmitCount }` (no `keepDirty` — dirtiness derives from values vs initial, and reset makes them equal) |
86
88
  | `resetField(path)` | reset one field to its initial value, clearing its (and descendants') error/touched state (dirtiness clears by definition — the value now equals initial) |
87
- | `adoptValues(values)` | mid-session rebase: replaces `values` + `initialValues` and clears `errors`, but **preserves** interaction state (`touched`, `submitCount`, `isSubmitting`, `isValidating`, `mode`). Use `reset()` for a full wipe |
89
+ | `adoptValues(values)` | mid-session rebase: replaces `values` + `initialValues` and clears `errors` and in-flight validation flags (`isValidating`/`isValidatingForm` — the rebase disowns in-flight passes), but **preserves** interaction state (`touched`, `submitCount`, `isSubmitting`, `mode`). Use `reset()` for a full wipe |
88
90
  | `updateState(updater)` | atomic multi-field patch; `errors` is derived from `schemaErrors`/`serverErrors`, so patch the channels — the patch type omits `errors` entirely, and a plain-JS `errors` patch is warned about and ignored |
89
91
  | `validate()` / `validateField(path)` / `validateFields(paths)` | sync validation; on an async schema they transparently start the async pass instead (`validate`/`validateField` return `{ kind: "pending", promise }`, `validateFields` returns the `Promise<boolean>` itself) |
90
92
  | `validateAsync()` / `validateFieldAsync(path)` / `validateFieldsAsync(paths)` | async; supports `async .refine` |
91
- | `submit(onValid, onInvalid?, { force? })` → `Promise<SubmitResult>` | full submit flow; resolves `{ kind: "valid", data }`, `{ kind: "invalid", errors }` (errored fields are also marked touched), or `{ kind: "skipped" }` when another submit is in flight |
93
+ | `submit(onValid, onInvalid?, { force? })` → `Promise<SubmitResult>` | full submit flow; resolves `{ kind: "valid", data }`, `{ kind: "invalid", errors }` (errored fields are also marked touched), `{ kind: "skipped" }` when another submit is in flight, or `{ kind: "error", error }` when `onValid` throws/rejects — submit resolves instead of rejecting, so `handleSubmit` never produces an unhandled rejection from an `onValid` throw |
92
94
  | `handleSubmit(onValid, onInvalid?)(event?)` | event handler wrapper that calls `preventDefault` |
93
95
  | `getField(path)` | typed one-shot value read |
94
96
  | `getFieldState(path)` | typed one-shot read of a field's full slice (value/error/touched/dirty/isValidating) |
@@ -127,7 +129,7 @@ name.dirty;
127
129
  name.isValidating;
128
130
  name.setValue(v);
129
131
  name.setTouched();
130
- name.setError([...]);
132
+ name.setError("taken"); // or ["taken", "reserved"]
131
133
  name.clearError();
132
134
  name.validate();
133
135
  name.validateAsync();
@@ -176,7 +178,7 @@ Selector-style subscription. Use `useFormSelectorShallow` for selectors that ret
176
178
 
177
179
  > Formerly `useFormState` / `useFormStateShallow` — renamed because React DOM
178
180
  > ships its own (deprecated) `useFormState` and auto-imports regularly grabbed
179
- > the wrong one. The old names still work as deprecated aliases.
181
+ > the wrong one. The old names were removed in 0.4.0.
180
182
 
181
183
  ### `useFormError(form)`
182
184
 
@@ -256,6 +258,20 @@ works out of the box. Wire it into the invalid-submit handler:
256
258
  <form onSubmit={form.handleSubmit(onValid, (errors) => focusFirstError(errors))}>
257
259
  ```
258
260
 
261
+ On a page with several forms, pass the form element (e.g. via a ref) as `root`
262
+ so the search stays inside your form — without it the whole document is
263
+ scanned, and a root-`""` error's first-control fallback refuses to guess
264
+ between forms (it returns `false` when the document holds more than one
265
+ `<form>`):
266
+
267
+ ```tsx
268
+ <form ref={formRef} onSubmit={form.handleSubmit(onValid, (errors) => focusFirstError(errors, formRef.current ?? undefined))}>
269
+ ```
270
+
271
+ It returns `true` only when a control actually received focus — disabled or
272
+ hidden controls, controls inside a closed `<dialog>`, and controls that refuse
273
+ focus (e.g. `display: none`) are passed over for the next match.
274
+
259
275
  For custom focus logic, every bound component also takes a `ref` to its input.
260
276
 
261
277
  ## Sharing a form across components
@@ -409,6 +425,12 @@ form.handleSubmit(async (data) => {
409
425
  });
410
426
  ```
411
427
 
428
+ ## Contributing
429
+
430
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) — note the repo has three
431
+ independent npm roots (`.`, `examples/`, `cli/`), and the root test suite
432
+ needs `examples/` installed.
433
+
412
434
  ## License
413
435
 
414
436
  MIT
package/dist/index.cjs CHANGED
@@ -188,6 +188,39 @@ var emptyValueForSchema = (schema) => {
188
188
  };
189
189
  return walk(schema, false);
190
190
  };
191
+ var unwrapForWalk = (s) => {
192
+ if (s instanceof zod.z.ZodOptional || s instanceof zod.z.ZodNullable) {
193
+ return s.unwrap();
194
+ }
195
+ if (s instanceof zod.z.ZodDefault || s instanceof zod.z.ZodPrefault || s instanceof zod.z.ZodCatch || s instanceof zod.z.ZodReadonly || s instanceof zod.z.ZodNonOptional) {
196
+ return s.def.innerType;
197
+ }
198
+ if (s instanceof zod.z.ZodPipe) return s.def.in;
199
+ return null;
200
+ };
201
+ var isLeafSchema = (s) => s instanceof zod.z.ZodString || s instanceof zod.z.ZodNumber || s instanceof zod.z.ZodBoolean || s instanceof zod.z.ZodBigInt || s instanceof zod.z.ZodDate || s instanceof zod.z.ZodEnum || s instanceof zod.z.ZodLiteral || s instanceof zod.z.ZodNull || s instanceof zod.z.ZodUndefined || s instanceof zod.z.ZodVoid || s instanceof zod.z.ZodNaN || s instanceof zod.z.ZodSymbol;
202
+ var SCHEMA_WALK_BUDGET = 64;
203
+ var schemaHasPath = (schema, path) => {
204
+ const walk = (s, segments, budget) => {
205
+ if (budget <= 0) return true;
206
+ const inner = unwrapForWalk(s);
207
+ if (inner !== null) return walk(inner, segments, budget - 1);
208
+ const [head, ...rest] = segments;
209
+ if (head === void 0) return true;
210
+ if (s instanceof zod.z.ZodObject) {
211
+ const shape = s.shape;
212
+ const child = shape[String(head)];
213
+ if (child !== void 0) return walk(child, rest, budget - 1);
214
+ const catchall = s.def.catchall;
215
+ return catchall !== void 0 && !(catchall instanceof zod.z.ZodNever);
216
+ }
217
+ if (s instanceof zod.z.ZodArray) {
218
+ return typeof head === "number" ? walk(s.element, rest, budget - 1) : false;
219
+ }
220
+ return !isLeafSchema(s);
221
+ };
222
+ return walk(schema, parsePath(path), SCHEMA_WALK_BUDGET);
223
+ };
191
224
  var hasChecks = (s) => {
192
225
  const checks = s.def.checks;
193
226
  return checks !== void 0 && checks.length > 0;
@@ -229,13 +262,14 @@ var dirtyPathsOf = (values, initial, prefix) => {
229
262
  if (valuesEqual(values, initial)) return [];
230
263
  if (isPlainObject(values) && isPlainObject(initial)) {
231
264
  const keys = /* @__PURE__ */ new Set([...Object.keys(values), ...Object.keys(initial)]);
232
- return [...keys].flatMap(
265
+ const childPaths = [...keys].flatMap(
233
266
  (k) => dirtyPathsOf(
234
267
  values[k],
235
268
  initial[k],
236
269
  prefix === "" ? k : `${prefix}.${k}`
237
270
  )
238
271
  );
272
+ return childPaths.length > 0 ? childPaths : [prefix];
239
273
  }
240
274
  return [prefix];
241
275
  };
@@ -359,6 +393,16 @@ var createForm = (schema, options) => {
359
393
  subschemaCache.set(path, sub);
360
394
  return sub;
361
395
  };
396
+ const checkedSchemaPaths = /* @__PURE__ */ new Set();
397
+ const warnMissingSchemaPath = (fn, path) => {
398
+ if (path === "" || checkedSchemaPaths.has(path)) return;
399
+ checkedSchemaPaths.add(path);
400
+ if (!schemaHasPath(schema, path)) {
401
+ console.warn(
402
+ `[formstand] ${fn}("${path}"): path does not exist in the schema \u2014 result will always be valid.`
403
+ );
404
+ }
405
+ };
362
406
  const fieldScopeFor = (path, values) => {
363
407
  const sub = path === "" ? null : subschemaAt(path);
364
408
  if (sub === null) {
@@ -396,6 +440,7 @@ var createForm = (schema, options) => {
396
440
  serverErrors: omitScope(state.serverErrors, [path])
397
441
  });
398
442
  const validateField = (path) => {
443
+ warnMissingSchemaPath("validateField", path);
399
444
  try {
400
445
  const scoped = scopedFieldErrors(path);
401
446
  store.setState((state) => ({
@@ -421,10 +466,11 @@ var createForm = (schema, options) => {
421
466
  };
422
467
  };
423
468
  const sequences = /* @__PURE__ */ new Map();
469
+ const seqRef = { current: 0 };
424
470
  const nextSeq = (key) => {
425
- const next = (sequences.get(key) ?? 0) + 1;
426
- sequences.set(key, next);
427
- return next;
471
+ seqRef.current += 1;
472
+ sequences.set(key, seqRef.current);
473
+ return seqRef.current;
428
474
  };
429
475
  const validateAsyncOnForm = async () => {
430
476
  const seq = nextSeq(FORM_SEQ_KEY);
@@ -504,6 +550,13 @@ var createForm = (schema, options) => {
504
550
  return requestedValid;
505
551
  };
506
552
  const validateFieldAsync = async (path) => {
553
+ warnMissingSchemaPath("validateFieldAsync", path);
554
+ if (path === "") {
555
+ const result = await validateAsyncOnForm();
556
+ return fieldResult(
557
+ result.kind === "invalid" ? result.errors : emptyErrors
558
+ );
559
+ }
507
560
  const seq = nextSeq(path);
508
561
  const valuesAtStart = store.getState().values;
509
562
  store.setState((state) => ({
@@ -563,7 +616,13 @@ var createForm = (schema, options) => {
563
616
  )
564
617
  }),
565
618
  touched: reKeyByArrayPath(state.touched, path, mapper),
566
- isValidating: reKeyByArrayPath(state.isValidating, path, mapper)
619
+ // Drop (don't re-key) in-flight flags under the array: the op changes
620
+ // `values`, so every pass they belong to fails its values-changed
621
+ // guard and never commits — but each pass's cleanup omits its
622
+ // ORIGINAL key, so a re-keyed entry would be orphaned and stick
623
+ // forever. Dropped, the flag is gone now and the cleanup is a
624
+ // harmless no-op.
625
+ isValidating: omitScope(state.isValidating, [path])
567
626
  };
568
627
  });
569
628
  };
@@ -585,33 +644,43 @@ var createForm = (schema, options) => {
585
644
  submitCount: state.submitCount + 1
586
645
  }));
587
646
  try {
588
- const result = await validateAsync(schema, store.getState().values);
647
+ const valuesAtStart = store.getState().values;
648
+ const result = await validateAsync(schema, valuesAtStart);
649
+ const staleValues = store.getState().values !== valuesAtStart;
589
650
  if (result.kind === "invalid") {
590
- store.setState((state) => {
591
- const committed = commitFullPass(state, result.errors);
592
- return {
593
- ...state,
594
- ...committed,
595
- // Mark every errored field touched so touched-gated error UIs
596
- // show something after the canonical first failed submit. The
597
- // "" key (schema-level refine) has no field to touch.
598
- touched: {
599
- ...state.touched,
600
- ...Object.fromEntries(
601
- Object.keys(committed.errors).filter((k) => k !== "").map((k) => [k, true])
602
- )
603
- }
604
- };
605
- });
606
- const merged = store.getState().errors;
651
+ if (!staleValues) {
652
+ store.setState((state) => {
653
+ const committed = commitFullPass(state, result.errors);
654
+ return {
655
+ ...state,
656
+ ...committed,
657
+ // Mark every errored field touched so touched-gated error UIs
658
+ // show something after the canonical first failed submit. The
659
+ // "" key (schema-level refine) has no field to touch.
660
+ touched: {
661
+ ...state.touched,
662
+ ...Object.fromEntries(
663
+ Object.keys(committed.errors).filter((k) => k !== "").map((k) => [k, true])
664
+ )
665
+ }
666
+ };
667
+ });
668
+ }
669
+ const merged = staleValues ? result.errors : store.getState().errors;
607
670
  onInvalid?.(merged);
608
671
  return { kind: "invalid", errors: merged };
609
672
  }
610
- store.setState((state) => ({
611
- ...state,
612
- ...commitFullPass(state, emptyErrors)
613
- }));
614
- await onValid(result.data);
673
+ if (!staleValues) {
674
+ store.setState((state) => ({
675
+ ...state,
676
+ ...commitFullPass(state, emptyErrors)
677
+ }));
678
+ }
679
+ try {
680
+ await onValid(result.data);
681
+ } catch (error) {
682
+ return { kind: "error", error };
683
+ }
615
684
  return { kind: "valid", data: result.data };
616
685
  } finally {
617
686
  inFlight.count -= 1;
@@ -743,7 +812,12 @@ var createForm = (schema, options) => {
743
812
  ...errorChannels(state, {
744
813
  schemaErrors: emptyErrors,
745
814
  serverErrors: emptyErrors
746
- })
815
+ }),
816
+ // Clearing `sequences` disowns every in-flight pass, including its
817
+ // flag cleanup (the ownership check fails, so it never writes) —
818
+ // clear the flags here too, mirroring reset(), or they'd stick.
819
+ isValidating: emptyBools,
820
+ isValidatingForm: false
747
821
  }));
748
822
  },
749
823
  reset: (nextInitial, resetOptions) => {
@@ -908,8 +982,6 @@ function useFormSelector(form, selector) {
908
982
  function useFormSelectorShallow(form, selector) {
909
983
  return react$1.useStore(form.store, shallow.useShallow(selector));
910
984
  }
911
- var useFormState = useFormSelector;
912
- var useFormStateShallow = useFormSelectorShallow;
913
985
  var useFormError = (form) => react$1.useStore(
914
986
  form.store,
915
987
  shallow.useShallow((state) => state.errors[""])
@@ -1206,7 +1278,12 @@ var NumberField = ({
1206
1278
  /* @__PURE__ */ jsxRuntime.jsx(ErrorText, { id: errorId, error: field.error })
1207
1279
  ] });
1208
1280
  };
1209
- var CheckboxField = ({ form, path, label, ref }) => {
1281
+ var CheckboxField = ({
1282
+ form,
1283
+ path,
1284
+ label,
1285
+ ref
1286
+ }) => {
1210
1287
  const id = react.useId();
1211
1288
  const errorId = `${id}-error`;
1212
1289
  const field = useField(form, path);
@@ -1250,7 +1327,12 @@ var SelectField = ({
1250
1327
  "aria-describedby": hasError(field.error) ? errorId : void 0,
1251
1328
  children: [
1252
1329
  showEmptyOption ? /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", disabled: true, children: placeholder ?? "" }) : null,
1253
- options.map((opt) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: opt.value, children: opt.label }, opt.value))
1330
+ options.map((opt, index) => (
1331
+ // Index-qualified: option lists may legitimately repeat a value
1332
+ // (e.g. differently-labelled aliases), and bare value keys would
1333
+ // collide.
1334
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: opt.value, children: opt.label }, `${index}-${opt.value}`)
1335
+ ))
1254
1336
  ]
1255
1337
  }
1256
1338
  ),
@@ -1259,25 +1341,40 @@ var SelectField = ({
1259
1341
  };
1260
1342
 
1261
1343
  // src/react/focusError.ts
1344
+ var focusableControls = (scope) => [
1345
+ ...scope.querySelectorAll(
1346
+ "input[name], select[name], textarea[name]"
1347
+ )
1348
+ ].filter(
1349
+ (el) => !el.matches(':disabled, [type="hidden"]') && el.closest("dialog:not([open])") === null
1350
+ );
1351
+ var focusFirstOf = (candidates) => candidates.some((el) => {
1352
+ el.focus();
1353
+ return document.activeElement === el;
1354
+ });
1355
+ var nameMatchesAny = (el, paths) => {
1356
+ const name = el.getAttribute("name");
1357
+ return name !== null && paths.some((p) => isPathOrChild(name, p));
1358
+ };
1262
1359
  var focusFirstError = (errors, root) => {
1263
1360
  const scope = root ?? document;
1264
1361
  const erroredPaths = Object.keys(errors).filter(
1265
1362
  (k) => k !== "" && (errors[k]?.length ?? 0) > 0
1266
1363
  );
1267
1364
  const hasRootError = (errors[""]?.length ?? 0) > 0;
1268
- const controls = [
1269
- ...scope.querySelectorAll(
1270
- "input[name], select[name], textarea[name]"
1271
- )
1272
- ].filter((el) => !el.matches(':disabled, [type="hidden"]'));
1273
- const fieldMatch = controls.find((el) => {
1274
- const name = el.getAttribute("name");
1275
- return name !== null && erroredPaths.some((k) => isPathOrChild(name, k));
1276
- });
1277
- const target = fieldMatch ?? (hasRootError ? controls[0] : void 0);
1278
- target?.focus();
1279
- return target !== void 0;
1365
+ const controls = focusableControls(scope);
1366
+ const fieldMatches = controls.filter(
1367
+ (el) => nameMatchesAny(el, erroredPaths)
1368
+ );
1369
+ const rootFallbackApplies = hasRootError && (root !== void 0 || document.querySelectorAll("form").length <= 1);
1370
+ const candidates = fieldMatches.length > 0 ? fieldMatches : rootFallbackApplies ? controls : [];
1371
+ return focusFirstOf(candidates);
1280
1372
  };
1373
+ var focusField = (path, root) => focusFirstOf(
1374
+ focusableControls(root ?? document).filter(
1375
+ (el) => nameMatchesAny(el, [path])
1376
+ )
1377
+ );
1281
1378
  var EMPTY_ID_STATE = { items: [], ids: [], counter: 0 };
1282
1379
  var EMPTY_ITEMS = [];
1283
1380
  var reconcileIds = (prev, nextItems) => {
@@ -1390,11 +1487,15 @@ exports.TextField = TextField;
1390
1487
  exports.checkboxProps = checkboxProps;
1391
1488
  exports.createForm = createForm;
1392
1489
  exports.createFormContext = createFormContext;
1490
+ exports.emptyValueForSchema = emptyValueForSchema;
1393
1491
  exports.flattenIssues = flattenIssues;
1492
+ exports.focusField = focusField;
1394
1493
  exports.focusFirstError = focusFirstError;
1395
1494
  exports.getAtPath = getAtPath;
1396
1495
  exports.isAsyncRequiredError = isAsyncRequiredError;
1397
1496
  exports.numberInputProps = numberInputProps;
1497
+ exports.numberToInputText = numberToInputText;
1498
+ exports.parseNumberText = parseNumberText;
1398
1499
  exports.parsePath = parsePath;
1399
1500
  exports.selectProps = selectProps;
1400
1501
  exports.setAtPath = setAtPath;
@@ -1406,8 +1507,6 @@ exports.useForm = useForm;
1406
1507
  exports.useFormError = useFormError;
1407
1508
  exports.useFormSelector = useFormSelector;
1408
1509
  exports.useFormSelectorShallow = useFormSelectorShallow;
1409
- exports.useFormState = useFormState;
1410
- exports.useFormStateShallow = useFormStateShallow;
1411
1510
  exports.useIsDirty = useIsDirty;
1412
1511
  exports.useIsSubmitting = useIsSubmitting;
1413
1512
  exports.useIsValid = useIsValid;