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.js CHANGED
@@ -200,10 +200,12 @@ var hasOwnSlot = (obj, path) => parsePath(path).reduce(
200
200
  ).ok;
201
201
  var writeIntoArray = (arr, index, rest, value, kindAt, prefix) => {
202
202
  const length = Math.max(arr.length, index + 1);
203
- return Array.from(
204
- { length },
205
- (_, i) => i === index ? writeSegments(arr[i], rest, value, kindAt, [...prefix, index]) : arr[i]
206
- );
203
+ const child = writeSegments(arr[index], rest, value, kindAt, [
204
+ ...prefix,
205
+ index
206
+ ]);
207
+ if (index < arr.length && child === arr[index]) return arr;
208
+ return Array.from({ length }, (_, i) => i === index ? child : arr[i]);
207
209
  };
208
210
  var refusesHugeIndex = (index) => {
209
211
  if (index > MAX_ARRAY_INDEX) {
@@ -235,10 +237,12 @@ var writeSegments = (obj, segments, value, kindAt, prefix) => {
235
237
  return obj;
236
238
  }
237
239
  const key2 = String(head);
238
- return {
239
- ...obj,
240
- [key2]: writeSegments(obj[key2], rest, value, kindAt, [...prefix, key2])
241
- };
240
+ const child = writeSegments(obj[key2], rest, value, kindAt, [
241
+ ...prefix,
242
+ key2
243
+ ]);
244
+ if (child === obj[key2] && Object.hasOwn(obj, key2)) return obj;
245
+ return { ...obj, [key2]: child };
242
246
  }
243
247
  const kind = kindAt?.(prefix.map(String).join(".")) ?? (typeof head === "number" ? "array" : "record");
244
248
  if (kind === "array") {
@@ -317,7 +321,11 @@ var unwrapForWalk = (s) => {
317
321
  if (s instanceof z.ZodDefault || s instanceof z.ZodPrefault || s instanceof z.ZodCatch || s instanceof z.ZodReadonly || s instanceof z.ZodNonOptional) {
318
322
  return s.def.innerType;
319
323
  }
320
- if (s instanceof z.ZodPipe) return s.def.in;
324
+ if (s instanceof z.ZodPipe) {
325
+ const input = s.def.in;
326
+ return input instanceof z.ZodTransform ? s.def.out : input;
327
+ }
328
+ if (s instanceof z.ZodLazy) return s.def.getter();
321
329
  return null;
322
330
  };
323
331
  var isLeafSchema = (s) => s instanceof z.ZodString || s instanceof z.ZodNumber || s instanceof z.ZodBoolean || s instanceof z.ZodBigInt || s instanceof z.ZodDate || s instanceof z.ZodEnum || s instanceof z.ZodLiteral || s instanceof z.ZodNull || s instanceof z.ZodUndefined || s instanceof z.ZodVoid || s instanceof z.ZodNaN || s instanceof z.ZodSymbol;
@@ -383,10 +391,31 @@ var hasChecks = (s) => {
383
391
  const checks = s.def.checks;
384
392
  return checks !== void 0 && checks.length > 0;
385
393
  };
386
- var fieldSchemaForIntrospection = (schema, path) => {
387
- const childOf = (s, segment) => {
394
+ var optionClaims = (option, discriminator, tag) => {
395
+ const strip = (s) => {
388
396
  const inner = unwrapForWalk(s);
389
- if (inner !== null) return childOf(inner, segment);
397
+ return inner === null ? s : strip(inner);
398
+ };
399
+ const node = strip(option);
400
+ if (!(node instanceof z.ZodObject)) return false;
401
+ const shape = node.shape;
402
+ const at = Object.hasOwn(shape, discriminator) ? shape[discriminator] : void 0;
403
+ if (at === void 0) return false;
404
+ const leaf = strip(at);
405
+ if (leaf instanceof z.ZodLiteral) {
406
+ return leaf.def.values.includes(tag);
407
+ }
408
+ if (leaf instanceof z.ZodEnum) {
409
+ return Object.values(leaf.def.entries).includes(
410
+ tag
411
+ );
412
+ }
413
+ return false;
414
+ };
415
+ var fieldSchemaForIntrospection = (schema, path, values) => {
416
+ const childOf = (s, segment, holder) => {
417
+ const inner = unwrapForWalk(s);
418
+ if (inner !== null) return childOf(inner, segment, holder);
390
419
  if (s instanceof z.ZodObject && typeof segment === "string") {
391
420
  const shape = s.shape;
392
421
  return Object.hasOwn(shape, segment) ? shape[segment] ?? null : null;
@@ -400,19 +429,45 @@ var fieldSchemaForIntrospection = (schema, path) => {
400
429
  if (s instanceof z.ZodRecord) {
401
430
  return s.valueType;
402
431
  }
432
+ if (s instanceof z.ZodIntersection) {
433
+ return childOf(s.def.left, segment, holder) ?? childOf(s.def.right, segment, holder);
434
+ }
403
435
  if (s instanceof z.ZodDiscriminatedUnion || s instanceof z.ZodUnion) {
404
436
  const options = s.def.options;
405
- return options.reduce(
406
- (found, option) => found ?? childOf(option, segment),
437
+ const discriminator = s instanceof z.ZodDiscriminatedUnion ? s.def.discriminator : null;
438
+ const tag = discriminator !== null && typeof holder === "object" && holder !== null ? holder[discriminator] : void 0;
439
+ const live = discriminator === null || tag === void 0 ? void 0 : options.find((option) => optionClaims(option, discriminator, tag));
440
+ const candidates = live === void 0 ? options : [live];
441
+ return candidates.reduce(
442
+ (found, option) => found ?? childOf(option, segment, holder),
407
443
  null
408
444
  );
409
445
  }
410
446
  return null;
411
447
  };
412
- return parsePath(path).reduce(
413
- (current, segment) => current === null ? null : childOf(current, segment),
414
- schema
415
- );
448
+ const segments = parsePath(path);
449
+ return segments.reduce(
450
+ (acc, segment) => acc.s === null ? acc : {
451
+ s: childOf(
452
+ acc.s,
453
+ segment,
454
+ values === void 0 ? void 0 : getAtPath(values, acc.prefix)
455
+ ),
456
+ prefix: acc.prefix === "" ? String(segment) : `${acc.prefix}.${String(segment)}`
457
+ },
458
+ { s: schema, prefix: "" }
459
+ ).s;
460
+ };
461
+ var schemaAcceptsUndefined = (schema) => schema instanceof z.ZodOptional || schema instanceof z.ZodDefault || schema instanceof z.ZodPrefault || schema instanceof z.ZodNullable && schemaAcceptsUndefined(schema.unwrap());
462
+ var isObjectSchemaAt = (schema, path) => {
463
+ const sub = fieldSchemaForIntrospection(schema, path);
464
+ if (sub === null) return true;
465
+ const strip = (s) => {
466
+ const inner = unwrapForWalk(s);
467
+ return inner === null ? s : strip(inner);
468
+ };
469
+ const node = strip(sub);
470
+ return !(node instanceof z.ZodUnion || node instanceof z.ZodDiscriminatedUnion || node instanceof z.ZodRecord || node instanceof z.ZodMap || node instanceof z.ZodSet || node instanceof z.ZodArray || node instanceof z.ZodTuple || isLeafSchema(node));
416
471
  };
417
472
  var containerKindAt = (schema) => (prefix) => {
418
473
  const unwrapAll = (s) => {
@@ -532,7 +587,8 @@ var scopeParse = (result, path, viaSubschema) => {
532
587
  var pruneUnreported = (prev, full) => {
533
588
  const entries = Object.entries(prev);
534
589
  const kept = entries.filter(([k]) => Object.hasOwn(full, k));
535
- return kept.length === entries.length ? prev : Object.fromEntries(kept);
590
+ const unchanged = kept.length === entries.length && kept.every(([k, list]) => full[k] === list);
591
+ return unchanged ? prev : Object.fromEntries(kept.map(([k, list]) => [k, full[k] ?? list]));
536
592
  };
537
593
  var dropEmptyLists = (map) => {
538
594
  const entries = Object.entries(map);
@@ -904,7 +960,14 @@ var createForm = (schema, options) => {
904
960
  fresh.flatMap((path) => Object.entries(errorsFor(path)))
905
961
  ),
906
962
  fresh,
907
- parsed.kind === "full" ? parsed.errors : null
963
+ // The prune map describes the values the batch STARTED with.
964
+ // A skip scope (an index into an empty array) still reads as
965
+ // fresh after an unrelated change, so a batch can commit under
966
+ // changed values; pruning with the stale map then dropped
967
+ // errors committed elsewhere in flight (an email error landing
968
+ // while a wizard step with such a path validated). The prune
969
+ // rides only while the whole values reference held.
970
+ parsed.kind === "full" && live.values === valuesAtStart ? parsed.errors : null
908
971
  ),
909
972
  isValidating: omitKeys(state.isValidating, ownedKeys)
910
973
  }));
@@ -1078,12 +1141,14 @@ var createForm = (schema, options) => {
1078
1141
  }
1079
1142
  }
1080
1143
  };
1081
- if (options.validateOnMount === true) {
1144
+ const judgeBaseline = () => {
1145
+ if (options.validateOnMount !== true) return;
1082
1146
  const result = validate();
1083
1147
  if (result.kind === "pending") {
1084
1148
  void result.promise.catch(reportBackgroundFailure);
1085
1149
  }
1086
- }
1150
+ };
1151
+ judgeBaseline();
1087
1152
  return Object.freeze({
1088
1153
  schema,
1089
1154
  store,
@@ -1098,8 +1163,12 @@ var createForm = (schema, options) => {
1098
1163
  return store.subscribe((state) => {
1099
1164
  const next = snapshotField(state, path);
1100
1165
  if (!shallowFieldEqual(ref.current, next)) {
1166
+ const prev = ref.current;
1101
1167
  ref.current = next;
1102
- listener(next);
1168
+ listener(
1169
+ next,
1170
+ prev
1171
+ );
1103
1172
  }
1104
1173
  });
1105
1174
  },
@@ -1134,10 +1203,12 @@ var createForm = (schema, options) => {
1134
1203
  if (Object.is(getAtPath(currentValues, path), value) && (value !== void 0 || hasOwnSlot(currentValues, path))) {
1135
1204
  return;
1136
1205
  }
1206
+ const next = setAtPath(currentValues, path, value, kindAt);
1207
+ if (next === currentValues) return;
1137
1208
  clearArrayOpsUnder(store, path);
1138
1209
  store.setState((state) => ({
1139
1210
  ...state,
1140
- values: setAtPath(state.values, path, value, kindAt),
1211
+ values: next,
1141
1212
  ...errorChannels(state, {
1142
1213
  serverErrors: releaseSpine(state.serverErrors, path)
1143
1214
  })
@@ -1244,6 +1315,7 @@ var createForm = (schema, options) => {
1244
1315
  isValidating: emptyBools,
1245
1316
  isValidatingForm: false
1246
1317
  }));
1318
+ judgeBaseline();
1247
1319
  },
1248
1320
  reset: (nextInitial, resetOptions) => {
1249
1321
  sequences.clear();
@@ -1268,6 +1340,7 @@ var createForm = (schema, options) => {
1268
1340
  isValidatingForm: false
1269
1341
  };
1270
1342
  });
1343
+ if (resetOptions?.keepErrors !== true) judgeBaseline();
1271
1344
  },
1272
1345
  resetField: (path) => {
1273
1346
  const initialSlot = slotAtPath(
@@ -1362,6 +1435,7 @@ var createForm = (schema, options) => {
1362
1435
  },
1363
1436
  snapshot: () => store.getState(),
1364
1437
  restore: (snap) => {
1438
+ sequences.clear();
1365
1439
  clearArrayOpsUnder(store, "");
1366
1440
  store.setState(() => {
1367
1441
  const schemaErrors = dropEmptyLists(snap.schemaErrors ?? emptyErrors);
@@ -1468,6 +1542,7 @@ var persistForm = (form, options) => {
1468
1542
  const schema = form.schema;
1469
1543
  const fillRequiredGaps = (draft, initial, path) => {
1470
1544
  if (!isPlainObject(draft) || !isPlainObject(initial)) return draft;
1545
+ if (!isObjectSchemaAt(schema, path)) return draft;
1471
1546
  return Object.keys(initial).reduce(
1472
1547
  (acc, key) => {
1473
1548
  const childPath = joinPath(path, key);
@@ -1477,21 +1552,34 @@ var persistForm = (form, options) => {
1477
1552
  }
1478
1553
  if (initial[key] === void 0) return acc;
1479
1554
  const sub = fieldSchemaForIntrospection(schema, childPath);
1480
- return sub !== null && isClearableSchema(sub) ? acc : { ...acc, [key]: initial[key] };
1555
+ return sub !== null && schemaAcceptsUndefined(sub) ? acc : { ...acc, [key]: initial[key] };
1481
1556
  },
1482
1557
  draft
1483
1558
  );
1484
1559
  };
1560
+ const applying = { current: false };
1485
1561
  const applyDraft = (values) => {
1486
1562
  const filled = fillRequiredGaps(
1487
1563
  values,
1488
1564
  form.getState().initialValues,
1489
1565
  ""
1490
1566
  );
1491
- if (apply === "edit" || apply === "restore") {
1492
- form.setValues(filled);
1493
- } else {
1494
- form.adoptValues(filled);
1567
+ applying.current = true;
1568
+ try {
1569
+ if (apply === "edit" || apply === "restore") {
1570
+ form.setValues(filled);
1571
+ } else {
1572
+ form.adoptValues(filled);
1573
+ }
1574
+ } finally {
1575
+ applying.current = false;
1576
+ }
1577
+ };
1578
+ const isCurrent = (values) => {
1579
+ try {
1580
+ return JSON.stringify(values) === JSON.stringify(form.getState().values);
1581
+ } catch {
1582
+ return false;
1495
1583
  }
1496
1584
  };
1497
1585
  const referenceShape = () => {
@@ -1502,7 +1590,7 @@ var persistForm = (form, options) => {
1502
1590
  }
1503
1591
  };
1504
1592
  const flexibleAt = (path) => schemaAllowsKindChangeAt(schema, path);
1505
- const inspect = () => {
1593
+ const inspect = (skipWhenCurrent) => {
1506
1594
  const raw = (() => {
1507
1595
  try {
1508
1596
  return storage === null ? null : storage.getItem(options.key);
@@ -1548,14 +1636,15 @@ var persistForm = (form, options) => {
1548
1636
  reason: `its shape conflicts with the form's initial values at "${conflict}" (set the version option when the schema changes)`
1549
1637
  };
1550
1638
  }
1639
+ if (skipWhenCurrent && isCurrent(values)) return { kind: "applied" };
1551
1640
  applyDraft(values);
1552
1641
  return { kind: "applied" };
1553
1642
  } catch {
1554
1643
  return { kind: "ignored", reason: "it is not valid JSON" };
1555
1644
  }
1556
1645
  };
1557
- const load = () => {
1558
- const outcome = inspect();
1646
+ const loadWith = (skipWhenCurrent) => {
1647
+ const outcome = inspect(skipWhenCurrent);
1559
1648
  if (outcome.kind === "ignored" && isDevBuild()) {
1560
1649
  console.warn(
1561
1650
  `[formstand] persistForm: ignored the draft under "${options.key}": ${outcome.reason}. The next edit overwrites it.`
@@ -1563,6 +1652,7 @@ var persistForm = (form, options) => {
1563
1652
  }
1564
1653
  return outcome.kind === "applied";
1565
1654
  };
1655
+ const load = () => loadWith(false);
1566
1656
  const writeDraft = (values) => {
1567
1657
  try {
1568
1658
  storage?.setItem(
@@ -1576,10 +1666,11 @@ var persistForm = (form, options) => {
1576
1666
  };
1577
1667
  const writes = createDebouncer(debounceMs);
1578
1668
  if (apply !== "manual") {
1579
- load();
1669
+ loadWith(true);
1580
1670
  }
1581
1671
  const cleared = { current: false };
1582
1672
  const unsubscribe = form.watchValues((values) => {
1673
+ if (applying.current) return;
1583
1674
  if (cleared.current && values === form.getState().initialValues) return;
1584
1675
  cleared.current = false;
1585
1676
  writes.schedule(() => writeDraft(values));
@@ -1608,15 +1699,27 @@ var usePersistForm = (form, options) => {
1608
1699
  latestOptions.current = options;
1609
1700
  });
1610
1701
  const key = options.key;
1611
- const storage = options.storage;
1702
+ const hasStorage = options.storage !== void 0;
1703
+ const storageProxy = useMemo(
1704
+ () => ({
1705
+ getItem: (k) => latestOptions.current.storage?.getItem(k) ?? null,
1706
+ setItem: (k, v) => latestOptions.current.storage?.setItem(k, v),
1707
+ removeItem: (k) => latestOptions.current.storage?.removeItem(k)
1708
+ }),
1709
+ []
1710
+ );
1612
1711
  useEffect(() => {
1613
- const handle = persistForm(form, latestOptions.current);
1712
+ const options2 = latestOptions.current;
1713
+ const handle = persistForm(form, {
1714
+ ...options2,
1715
+ ...options2.storage === void 0 ? {} : { storage: storageProxy }
1716
+ });
1614
1717
  inner.current = handle;
1615
1718
  return () => {
1616
1719
  inner.current = null;
1617
1720
  handle.dispose();
1618
1721
  };
1619
- }, [form, key, storage]);
1722
+ }, [form, key, hasStorage, storageProxy]);
1620
1723
  return useMemo(
1621
1724
  () => ({
1622
1725
  load: () => inner.current?.load() ?? false,
@@ -1688,6 +1791,12 @@ var numberInputProps = (field) => ({
1688
1791
  });
1689
1792
  var dateToInputText = (value) => {
1690
1793
  if (value === void 0 || value === null) return "";
1794
+ if (typeof value === "string") {
1795
+ const text = value;
1796
+ const parsed = parseDateText(text);
1797
+ if (parsed.kind === "date") return dateToInputText(parsed.value);
1798
+ if (/^\d{4}-\d{2}-\d{2}$/.test(text.trim())) return "";
1799
+ }
1691
1800
  const date = value instanceof Date ? value : new Date(value);
1692
1801
  if (Number.isNaN(date.getTime())) return "";
1693
1802
  const pad = (part) => String(part).padStart(2, "0");
@@ -1788,6 +1897,15 @@ var useMaskedInput = (field, options) => {
1788
1897
  }
1789
1898
  };
1790
1899
  };
1900
+ var settleOnLiveValues = async (form, fields, round = 0) => {
1901
+ const before = fields.map((path) => form.getField(path));
1902
+ const result = await form.validateFieldsAsync(fields);
1903
+ const changed = fields.some(
1904
+ (path, i) => !Object.is(form.getField(path), before[i])
1905
+ );
1906
+ if (!changed) return result;
1907
+ return round < 3 ? settleOnLiveValues(form, fields, round + 1) : null;
1908
+ };
1791
1909
  var useFormSteps = (form, steps) => {
1792
1910
  const [step, setStep] = useState(0);
1793
1911
  const [visited, setVisited] = useState(
@@ -1801,7 +1919,8 @@ var useFormSteps = (form, steps) => {
1801
1919
  async (index) => {
1802
1920
  const definition = latest.current[index];
1803
1921
  if (definition === void 0) return true;
1804
- const result = await form.validateFieldsAsync(definition.fields);
1922
+ const result = await settleOnLiveValues(form, definition.fields);
1923
+ if (result === null) return false;
1805
1924
  if (result.kind === "invalid") {
1806
1925
  Object.keys(result.errors).forEach((key) => {
1807
1926
  form.setTouched(key, true);
@@ -1813,46 +1932,49 @@ var useFormSteps = (form, steps) => {
1813
1932
  [form]
1814
1933
  );
1815
1934
  const currentStep = useRef(step);
1816
- useEffect(() => {
1817
- currentStep.current = step;
1818
- });
1935
+ const navigate = useCallback((target) => {
1936
+ currentStep.current = target;
1937
+ setStep(target);
1938
+ }, []);
1819
1939
  const next = useCallback(async () => {
1820
- const ok = await validateStep(step);
1940
+ const from = currentStep.current;
1941
+ const ok = await validateStep(from);
1821
1942
  if (!ok) return false;
1822
- if (currentStep.current !== step) return false;
1823
- const target = Math.min(step + 1, latest.current.length - 1);
1824
- setStep(target);
1943
+ if (currentStep.current !== from) return false;
1944
+ const target = Math.max(0, Math.min(from + 1, latest.current.length - 1));
1945
+ navigate(target);
1825
1946
  setVisited((prev) => /* @__PURE__ */ new Set([...prev, target]));
1826
1947
  return true;
1827
- }, [validateStep, step]);
1948
+ }, [validateStep, navigate]);
1828
1949
  const back = useCallback(() => {
1829
- setStep((prev) => Math.max(prev - 1, 0));
1830
- }, []);
1950
+ navigate(Math.max(currentStep.current - 1, 0));
1951
+ }, [navigate]);
1831
1952
  const goTo = useCallback(
1832
1953
  async (target) => {
1833
1954
  const count = latest.current.length;
1834
1955
  if (!Number.isFinite(target)) return currentStep.current;
1956
+ const from = currentStep.current;
1835
1957
  const clamped = Math.max(0, Math.min(Math.trunc(target), count - 1));
1836
- if (clamped <= step) {
1837
- setStep(clamped);
1958
+ if (clamped <= from) {
1959
+ navigate(clamped);
1838
1960
  return clamped;
1839
1961
  }
1840
- const walk = async (from) => {
1841
- if (from >= clamped) return clamped;
1842
- return await validateStep(from) ? walk(from + 1) : from;
1962
+ const walk = async (at) => {
1963
+ if (at >= clamped) return clamped;
1964
+ return await validateStep(at) ? walk(at + 1) : at;
1843
1965
  };
1844
- const landed = await walk(step);
1845
- if (currentStep.current !== step) return currentStep.current;
1846
- setStep(landed);
1966
+ const landed = await walk(from);
1967
+ if (currentStep.current !== from) return currentStep.current;
1968
+ navigate(landed);
1847
1969
  setVisited(
1848
1970
  (prev) => /* @__PURE__ */ new Set([
1849
1971
  ...prev,
1850
- ...Array.from({ length: landed - step + 1 }, (_, i) => step + i)
1972
+ ...Array.from({ length: landed - from + 1 }, (_, i) => from + i)
1851
1973
  ])
1852
1974
  );
1853
1975
  return landed;
1854
1976
  },
1855
- [validateStep, step]
1977
+ [validateStep, navigate]
1856
1978
  );
1857
1979
  const errorFlags = useFormSelectorShallow(
1858
1980
  form,
@@ -1945,6 +2067,7 @@ var createFormContext = () => {
1945
2067
  return { Provider, useFormContext };
1946
2068
  };
1947
2069
  function useField(form, pathArg, options) {
2070
+ const formSchema = form.schema;
1948
2071
  const slice = useStore(
1949
2072
  form.store,
1950
2073
  useShallow((state) => {
@@ -1960,25 +2083,26 @@ function useField(form, pathArg, options) {
1960
2083
  error: entryAt(state.errors, p),
1961
2084
  touched: flagAt(state.touched, p),
1962
2085
  dirty: isFieldDirty(value, initialValue2),
1963
- isValidating: flagAt(state.isValidating, p)
2086
+ isValidating: flagAt(state.isValidating, p),
2087
+ // Resolved inside the selector so a discriminant change re-reads
2088
+ // the ACTIVE variant's wrapper for a key the variants share; the
2089
+ // schema node is a stable reference, so the shallow compare holds.
2090
+ schema: formSchema === void 0 ? null : fieldSchemaForIntrospection(formSchema, p, state.values)
1964
2091
  };
1965
2092
  })
1966
2093
  );
1967
2094
  const path = slice.path;
1968
- const formSchema = form.schema;
2095
+ const sub = slice.schema;
1969
2096
  const initialValue = slice.initialValue;
1970
2097
  const { emptyValue, clearable } = useMemo(() => {
1971
- if (formSchema !== void 0) {
1972
- const sub = fieldSchemaForIntrospection(formSchema, path);
1973
- if (sub !== null) {
1974
- return {
1975
- emptyValue: emptyValueForSchema(sub),
1976
- clearable: isClearableSchema(sub)
1977
- };
1978
- }
2098
+ if (sub !== null) {
2099
+ return {
2100
+ emptyValue: emptyValueForSchema(sub),
2101
+ clearable: isClearableSchema(sub)
2102
+ };
1979
2103
  }
1980
2104
  return initialValue === null ? { emptyValue: null, clearable: true } : { emptyValue: void 0, clearable: false };
1981
- }, [formSchema, path, initialValue]);
2105
+ }, [sub, initialValue]);
1982
2106
  const debounceMs = options?.debounceMs;
1983
2107
  const debouncerRef = useRef(null);
1984
2108
  useEffect(() => {
@@ -2088,10 +2212,11 @@ function useField(form, pathArg, options) {
2088
2212
  }
2089
2213
 
2090
2214
  // src/react/useVariantField.ts
2091
- function useVariantField(form, unionPath, field) {
2215
+ function useVariantField(form, unionPath, field, options) {
2092
2216
  return useField(
2093
2217
  form,
2094
- `${unionPath}.${field}`
2218
+ `${unionPath}.${field}`,
2219
+ options
2095
2220
  );
2096
2221
  }
2097
2222
  var EMPTY_ITEMS = [];
@@ -2254,6 +2379,12 @@ function useFieldArray(form, pathArg) {
2254
2379
  const items = slice.items;
2255
2380
  const error = slice.error;
2256
2381
  const ids = deriveIds(form.store, path, items).ids;
2382
+ const validateNow = useCallback(() => {
2383
+ const result = form.validateField?.(path);
2384
+ if (result?.kind === "pending") {
2385
+ void result.promise.catch(reportBackgroundFailure);
2386
+ }
2387
+ }, [form, path]);
2257
2388
  const revalidate = useCallback(() => {
2258
2389
  const state = form.store.getState();
2259
2390
  if (shouldValidateOn(
@@ -2263,9 +2394,9 @@ function useFieldArray(form, pathArg) {
2263
2394
  state.submitCount > 0,
2264
2395
  flagAt(state.touched, path)
2265
2396
  )) {
2266
- form.validateField?.(path);
2397
+ validateNow();
2267
2398
  }
2268
- }, [form, path]);
2399
+ }, [form, path, validateNow]);
2269
2400
  const push = useCallback(
2270
2401
  (item) => {
2271
2402
  form.arrayPush(path, item);
@@ -2330,13 +2461,17 @@ function useFieldArray(form, pathArg) {
2330
2461
  state.submitCount > 0,
2331
2462
  true
2332
2463
  )) {
2333
- form.validateField?.(path);
2464
+ validateNow();
2334
2465
  }
2335
- }, [form, path]);
2466
+ }, [form, path, validateNow]);
2336
2467
  const validate = useCallback(
2337
2468
  () => form.validateField?.(path) ?? { kind: "valid" },
2338
2469
  [form, path]
2339
2470
  );
2471
+ const validateAsync2 = useCallback(
2472
+ () => form.validateFieldAsync?.(path) ?? Promise.resolve({ kind: "valid" }),
2473
+ [form, path]
2474
+ );
2340
2475
  const replace = useCallback(
2341
2476
  (next) => {
2342
2477
  form.setValue?.(path, next);
@@ -2346,7 +2481,15 @@ function useFieldArray(form, pathArg) {
2346
2481
  );
2347
2482
  const update = useCallback(
2348
2483
  (index, item) => {
2349
- form.setValue?.(`${path}.${index}`, item);
2484
+ const current = getAtPath(form.store.getState().values, path);
2485
+ const length = Array.isArray(current) ? current.length : 0;
2486
+ if (!Number.isInteger(index) || index < 0 || index >= length) {
2487
+ console.warn(
2488
+ `[formstand] array op on "${path}" with an out-of-range or non-integer index (length ${String(length)}). Operation skipped.`
2489
+ );
2490
+ return;
2491
+ }
2492
+ form.setValue?.(`${path}.${String(index)}`, item);
2350
2493
  revalidate();
2351
2494
  },
2352
2495
  [form, path, revalidate]
@@ -2378,6 +2521,7 @@ function useFieldArray(form, pathArg) {
2378
2521
  setTouched,
2379
2522
  onBlur,
2380
2523
  validate,
2524
+ validateAsync: validateAsync2,
2381
2525
  push,
2382
2526
  remove,
2383
2527
  insert,
@@ -2401,6 +2545,7 @@ function useFieldArray(form, pathArg) {
2401
2545
  setTouched,
2402
2546
  onBlur,
2403
2547
  validate,
2548
+ validateAsync2,
2404
2549
  push,
2405
2550
  remove,
2406
2551
  insert,
@@ -2428,7 +2573,7 @@ var useFormError = (form) => useStore(
2428
2573
  function useFormValues(form) {
2429
2574
  return useStore(form.store, (state) => state.values);
2430
2575
  }
2431
- function useFields(form, paths) {
2576
+ function useFields(form, paths, options) {
2432
2577
  const [arity] = useState(paths.length);
2433
2578
  if (paths.length !== arity) {
2434
2579
  throw new Error(
@@ -2438,7 +2583,11 @@ function useFields(form, paths) {
2438
2583
  const fields = paths.map(
2439
2584
  (path) => (
2440
2585
  // 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
2441
- useField(form, path)
2586
+ useField(
2587
+ form,
2588
+ path,
2589
+ options
2590
+ )
2442
2591
  )
2443
2592
  );
2444
2593
  return useMemo(() => {
@@ -2501,10 +2650,11 @@ var capitalize = (name) => name.length === 0 ? name : name.charAt(0).toUpperCase
2501
2650
  var createFormHooks = (form, name) => {
2502
2651
  const structural = form;
2503
2652
  const useBoundField = (path, options) => useField(structural, path, options);
2504
- const useBoundVariantField = (unionPath, field) => useVariantField(
2653
+ const useBoundVariantField = (unionPath, field, options) => useVariantField(
2505
2654
  structural,
2506
2655
  unionPath,
2507
- field
2656
+ field,
2657
+ options
2508
2658
  );
2509
2659
  const useBoundFieldArray = (path) => useFieldArray(structural, path);
2510
2660
  const useBoundVariantFieldArray = (unionPath, field) => useVariantFieldArray(
@@ -2520,9 +2670,10 @@ var createFormHooks = (form, name) => {
2520
2670
  const useBoundIsValidating = (path) => useIsValidating(structural, path);
2521
2671
  const useBoundIsSubmitting = () => useIsSubmitting(structural);
2522
2672
  const useBoundValues = () => useFormValues(structural);
2523
- const useBoundFields = (paths) => useFields(
2673
+ const useBoundFields = (paths, options) => useFields(
2524
2674
  structural,
2525
- paths
2675
+ paths,
2676
+ options
2526
2677
  );
2527
2678
  const useBoundSubmitCount = () => useSubmitCount(structural);
2528
2679
  const useBoundSteps = (steps) => useFormSteps(form, steps);
@@ -2572,7 +2723,7 @@ var FieldShell = ({
2572
2723
  wrapLabel,
2573
2724
  children
2574
2725
  }) => /* @__PURE__ */ jsxs("div", { className: "zf-field", children: [
2575
- wrapLabel === true ? /* @__PURE__ */ jsxs("label", { htmlFor: id, className: "zf-label", children: [
2726
+ wrapLabel === true && label !== void 0 ? /* @__PURE__ */ jsxs("label", { htmlFor: id, className: "zf-label", children: [
2576
2727
  children,
2577
2728
  " ",
2578
2729
  label