formstand 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -57,7 +57,7 @@ var valuesEqual = (a, b) => {
57
57
  if (isPlainObject(a) && isPlainObject(b)) {
58
58
  const aKeys = Object.keys(a);
59
59
  const bKeys = Object.keys(b);
60
- return aKeys.length === bKeys.length && aKeys.every((k) => valuesEqual(a[k], b[k]));
60
+ return aKeys.length === bKeys.length && aKeys.every((k) => Object.hasOwn(b, k) && valuesEqual(a[k], b[k]));
61
61
  }
62
62
  return false;
63
63
  };
@@ -92,7 +92,7 @@ var readSegment = (obj, segment) => {
92
92
  if (Array.isArray(obj)) {
93
93
  return typeof segment === "number" ? obj[segment] : void 0;
94
94
  }
95
- return isPlainRecord(obj) ? obj[String(segment)] : void 0;
95
+ return isPlainRecord(obj) && Object.hasOwn(obj, String(segment)) ? obj[String(segment)] : void 0;
96
96
  };
97
97
  var getAtPath = (obj, path) => parsePath(path).reduce(readSegment, obj);
98
98
  var slotAtPath = (obj, path) => {
@@ -135,6 +135,12 @@ var writeSegments = (obj, segments, value) => {
135
135
  return writeIntoArray(obj, head, rest, value);
136
136
  }
137
137
  if (isPlainRecord(obj)) {
138
+ if (!isPlainObject(obj)) {
139
+ console.warn(
140
+ `[formstand] cannot write key "${String(head)}" through a non-plain object (Date/Map/Set/class instance); value left unchanged.`
141
+ );
142
+ return obj;
143
+ }
138
144
  const key = String(head);
139
145
  return { ...obj, [key]: writeSegments(obj[key], rest, value) };
140
146
  }
@@ -668,7 +674,11 @@ var createForm = (schema, options) => {
668
674
  });
669
675
  }
670
676
  const merged = stale ? result.errors : store.getState().errors;
671
- onInvalid?.(merged);
677
+ try {
678
+ onInvalid?.(merged);
679
+ } catch (error) {
680
+ return { kind: "error", error };
681
+ }
672
682
  return { kind: "invalid", errors: merged };
673
683
  }
674
684
  if (!stale) {
@@ -928,9 +938,13 @@ var createForm = (schema, options) => {
928
938
  // In-flight state is owned by live passes, never by snapshots: a
929
939
  // snapshotted flag has no pass left to clear it (the pass that set
930
940
  // it cleared its ORIGINAL entry when it settled), so restoring one
931
- // would stick it forever.
941
+ // would stick it forever. isSubmitting follows the same rule — a
942
+ // mid-submit snapshot restored later must not disable submit
943
+ // buttons forever, so it reflects the LIVE submit count instead of
944
+ // the snapshotted flag (a live pass's finally still resets it).
932
945
  isValidating: emptyBools,
933
- isValidatingForm: false
946
+ isValidatingForm: false,
947
+ isSubmitting: inFlight.count > 0
934
948
  };
935
949
  })
936
950
  });
@@ -958,15 +972,17 @@ var useForm = (schema, options) => {
958
972
  formRef.current = createForm(schema, options);
959
973
  }
960
974
  const form = formRef.current;
975
+ const mountInitialsRef = useRef(options.initialValues);
961
976
  const warnedRef = useRef(false);
962
977
  useEffect(() => {
963
- if (!warnedRef.current && form.schema !== schema) {
978
+ const changedInitials = !Object.is(options.initialValues, mountInitialsRef.current) && !valuesEqual(options.initialValues, mountInitialsRef.current);
979
+ if (!warnedRef.current && (form.schema !== schema || changedInitials)) {
964
980
  warnedRef.current = true;
965
981
  console.warn(
966
- "[formstand] useForm received a different schema reference after the first render; schema and options changes are ignored for the life of the component. If the schema is defined inline this is harmless (but consider hoisting it to module scope). For initial values that arrive later (e.g. from a fetch), call form.adoptValues(values) or form.reset(values) instead."
982
+ "[formstand] useForm received a different schema or initial values after the first render; schema and options changes are ignored for the life of the component. If the schema is defined inline this is harmless (but consider hoisting it to module scope). For initial values that arrive later (e.g. from a fetch), call form.adoptValues(values) or form.reset(values) instead."
967
983
  );
968
984
  }
969
- }, [form, schema]);
985
+ }, [form, schema, options.initialValues]);
970
986
  return form;
971
987
  };
972
988
  var createFormContext = () => {
@@ -1132,23 +1148,22 @@ var reconcileIds = (prev, nextItems) => {
1132
1148
  const prevIndex = buckets.get(item)?.shift();
1133
1149
  return prevIndex === void 0 ? null : prev.ids[prevIndex] ?? null;
1134
1150
  });
1135
- const leftover = [...buckets.values()].flat().sort((a, b) => a - b).map((i) => prev.ids[i]).filter((id) => id !== void 0);
1136
- const ordinals = matchedIds.reduce((acc, id) => {
1137
- const count = acc.length === 0 ? 0 : acc[acc.length - 1] ?? 0;
1138
- acc.push(id === null ? count + 1 : count);
1139
- return acc;
1151
+ const leftoverAt = new Map(
1152
+ [...buckets.values()].flat().flatMap((i) => {
1153
+ const id = prev.ids[i];
1154
+ return id === void 0 ? [] : [[i, id]];
1155
+ })
1156
+ );
1157
+ const reused = matchedIds.map((id, i) => id ?? leftoverAt.get(i) ?? null);
1158
+ const mintCounts = reused.reduce((acc, id) => {
1159
+ const count = acc[acc.length - 1] ?? 0;
1160
+ return [...acc, id === null ? count + 1 : count];
1140
1161
  }, []);
1141
- const unmatched = ordinals.length === 0 ? 0 : ordinals[ordinals.length - 1] ?? 0;
1142
- const ids = matchedIds.map((id, i) => {
1143
- if (id !== null) return id;
1144
- const ordinal = ordinals[i] ?? 0;
1145
- return leftover[ordinal - 1] ?? `__zfa_${prev.counter + (ordinal - leftover.length)}`;
1146
- });
1147
- return {
1148
- items: nextItems,
1149
- ids,
1150
- counter: prev.counter + Math.max(0, unmatched - leftover.length)
1151
- };
1162
+ const minted = mintCounts[mintCounts.length - 1] ?? 0;
1163
+ const ids = reused.map(
1164
+ (id, i) => id ?? `__zfa_${prev.counter + (mintCounts[i] ?? 0)}`
1165
+ );
1166
+ return { items: nextItems, ids, counter: prev.counter + minted };
1152
1167
  };
1153
1168
  function useFieldArray(form, pathArg) {
1154
1169
  const path = useStore(
@@ -1465,7 +1480,8 @@ var SelectField = ({
1465
1480
  const id = useId();
1466
1481
  const errorId = `${id}-error`;
1467
1482
  const field = useField(form, path);
1468
- const showEmptyOption = field.value === void 0 || field.value === null || placeholder !== void 0;
1483
+ const clearable = field.emptyValue === null;
1484
+ const showEmptyOption = clearable || field.value === void 0 || field.value === null || placeholder !== void 0;
1469
1485
  return /* @__PURE__ */ jsxs("div", { className: "zf-field", children: [
1470
1486
  label !== void 0 ? /* @__PURE__ */ jsx("label", { htmlFor: id, className: "zf-label", children: label }) : null,
1471
1487
  /* @__PURE__ */ jsxs(
@@ -1476,7 +1492,7 @@ var SelectField = ({
1476
1492
  ...selectProps(field),
1477
1493
  "aria-describedby": hasError(field.error) ? errorId : void 0,
1478
1494
  children: [
1479
- showEmptyOption ? /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: placeholder ?? "" }) : null,
1495
+ showEmptyOption ? /* @__PURE__ */ jsx("option", { value: "", disabled: !clearable, children: placeholder ?? "" }) : null,
1480
1496
  options.map((opt, index) => (
1481
1497
  // Index-qualified: option lists may legitimately repeat a value
1482
1498
  // (e.g. differently-labelled aliases), and bare value keys would