formstand 0.26.0 → 0.26.1

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.cjs CHANGED
@@ -212,10 +212,12 @@ var hasOwnSlot = (obj, path) => parsePath(path).reduce(
212
212
  ).ok;
213
213
  var writeIntoArray = (arr, index, rest, value, kindAt, prefix) => {
214
214
  const length = Math.max(arr.length, index + 1);
215
- return Array.from(
216
- { length },
217
- (_, i) => i === index ? writeSegments(arr[i], rest, value, kindAt, [...prefix, index]) : arr[i]
218
- );
215
+ const child = writeSegments(arr[index], rest, value, kindAt, [
216
+ ...prefix,
217
+ index
218
+ ]);
219
+ if (index < arr.length && child === arr[index]) return arr;
220
+ return Array.from({ length }, (_, i) => i === index ? child : arr[i]);
219
221
  };
220
222
  var refusesHugeIndex = (index) => {
221
223
  if (index > MAX_ARRAY_INDEX) {
@@ -247,10 +249,12 @@ var writeSegments = (obj, segments, value, kindAt, prefix) => {
247
249
  return obj;
248
250
  }
249
251
  const key2 = String(head);
250
- return {
251
- ...obj,
252
- [key2]: writeSegments(obj[key2], rest, value, kindAt, [...prefix, key2])
253
- };
252
+ const child = writeSegments(obj[key2], rest, value, kindAt, [
253
+ ...prefix,
254
+ key2
255
+ ]);
256
+ if (child === obj[key2] && Object.hasOwn(obj, key2)) return obj;
257
+ return { ...obj, [key2]: child };
254
258
  }
255
259
  const kind = kindAt?.(prefix.map(String).join(".")) ?? (typeof head === "number" ? "array" : "record");
256
260
  if (kind === "array") {
@@ -329,7 +333,11 @@ var unwrapForWalk = (s) => {
329
333
  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) {
330
334
  return s.def.innerType;
331
335
  }
332
- if (s instanceof zod.z.ZodPipe) return s.def.in;
336
+ if (s instanceof zod.z.ZodPipe) {
337
+ const input = s.def.in;
338
+ return input instanceof zod.z.ZodTransform ? s.def.out : input;
339
+ }
340
+ if (s instanceof zod.z.ZodLazy) return s.def.getter();
333
341
  return null;
334
342
  };
335
343
  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;
@@ -395,10 +403,31 @@ var hasChecks = (s) => {
395
403
  const checks = s.def.checks;
396
404
  return checks !== void 0 && checks.length > 0;
397
405
  };
398
- var fieldSchemaForIntrospection = (schema, path) => {
399
- const childOf = (s, segment) => {
406
+ var optionClaims = (option, discriminator, tag) => {
407
+ const strip = (s) => {
400
408
  const inner = unwrapForWalk(s);
401
- if (inner !== null) return childOf(inner, segment);
409
+ return inner === null ? s : strip(inner);
410
+ };
411
+ const node = strip(option);
412
+ if (!(node instanceof zod.z.ZodObject)) return false;
413
+ const shape = node.shape;
414
+ const at = Object.hasOwn(shape, discriminator) ? shape[discriminator] : void 0;
415
+ if (at === void 0) return false;
416
+ const leaf = strip(at);
417
+ if (leaf instanceof zod.z.ZodLiteral) {
418
+ return leaf.def.values.includes(tag);
419
+ }
420
+ if (leaf instanceof zod.z.ZodEnum) {
421
+ return Object.values(leaf.def.entries).includes(
422
+ tag
423
+ );
424
+ }
425
+ return false;
426
+ };
427
+ var fieldSchemaForIntrospection = (schema, path, values) => {
428
+ const childOf = (s, segment, holder) => {
429
+ const inner = unwrapForWalk(s);
430
+ if (inner !== null) return childOf(inner, segment, holder);
402
431
  if (s instanceof zod.z.ZodObject && typeof segment === "string") {
403
432
  const shape = s.shape;
404
433
  return Object.hasOwn(shape, segment) ? shape[segment] ?? null : null;
@@ -412,19 +441,45 @@ var fieldSchemaForIntrospection = (schema, path) => {
412
441
  if (s instanceof zod.z.ZodRecord) {
413
442
  return s.valueType;
414
443
  }
444
+ if (s instanceof zod.z.ZodIntersection) {
445
+ return childOf(s.def.left, segment, holder) ?? childOf(s.def.right, segment, holder);
446
+ }
415
447
  if (s instanceof zod.z.ZodDiscriminatedUnion || s instanceof zod.z.ZodUnion) {
416
448
  const options = s.def.options;
417
- return options.reduce(
418
- (found, option) => found ?? childOf(option, segment),
449
+ const discriminator = s instanceof zod.z.ZodDiscriminatedUnion ? s.def.discriminator : null;
450
+ const tag = discriminator !== null && typeof holder === "object" && holder !== null ? holder[discriminator] : void 0;
451
+ const live = discriminator === null || tag === void 0 ? void 0 : options.find((option) => optionClaims(option, discriminator, tag));
452
+ const candidates = live === void 0 ? options : [live];
453
+ return candidates.reduce(
454
+ (found, option) => found ?? childOf(option, segment, holder),
419
455
  null
420
456
  );
421
457
  }
422
458
  return null;
423
459
  };
424
- return parsePath(path).reduce(
425
- (current, segment) => current === null ? null : childOf(current, segment),
426
- schema
427
- );
460
+ const segments = parsePath(path);
461
+ return segments.reduce(
462
+ (acc, segment) => acc.s === null ? acc : {
463
+ s: childOf(
464
+ acc.s,
465
+ segment,
466
+ values === void 0 ? void 0 : getAtPath(values, acc.prefix)
467
+ ),
468
+ prefix: acc.prefix === "" ? String(segment) : `${acc.prefix}.${String(segment)}`
469
+ },
470
+ { s: schema, prefix: "" }
471
+ ).s;
472
+ };
473
+ var schemaAcceptsUndefined = (schema) => schema instanceof zod.z.ZodOptional || schema instanceof zod.z.ZodDefault || schema instanceof zod.z.ZodPrefault || schema instanceof zod.z.ZodNullable && schemaAcceptsUndefined(schema.unwrap());
474
+ var isObjectSchemaAt = (schema, path) => {
475
+ const sub = fieldSchemaForIntrospection(schema, path);
476
+ if (sub === null) return true;
477
+ const strip = (s) => {
478
+ const inner = unwrapForWalk(s);
479
+ return inner === null ? s : strip(inner);
480
+ };
481
+ const node = strip(sub);
482
+ return !(node instanceof zod.z.ZodUnion || node instanceof zod.z.ZodDiscriminatedUnion || node instanceof zod.z.ZodRecord || node instanceof zod.z.ZodMap || node instanceof zod.z.ZodSet || node instanceof zod.z.ZodArray || node instanceof zod.z.ZodTuple || isLeafSchema(node));
428
483
  };
429
484
  var containerKindAt = (schema) => (prefix) => {
430
485
  const unwrapAll = (s) => {
@@ -544,7 +599,8 @@ var scopeParse = (result, path, viaSubschema) => {
544
599
  var pruneUnreported = (prev, full) => {
545
600
  const entries = Object.entries(prev);
546
601
  const kept = entries.filter(([k]) => Object.hasOwn(full, k));
547
- return kept.length === entries.length ? prev : Object.fromEntries(kept);
602
+ const unchanged = kept.length === entries.length && kept.every(([k, list]) => full[k] === list);
603
+ return unchanged ? prev : Object.fromEntries(kept.map(([k, list]) => [k, full[k] ?? list]));
548
604
  };
549
605
  var dropEmptyLists = (map) => {
550
606
  const entries = Object.entries(map);
@@ -916,7 +972,14 @@ var createForm = (schema, options) => {
916
972
  fresh.flatMap((path) => Object.entries(errorsFor(path)))
917
973
  ),
918
974
  fresh,
919
- parsed.kind === "full" ? parsed.errors : null
975
+ // The prune map describes the values the batch STARTED with.
976
+ // A skip scope (an index into an empty array) still reads as
977
+ // fresh after an unrelated change, so a batch can commit under
978
+ // changed values; pruning with the stale map then dropped
979
+ // errors committed elsewhere in flight (an email error landing
980
+ // while a wizard step with such a path validated). The prune
981
+ // rides only while the whole values reference held.
982
+ parsed.kind === "full" && live.values === valuesAtStart ? parsed.errors : null
920
983
  ),
921
984
  isValidating: omitKeys(state.isValidating, ownedKeys)
922
985
  }));
@@ -1090,12 +1153,14 @@ var createForm = (schema, options) => {
1090
1153
  }
1091
1154
  }
1092
1155
  };
1093
- if (options.validateOnMount === true) {
1156
+ const judgeBaseline = () => {
1157
+ if (options.validateOnMount !== true) return;
1094
1158
  const result = validate();
1095
1159
  if (result.kind === "pending") {
1096
1160
  void result.promise.catch(reportBackgroundFailure);
1097
1161
  }
1098
- }
1162
+ };
1163
+ judgeBaseline();
1099
1164
  return Object.freeze({
1100
1165
  schema,
1101
1166
  store,
@@ -1110,8 +1175,12 @@ var createForm = (schema, options) => {
1110
1175
  return store.subscribe((state) => {
1111
1176
  const next = snapshotField(state, path);
1112
1177
  if (!shallowFieldEqual(ref.current, next)) {
1178
+ const prev = ref.current;
1113
1179
  ref.current = next;
1114
- listener(next);
1180
+ listener(
1181
+ next,
1182
+ prev
1183
+ );
1115
1184
  }
1116
1185
  });
1117
1186
  },
@@ -1146,10 +1215,12 @@ var createForm = (schema, options) => {
1146
1215
  if (Object.is(getAtPath(currentValues, path), value) && (value !== void 0 || hasOwnSlot(currentValues, path))) {
1147
1216
  return;
1148
1217
  }
1218
+ const next = setAtPath(currentValues, path, value, kindAt);
1219
+ if (next === currentValues) return;
1149
1220
  clearArrayOpsUnder(store, path);
1150
1221
  store.setState((state) => ({
1151
1222
  ...state,
1152
- values: setAtPath(state.values, path, value, kindAt),
1223
+ values: next,
1153
1224
  ...errorChannels(state, {
1154
1225
  serverErrors: releaseSpine(state.serverErrors, path)
1155
1226
  })
@@ -1256,6 +1327,7 @@ var createForm = (schema, options) => {
1256
1327
  isValidating: emptyBools,
1257
1328
  isValidatingForm: false
1258
1329
  }));
1330
+ judgeBaseline();
1259
1331
  },
1260
1332
  reset: (nextInitial, resetOptions) => {
1261
1333
  sequences.clear();
@@ -1280,6 +1352,7 @@ var createForm = (schema, options) => {
1280
1352
  isValidatingForm: false
1281
1353
  };
1282
1354
  });
1355
+ if (resetOptions?.keepErrors !== true) judgeBaseline();
1283
1356
  },
1284
1357
  resetField: (path) => {
1285
1358
  const initialSlot = slotAtPath(
@@ -1374,6 +1447,7 @@ var createForm = (schema, options) => {
1374
1447
  },
1375
1448
  snapshot: () => store.getState(),
1376
1449
  restore: (snap) => {
1450
+ sequences.clear();
1377
1451
  clearArrayOpsUnder(store, "");
1378
1452
  store.setState(() => {
1379
1453
  const schemaErrors = dropEmptyLists(snap.schemaErrors ?? emptyErrors);
@@ -1497,6 +1571,7 @@ var persistForm = (form, options) => {
1497
1571
  const schema = form.schema;
1498
1572
  const fillRequiredGaps = (draft, initial, path) => {
1499
1573
  if (!isPlainObject(draft) || !isPlainObject(initial)) return draft;
1574
+ if (!isObjectSchemaAt(schema, path)) return draft;
1500
1575
  return Object.keys(initial).reduce(
1501
1576
  (acc, key) => {
1502
1577
  const childPath = joinPath(path, key);
@@ -1506,21 +1581,34 @@ var persistForm = (form, options) => {
1506
1581
  }
1507
1582
  if (initial[key] === void 0) return acc;
1508
1583
  const sub = fieldSchemaForIntrospection(schema, childPath);
1509
- return sub !== null && isClearableSchema(sub) ? acc : { ...acc, [key]: initial[key] };
1584
+ return sub !== null && schemaAcceptsUndefined(sub) ? acc : { ...acc, [key]: initial[key] };
1510
1585
  },
1511
1586
  draft
1512
1587
  );
1513
1588
  };
1589
+ const applying = { current: false };
1514
1590
  const applyDraft = (values) => {
1515
1591
  const filled = fillRequiredGaps(
1516
1592
  values,
1517
1593
  form.getState().initialValues,
1518
1594
  ""
1519
1595
  );
1520
- if (apply === "edit" || apply === "restore") {
1521
- form.setValues(filled);
1522
- } else {
1523
- form.adoptValues(filled);
1596
+ applying.current = true;
1597
+ try {
1598
+ if (apply === "edit" || apply === "restore") {
1599
+ form.setValues(filled);
1600
+ } else {
1601
+ form.adoptValues(filled);
1602
+ }
1603
+ } finally {
1604
+ applying.current = false;
1605
+ }
1606
+ };
1607
+ const isCurrent = (values) => {
1608
+ try {
1609
+ return JSON.stringify(values) === JSON.stringify(form.getState().values);
1610
+ } catch {
1611
+ return false;
1524
1612
  }
1525
1613
  };
1526
1614
  const referenceShape = () => {
@@ -1531,7 +1619,7 @@ var persistForm = (form, options) => {
1531
1619
  }
1532
1620
  };
1533
1621
  const flexibleAt = (path) => schemaAllowsKindChangeAt(schema, path);
1534
- const inspect = () => {
1622
+ const inspect = (skipWhenCurrent) => {
1535
1623
  const raw = (() => {
1536
1624
  try {
1537
1625
  return storage === null ? null : storage.getItem(options.key);
@@ -1577,14 +1665,15 @@ var persistForm = (form, options) => {
1577
1665
  reason: `its shape conflicts with the form's initial values at "${conflict}" (set the version option when the schema changes)`
1578
1666
  };
1579
1667
  }
1668
+ if (skipWhenCurrent && isCurrent(values)) return { kind: "applied" };
1580
1669
  applyDraft(values);
1581
1670
  return { kind: "applied" };
1582
1671
  } catch {
1583
1672
  return { kind: "ignored", reason: "it is not valid JSON" };
1584
1673
  }
1585
1674
  };
1586
- const load = () => {
1587
- const outcome = inspect();
1675
+ const loadWith = (skipWhenCurrent) => {
1676
+ const outcome = inspect(skipWhenCurrent);
1588
1677
  if (outcome.kind === "ignored" && isDevBuild()) {
1589
1678
  console.warn(
1590
1679
  `[formstand] persistForm: ignored the draft under "${options.key}": ${outcome.reason}. The next edit overwrites it.`
@@ -1592,6 +1681,7 @@ var persistForm = (form, options) => {
1592
1681
  }
1593
1682
  return outcome.kind === "applied";
1594
1683
  };
1684
+ const load = () => loadWith(false);
1595
1685
  const writeDraft = (values) => {
1596
1686
  try {
1597
1687
  storage?.setItem(
@@ -1605,10 +1695,11 @@ var persistForm = (form, options) => {
1605
1695
  };
1606
1696
  const writes = createDebouncer(debounceMs);
1607
1697
  if (apply !== "manual") {
1608
- load();
1698
+ loadWith(true);
1609
1699
  }
1610
1700
  const cleared = { current: false };
1611
1701
  const unsubscribe = form.watchValues((values) => {
1702
+ if (applying.current) return;
1612
1703
  if (cleared.current && values === form.getState().initialValues) return;
1613
1704
  cleared.current = false;
1614
1705
  writes.schedule(() => writeDraft(values));
@@ -1637,15 +1728,27 @@ var usePersistForm = (form, options) => {
1637
1728
  latestOptions.current = options;
1638
1729
  });
1639
1730
  const key = options.key;
1640
- const storage = options.storage;
1731
+ const hasStorage = options.storage !== void 0;
1732
+ const storageProxy = react.useMemo(
1733
+ () => ({
1734
+ getItem: (k) => latestOptions.current.storage?.getItem(k) ?? null,
1735
+ setItem: (k, v) => latestOptions.current.storage?.setItem(k, v),
1736
+ removeItem: (k) => latestOptions.current.storage?.removeItem(k)
1737
+ }),
1738
+ []
1739
+ );
1641
1740
  react.useEffect(() => {
1642
- const handle = persistForm(form, latestOptions.current);
1741
+ const options2 = latestOptions.current;
1742
+ const handle = persistForm(form, {
1743
+ ...options2,
1744
+ ...options2.storage === void 0 ? {} : { storage: storageProxy }
1745
+ });
1643
1746
  inner.current = handle;
1644
1747
  return () => {
1645
1748
  inner.current = null;
1646
1749
  handle.dispose();
1647
1750
  };
1648
- }, [form, key, storage]);
1751
+ }, [form, key, hasStorage, storageProxy]);
1649
1752
  return react.useMemo(
1650
1753
  () => ({
1651
1754
  load: () => inner.current?.load() ?? false,
@@ -1717,6 +1820,12 @@ var numberInputProps = (field) => ({
1717
1820
  });
1718
1821
  var dateToInputText = (value) => {
1719
1822
  if (value === void 0 || value === null) return "";
1823
+ if (typeof value === "string") {
1824
+ const text = value;
1825
+ const parsed = parseDateText(text);
1826
+ if (parsed.kind === "date") return dateToInputText(parsed.value);
1827
+ if (/^\d{4}-\d{2}-\d{2}$/.test(text.trim())) return "";
1828
+ }
1720
1829
  const date = value instanceof Date ? value : new Date(value);
1721
1830
  if (Number.isNaN(date.getTime())) return "";
1722
1831
  const pad = (part) => String(part).padStart(2, "0");
@@ -1825,6 +1934,15 @@ function useFormSelectorShallow(form, selector) {
1825
1934
  }
1826
1935
 
1827
1936
  // src/react/useFormSteps.ts
1937
+ var settleOnLiveValues = async (form, fields, round = 0) => {
1938
+ const before = fields.map((path) => form.getField(path));
1939
+ const result = await form.validateFieldsAsync(fields);
1940
+ const changed = fields.some(
1941
+ (path, i) => !Object.is(form.getField(path), before[i])
1942
+ );
1943
+ if (!changed) return result;
1944
+ return round < 3 ? settleOnLiveValues(form, fields, round + 1) : null;
1945
+ };
1828
1946
  var useFormSteps = (form, steps) => {
1829
1947
  const [step, setStep] = react.useState(0);
1830
1948
  const [visited, setVisited] = react.useState(
@@ -1838,7 +1956,8 @@ var useFormSteps = (form, steps) => {
1838
1956
  async (index) => {
1839
1957
  const definition = latest.current[index];
1840
1958
  if (definition === void 0) return true;
1841
- const result = await form.validateFieldsAsync(definition.fields);
1959
+ const result = await settleOnLiveValues(form, definition.fields);
1960
+ if (result === null) return false;
1842
1961
  if (result.kind === "invalid") {
1843
1962
  Object.keys(result.errors).forEach((key) => {
1844
1963
  form.setTouched(key, true);
@@ -1850,46 +1969,49 @@ var useFormSteps = (form, steps) => {
1850
1969
  [form]
1851
1970
  );
1852
1971
  const currentStep = react.useRef(step);
1853
- react.useEffect(() => {
1854
- currentStep.current = step;
1855
- });
1972
+ const navigate = react.useCallback((target) => {
1973
+ currentStep.current = target;
1974
+ setStep(target);
1975
+ }, []);
1856
1976
  const next = react.useCallback(async () => {
1857
- const ok = await validateStep(step);
1977
+ const from = currentStep.current;
1978
+ const ok = await validateStep(from);
1858
1979
  if (!ok) return false;
1859
- if (currentStep.current !== step) return false;
1860
- const target = Math.min(step + 1, latest.current.length - 1);
1861
- setStep(target);
1980
+ if (currentStep.current !== from) return false;
1981
+ const target = Math.max(0, Math.min(from + 1, latest.current.length - 1));
1982
+ navigate(target);
1862
1983
  setVisited((prev) => /* @__PURE__ */ new Set([...prev, target]));
1863
1984
  return true;
1864
- }, [validateStep, step]);
1985
+ }, [validateStep, navigate]);
1865
1986
  const back = react.useCallback(() => {
1866
- setStep((prev) => Math.max(prev - 1, 0));
1867
- }, []);
1987
+ navigate(Math.max(currentStep.current - 1, 0));
1988
+ }, [navigate]);
1868
1989
  const goTo = react.useCallback(
1869
1990
  async (target) => {
1870
1991
  const count = latest.current.length;
1871
1992
  if (!Number.isFinite(target)) return currentStep.current;
1993
+ const from = currentStep.current;
1872
1994
  const clamped = Math.max(0, Math.min(Math.trunc(target), count - 1));
1873
- if (clamped <= step) {
1874
- setStep(clamped);
1995
+ if (clamped <= from) {
1996
+ navigate(clamped);
1875
1997
  return clamped;
1876
1998
  }
1877
- const walk = async (from) => {
1878
- if (from >= clamped) return clamped;
1879
- return await validateStep(from) ? walk(from + 1) : from;
1999
+ const walk = async (at) => {
2000
+ if (at >= clamped) return clamped;
2001
+ return await validateStep(at) ? walk(at + 1) : at;
1880
2002
  };
1881
- const landed = await walk(step);
1882
- if (currentStep.current !== step) return currentStep.current;
1883
- setStep(landed);
2003
+ const landed = await walk(from);
2004
+ if (currentStep.current !== from) return currentStep.current;
2005
+ navigate(landed);
1884
2006
  setVisited(
1885
2007
  (prev) => /* @__PURE__ */ new Set([
1886
2008
  ...prev,
1887
- ...Array.from({ length: landed - step + 1 }, (_, i) => step + i)
2009
+ ...Array.from({ length: landed - from + 1 }, (_, i) => from + i)
1888
2010
  ])
1889
2011
  );
1890
2012
  return landed;
1891
2013
  },
1892
- [validateStep, step]
2014
+ [validateStep, navigate]
1893
2015
  );
1894
2016
  const errorFlags = useFormSelectorShallow(
1895
2017
  form,
@@ -1982,6 +2104,7 @@ var createFormContext = () => {
1982
2104
  return { Provider, useFormContext };
1983
2105
  };
1984
2106
  function useField(form, pathArg, options) {
2107
+ const formSchema = form.schema;
1985
2108
  const slice = react$1.useStore(
1986
2109
  form.store,
1987
2110
  shallow.useShallow((state) => {
@@ -1997,25 +2120,26 @@ function useField(form, pathArg, options) {
1997
2120
  error: entryAt(state.errors, p),
1998
2121
  touched: flagAt(state.touched, p),
1999
2122
  dirty: isFieldDirty(value, initialValue2),
2000
- isValidating: flagAt(state.isValidating, p)
2123
+ isValidating: flagAt(state.isValidating, p),
2124
+ // Resolved inside the selector so a discriminant change re-reads
2125
+ // the ACTIVE variant's wrapper for a key the variants share; the
2126
+ // schema node is a stable reference, so the shallow compare holds.
2127
+ schema: formSchema === void 0 ? null : fieldSchemaForIntrospection(formSchema, p, state.values)
2001
2128
  };
2002
2129
  })
2003
2130
  );
2004
2131
  const path = slice.path;
2005
- const formSchema = form.schema;
2132
+ const sub = slice.schema;
2006
2133
  const initialValue = slice.initialValue;
2007
2134
  const { emptyValue, clearable } = react.useMemo(() => {
2008
- if (formSchema !== void 0) {
2009
- const sub = fieldSchemaForIntrospection(formSchema, path);
2010
- if (sub !== null) {
2011
- return {
2012
- emptyValue: emptyValueForSchema(sub),
2013
- clearable: isClearableSchema(sub)
2014
- };
2015
- }
2135
+ if (sub !== null) {
2136
+ return {
2137
+ emptyValue: emptyValueForSchema(sub),
2138
+ clearable: isClearableSchema(sub)
2139
+ };
2016
2140
  }
2017
2141
  return initialValue === null ? { emptyValue: null, clearable: true } : { emptyValue: void 0, clearable: false };
2018
- }, [formSchema, path, initialValue]);
2142
+ }, [sub, initialValue]);
2019
2143
  const debounceMs = options?.debounceMs;
2020
2144
  const debouncerRef = react.useRef(null);
2021
2145
  react.useEffect(() => {
@@ -2125,10 +2249,11 @@ function useField(form, pathArg, options) {
2125
2249
  }
2126
2250
 
2127
2251
  // src/react/useVariantField.ts
2128
- function useVariantField(form, unionPath, field) {
2252
+ function useVariantField(form, unionPath, field, options) {
2129
2253
  return useField(
2130
2254
  form,
2131
- `${unionPath}.${field}`
2255
+ `${unionPath}.${field}`,
2256
+ options
2132
2257
  );
2133
2258
  }
2134
2259
  var EMPTY_ITEMS = [];
@@ -2291,6 +2416,12 @@ function useFieldArray(form, pathArg) {
2291
2416
  const items = slice.items;
2292
2417
  const error = slice.error;
2293
2418
  const ids = deriveIds(form.store, path, items).ids;
2419
+ const validateNow = react.useCallback(() => {
2420
+ const result = form.validateField?.(path);
2421
+ if (result?.kind === "pending") {
2422
+ void result.promise.catch(reportBackgroundFailure);
2423
+ }
2424
+ }, [form, path]);
2294
2425
  const revalidate = react.useCallback(() => {
2295
2426
  const state = form.store.getState();
2296
2427
  if (shouldValidateOn(
@@ -2300,9 +2431,9 @@ function useFieldArray(form, pathArg) {
2300
2431
  state.submitCount > 0,
2301
2432
  flagAt(state.touched, path)
2302
2433
  )) {
2303
- form.validateField?.(path);
2434
+ validateNow();
2304
2435
  }
2305
- }, [form, path]);
2436
+ }, [form, path, validateNow]);
2306
2437
  const push = react.useCallback(
2307
2438
  (item) => {
2308
2439
  form.arrayPush(path, item);
@@ -2367,13 +2498,17 @@ function useFieldArray(form, pathArg) {
2367
2498
  state.submitCount > 0,
2368
2499
  true
2369
2500
  )) {
2370
- form.validateField?.(path);
2501
+ validateNow();
2371
2502
  }
2372
- }, [form, path]);
2503
+ }, [form, path, validateNow]);
2373
2504
  const validate = react.useCallback(
2374
2505
  () => form.validateField?.(path) ?? { kind: "valid" },
2375
2506
  [form, path]
2376
2507
  );
2508
+ const validateAsync2 = react.useCallback(
2509
+ () => form.validateFieldAsync?.(path) ?? Promise.resolve({ kind: "valid" }),
2510
+ [form, path]
2511
+ );
2377
2512
  const replace = react.useCallback(
2378
2513
  (next) => {
2379
2514
  form.setValue?.(path, next);
@@ -2383,7 +2518,15 @@ function useFieldArray(form, pathArg) {
2383
2518
  );
2384
2519
  const update = react.useCallback(
2385
2520
  (index, item) => {
2386
- form.setValue?.(`${path}.${index}`, item);
2521
+ const current = getAtPath(form.store.getState().values, path);
2522
+ const length = Array.isArray(current) ? current.length : 0;
2523
+ if (!Number.isInteger(index) || index < 0 || index >= length) {
2524
+ console.warn(
2525
+ `[formstand] array op on "${path}" with an out-of-range or non-integer index (length ${String(length)}). Operation skipped.`
2526
+ );
2527
+ return;
2528
+ }
2529
+ form.setValue?.(`${path}.${String(index)}`, item);
2387
2530
  revalidate();
2388
2531
  },
2389
2532
  [form, path, revalidate]
@@ -2415,6 +2558,7 @@ function useFieldArray(form, pathArg) {
2415
2558
  setTouched,
2416
2559
  onBlur,
2417
2560
  validate,
2561
+ validateAsync: validateAsync2,
2418
2562
  push,
2419
2563
  remove,
2420
2564
  insert,
@@ -2438,6 +2582,7 @@ function useFieldArray(form, pathArg) {
2438
2582
  setTouched,
2439
2583
  onBlur,
2440
2584
  validate,
2585
+ validateAsync2,
2441
2586
  push,
2442
2587
  remove,
2443
2588
  insert,
@@ -2465,7 +2610,7 @@ var useFormError = (form) => react$1.useStore(
2465
2610
  function useFormValues(form) {
2466
2611
  return react$1.useStore(form.store, (state) => state.values);
2467
2612
  }
2468
- function useFields(form, paths) {
2613
+ function useFields(form, paths, options) {
2469
2614
  const [arity] = react.useState(paths.length);
2470
2615
  if (paths.length !== arity) {
2471
2616
  throw new Error(
@@ -2475,7 +2620,11 @@ function useFields(form, paths) {
2475
2620
  const fields = paths.map(
2476
2621
  (path) => (
2477
2622
  // eslint-disable-next-line react-hooks/rules-of-hooks -- a fixed hook count by the stable-arity contract above; each element must be a REAL UseFieldReturn so it spreads into the prop builders
2478
- useField(form, path)
2623
+ useField(
2624
+ form,
2625
+ path,
2626
+ options
2627
+ )
2479
2628
  )
2480
2629
  );
2481
2630
  return react.useMemo(() => {
@@ -2538,10 +2687,11 @@ var capitalize = (name) => name.length === 0 ? name : name.charAt(0).toUpperCase
2538
2687
  var createFormHooks = (form, name) => {
2539
2688
  const structural = form;
2540
2689
  const useBoundField = (path, options) => useField(structural, path, options);
2541
- const useBoundVariantField = (unionPath, field) => useVariantField(
2690
+ const useBoundVariantField = (unionPath, field, options) => useVariantField(
2542
2691
  structural,
2543
2692
  unionPath,
2544
- field
2693
+ field,
2694
+ options
2545
2695
  );
2546
2696
  const useBoundFieldArray = (path) => useFieldArray(structural, path);
2547
2697
  const useBoundVariantFieldArray = (unionPath, field) => useVariantFieldArray(
@@ -2557,9 +2707,10 @@ var createFormHooks = (form, name) => {
2557
2707
  const useBoundIsValidating = (path) => useIsValidating(structural, path);
2558
2708
  const useBoundIsSubmitting = () => useIsSubmitting(structural);
2559
2709
  const useBoundValues = () => useFormValues(structural);
2560
- const useBoundFields = (paths) => useFields(
2710
+ const useBoundFields = (paths, options) => useFields(
2561
2711
  structural,
2562
- paths
2712
+ paths,
2713
+ options
2563
2714
  );
2564
2715
  const useBoundSubmitCount = () => useSubmitCount(structural);
2565
2716
  const useBoundSteps = (steps) => useFormSteps(form, steps);
@@ -2609,7 +2760,7 @@ var FieldShell = ({
2609
2760
  wrapLabel,
2610
2761
  children
2611
2762
  }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "zf-field", children: [
2612
- wrapLabel === true ? /* @__PURE__ */ jsxRuntime.jsxs("label", { htmlFor: id, className: "zf-label", children: [
2763
+ wrapLabel === true && label !== void 0 ? /* @__PURE__ */ jsxRuntime.jsxs("label", { htmlFor: id, className: "zf-label", children: [
2613
2764
  children,
2614
2765
  " ",
2615
2766
  label