formstand 0.6.0 → 0.7.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,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0 — 2026-07-09
4
+
5
+ ### Added
6
+
7
+ - `createFormHooks(form, name?)` — every hook pre-wired to one form, the
8
+ provider-free way to share a module-singleton form. The optional name is
9
+ baked into the hook names at the type level and at runtime
10
+ (`createFormHooks(form, "invoice")` → `useInvoiceField`,
11
+ `useInvoiceFieldArray`, `useInvoiceSelector`, `useInvoiceSelectorShallow`,
12
+ `useInvoiceError`, `useInvoiceIsDirty`, `useInvoiceIsValid`,
13
+ `useInvoiceIsSubmitting`, `useInvoiceSubmitCount`), so a typo'd
14
+ destructure is a compile error; omit the name for unprefixed keys. Every
15
+ bound hook keeps its unbound signature minus the `form` argument — typed
16
+ paths, array item inference, path-scoped flags. Documented alongside
17
+ `createFormContext` with the singleton/SSR caveat; new "Hooks factory"
18
+ playground tab.
19
+
20
+ ### Docs & examples (no package changes)
21
+
22
+ - New "Onboarding" playground tab: a 26-field, five-section feature module
23
+ built on `createFormHooks` — `schema.ts` / `types.ts` / `hooks.ts`, one
24
+ file per field, one per section (section headers run the path-scoped
25
+ `useIsDirty`/`useIsValid` flags), with the whole folder shown in the
26
+ View code panel.
27
+ - The playground's View code panel is syntax-highlighted (Prism, ~25KB —
28
+ read-only, so no editor bundle).
29
+
3
30
  ## 0.6.0 — 2026-07-09
4
31
 
5
32
  ### Added
package/dist/index.cjs CHANGED
@@ -985,38 +985,6 @@ var createFormContext = () => {
985
985
  };
986
986
  return { Provider, useFormContext };
987
987
  };
988
- function useFormSelector(form, selector) {
989
- return react$1.useStore(form.store, selector);
990
- }
991
- function useFormSelectorShallow(form, selector) {
992
- return react$1.useStore(form.store, shallow.useShallow(selector));
993
- }
994
- var useFormError = (form) => react$1.useStore(
995
- form.store,
996
- shallow.useShallow((state) => state.errors[""])
997
- );
998
- function useIsDirty(form, path) {
999
- return react$1.useStore(
1000
- form.store,
1001
- (state) => path === void 0 || path === "" ? isFieldDirty(state.values, state.initialValues) : isFieldDirty(
1002
- getAtPath(state.values, path),
1003
- getAtPath(state.initialValues, path)
1004
- )
1005
- );
1006
- }
1007
- function useIsValid(form, path) {
1008
- return react$1.useStore(form.store, (state) => {
1009
- for (const k in state.errors) {
1010
- const errs = state.errors[k];
1011
- if (errs !== void 0 && errs.length > 0 && (path === void 0 || path === "" || isPathOrChild(k, path))) {
1012
- return false;
1013
- }
1014
- }
1015
- return true;
1016
- });
1017
- }
1018
- var useIsSubmitting = (form) => react$1.useStore(form.store, (state) => state.isSubmitting);
1019
- var useSubmitCount = (form) => react$1.useStore(form.store, (state) => state.submitCount);
1020
988
  function useField(form, pathArg, options) {
1021
989
  const slice = react$1.useStore(
1022
990
  form.store,
@@ -1146,6 +1114,169 @@ function useField(form, pathArg, options) {
1146
1114
  [slice, emptyValue, setValue, setTouched, setError, clearError, validate, validateAsync2, onBlur]
1147
1115
  );
1148
1116
  }
1117
+ var EMPTY_ID_STATE = { items: [], ids: [], counter: 0 };
1118
+ var EMPTY_ITEMS = [];
1119
+ var reconcileIds = (prev, nextItems) => {
1120
+ if (prev.items === nextItems) return prev;
1121
+ if (prev.items.length === nextItems.length && prev.items.every((item, i) => Object.is(item, nextItems[i]))) {
1122
+ return prev;
1123
+ }
1124
+ const buckets = prev.items.reduce((acc, item, i) => {
1125
+ const bucket = acc.get(item);
1126
+ if (bucket === void 0) {
1127
+ acc.set(item, [i]);
1128
+ } else {
1129
+ bucket.push(i);
1130
+ }
1131
+ return acc;
1132
+ }, /* @__PURE__ */ new Map());
1133
+ const matchedIds = nextItems.map((item) => {
1134
+ const prevIndex = buckets.get(item)?.shift();
1135
+ return prevIndex === void 0 ? null : prev.ids[prevIndex] ?? null;
1136
+ });
1137
+ const leftover = [...buckets.values()].flat().sort((a, b) => a - b).map((i) => prev.ids[i]).filter((id) => id !== void 0);
1138
+ const ordinals = matchedIds.reduce((acc, id) => {
1139
+ const count = acc.length === 0 ? 0 : acc[acc.length - 1] ?? 0;
1140
+ acc.push(id === null ? count + 1 : count);
1141
+ return acc;
1142
+ }, []);
1143
+ const unmatched = ordinals.length === 0 ? 0 : ordinals[ordinals.length - 1] ?? 0;
1144
+ const ids = matchedIds.map((id, i) => {
1145
+ if (id !== null) return id;
1146
+ const ordinal = ordinals[i] ?? 0;
1147
+ return leftover[ordinal - 1] ?? `__zfa_${prev.counter + (ordinal - leftover.length)}`;
1148
+ });
1149
+ return {
1150
+ items: nextItems,
1151
+ ids,
1152
+ counter: prev.counter + Math.max(0, unmatched - leftover.length)
1153
+ };
1154
+ };
1155
+ function useFieldArray(form, pathArg) {
1156
+ const path = react$1.useStore(
1157
+ form.store,
1158
+ (state) => typeof pathArg === "function" ? pathArg(state) : pathArg
1159
+ );
1160
+ const slice = react$1.useStore(
1161
+ form.store,
1162
+ shallow.useShallow((state) => {
1163
+ const value = getAtPath(state.values, path);
1164
+ return {
1165
+ items: Array.isArray(value) ? value : EMPTY_ITEMS,
1166
+ error: state.errors[path]
1167
+ };
1168
+ })
1169
+ );
1170
+ const items = slice.items;
1171
+ const error = slice.error;
1172
+ const [idEntry, setIdEntry] = react.useState({ form, path, state: EMPTY_ID_STATE });
1173
+ const base = idEntry.form === form && idEntry.path === path ? idEntry.state : { ...EMPTY_ID_STATE, counter: idEntry.state.counter };
1174
+ const nextIdState = reconcileIds(base, items);
1175
+ if (idEntry.form !== form || idEntry.path !== path || idEntry.state !== nextIdState) {
1176
+ setIdEntry({ form, path, state: nextIdState });
1177
+ }
1178
+ const ids = nextIdState.ids;
1179
+ const push = react.useCallback(
1180
+ (item) => form.arrayPush(path, item),
1181
+ [form, path]
1182
+ );
1183
+ const remove = react.useCallback(
1184
+ (index) => form.arrayRemove(path, index),
1185
+ [form, path]
1186
+ );
1187
+ const insert = react.useCallback(
1188
+ (index, item) => form.arrayInsert(path, index, item),
1189
+ [form, path]
1190
+ );
1191
+ const move = react.useCallback(
1192
+ (from, to) => form.arrayMove(path, from, to),
1193
+ [form, path]
1194
+ );
1195
+ const swap = react.useCallback(
1196
+ (a, b) => form.arraySwap(path, a, b),
1197
+ [form, path]
1198
+ );
1199
+ const fields = react.useMemo(
1200
+ () => items.map((value, index) => ({
1201
+ id: ids[index] ?? `__zfa_fallback_${index}`,
1202
+ value
1203
+ })),
1204
+ [items, ids]
1205
+ );
1206
+ return react.useMemo(
1207
+ () => ({
1208
+ fields,
1209
+ items,
1210
+ length: items.length,
1211
+ error,
1212
+ push,
1213
+ remove,
1214
+ insert,
1215
+ move,
1216
+ swap
1217
+ }),
1218
+ [fields, items, error, push, remove, insert, move, swap]
1219
+ );
1220
+ }
1221
+ var useFormError = (form) => react$1.useStore(
1222
+ form.store,
1223
+ shallow.useShallow((state) => state.errors[""])
1224
+ );
1225
+ function useIsDirty(form, path) {
1226
+ return react$1.useStore(
1227
+ form.store,
1228
+ (state) => path === void 0 || path === "" ? isFieldDirty(state.values, state.initialValues) : isFieldDirty(
1229
+ getAtPath(state.values, path),
1230
+ getAtPath(state.initialValues, path)
1231
+ )
1232
+ );
1233
+ }
1234
+ function useIsValid(form, path) {
1235
+ return react$1.useStore(form.store, (state) => {
1236
+ for (const k in state.errors) {
1237
+ const errs = state.errors[k];
1238
+ if (errs !== void 0 && errs.length > 0 && (path === void 0 || path === "" || isPathOrChild(k, path))) {
1239
+ return false;
1240
+ }
1241
+ }
1242
+ return true;
1243
+ });
1244
+ }
1245
+ var useIsSubmitting = (form) => react$1.useStore(form.store, (state) => state.isSubmitting);
1246
+ var useSubmitCount = (form) => react$1.useStore(form.store, (state) => state.submitCount);
1247
+ function useFormSelector(form, selector) {
1248
+ return react$1.useStore(form.store, selector);
1249
+ }
1250
+ function useFormSelectorShallow(form, selector) {
1251
+ return react$1.useStore(form.store, shallow.useShallow(selector));
1252
+ }
1253
+
1254
+ // src/react/createFormHooks.ts
1255
+ var capitalize = (name) => name.length === 0 ? name : name.charAt(0).toUpperCase() + name.slice(1);
1256
+ var createFormHooks = (form, name) => {
1257
+ const structural = form;
1258
+ const useBoundField = (path, options) => useField(structural, path, options);
1259
+ const useBoundFieldArray = (path) => useFieldArray(structural, path);
1260
+ const useBoundSelector = (selector) => useFormSelector(structural, selector);
1261
+ const useBoundSelectorShallow = (selector) => useFormSelectorShallow(structural, selector);
1262
+ const useBoundError = () => useFormError(structural);
1263
+ const useBoundIsDirty = (path) => useIsDirty(structural, path);
1264
+ const useBoundIsValid = (path) => useIsValid(structural, path);
1265
+ const useBoundIsSubmitting = () => useIsSubmitting(structural);
1266
+ const useBoundSubmitCount = () => useSubmitCount(structural);
1267
+ const prefix = capitalize(name ?? "");
1268
+ return {
1269
+ [`use${prefix}Field`]: useBoundField,
1270
+ [`use${prefix}FieldArray`]: useBoundFieldArray,
1271
+ [`use${prefix}Selector`]: useBoundSelector,
1272
+ [`use${prefix}SelectorShallow`]: useBoundSelectorShallow,
1273
+ [`use${prefix}Error`]: useBoundError,
1274
+ [`use${prefix}IsDirty`]: useBoundIsDirty,
1275
+ [`use${prefix}IsValid`]: useBoundIsValid,
1276
+ [`use${prefix}IsSubmitting`]: useBoundIsSubmitting,
1277
+ [`use${prefix}SubmitCount`]: useBoundSubmitCount
1278
+ };
1279
+ };
1149
1280
 
1150
1281
  // src/react/inputProps.ts
1151
1282
  var ariaInvalid = (field) => field.error !== void 0 && field.error.length > 0 ? true : void 0;
@@ -1400,110 +1531,6 @@ var focusField = (path, root) => {
1400
1531
  const controls = focusableControls(root ?? document);
1401
1532
  return path === "" ? (root !== void 0 || document.querySelectorAll("form").length <= 1) && focusFirstOf(controls) : focusFirstOf(controls.filter((el) => nameMatchesAny(el, [path])));
1402
1533
  };
1403
- var EMPTY_ID_STATE = { items: [], ids: [], counter: 0 };
1404
- var EMPTY_ITEMS = [];
1405
- var reconcileIds = (prev, nextItems) => {
1406
- if (prev.items === nextItems) return prev;
1407
- if (prev.items.length === nextItems.length && prev.items.every((item, i) => Object.is(item, nextItems[i]))) {
1408
- return prev;
1409
- }
1410
- const buckets = prev.items.reduce((acc, item, i) => {
1411
- const bucket = acc.get(item);
1412
- if (bucket === void 0) {
1413
- acc.set(item, [i]);
1414
- } else {
1415
- bucket.push(i);
1416
- }
1417
- return acc;
1418
- }, /* @__PURE__ */ new Map());
1419
- const matchedIds = nextItems.map((item) => {
1420
- const prevIndex = buckets.get(item)?.shift();
1421
- return prevIndex === void 0 ? null : prev.ids[prevIndex] ?? null;
1422
- });
1423
- const leftover = [...buckets.values()].flat().sort((a, b) => a - b).map((i) => prev.ids[i]).filter((id) => id !== void 0);
1424
- const ordinals = matchedIds.reduce((acc, id) => {
1425
- const count = acc.length === 0 ? 0 : acc[acc.length - 1] ?? 0;
1426
- acc.push(id === null ? count + 1 : count);
1427
- return acc;
1428
- }, []);
1429
- const unmatched = ordinals.length === 0 ? 0 : ordinals[ordinals.length - 1] ?? 0;
1430
- const ids = matchedIds.map((id, i) => {
1431
- if (id !== null) return id;
1432
- const ordinal = ordinals[i] ?? 0;
1433
- return leftover[ordinal - 1] ?? `__zfa_${prev.counter + (ordinal - leftover.length)}`;
1434
- });
1435
- return {
1436
- items: nextItems,
1437
- ids,
1438
- counter: prev.counter + Math.max(0, unmatched - leftover.length)
1439
- };
1440
- };
1441
- function useFieldArray(form, pathArg) {
1442
- const path = react$1.useStore(
1443
- form.store,
1444
- (state) => typeof pathArg === "function" ? pathArg(state) : pathArg
1445
- );
1446
- const slice = react$1.useStore(
1447
- form.store,
1448
- shallow.useShallow((state) => {
1449
- const value = getAtPath(state.values, path);
1450
- return {
1451
- items: Array.isArray(value) ? value : EMPTY_ITEMS,
1452
- error: state.errors[path]
1453
- };
1454
- })
1455
- );
1456
- const items = slice.items;
1457
- const error = slice.error;
1458
- const [idEntry, setIdEntry] = react.useState({ form, path, state: EMPTY_ID_STATE });
1459
- const base = idEntry.form === form && idEntry.path === path ? idEntry.state : { ...EMPTY_ID_STATE, counter: idEntry.state.counter };
1460
- const nextIdState = reconcileIds(base, items);
1461
- if (idEntry.form !== form || idEntry.path !== path || idEntry.state !== nextIdState) {
1462
- setIdEntry({ form, path, state: nextIdState });
1463
- }
1464
- const ids = nextIdState.ids;
1465
- const push = react.useCallback(
1466
- (item) => form.arrayPush(path, item),
1467
- [form, path]
1468
- );
1469
- const remove = react.useCallback(
1470
- (index) => form.arrayRemove(path, index),
1471
- [form, path]
1472
- );
1473
- const insert = react.useCallback(
1474
- (index, item) => form.arrayInsert(path, index, item),
1475
- [form, path]
1476
- );
1477
- const move = react.useCallback(
1478
- (from, to) => form.arrayMove(path, from, to),
1479
- [form, path]
1480
- );
1481
- const swap = react.useCallback(
1482
- (a, b) => form.arraySwap(path, a, b),
1483
- [form, path]
1484
- );
1485
- const fields = react.useMemo(
1486
- () => items.map((value, index) => ({
1487
- id: ids[index] ?? `__zfa_fallback_${index}`,
1488
- value
1489
- })),
1490
- [items, ids]
1491
- );
1492
- return react.useMemo(
1493
- () => ({
1494
- fields,
1495
- items,
1496
- length: items.length,
1497
- error,
1498
- push,
1499
- remove,
1500
- insert,
1501
- move,
1502
- swap
1503
- }),
1504
- [fields, items, error, push, remove, insert, move, swap]
1505
- );
1506
- }
1507
1534
 
1508
1535
  exports.CheckboxField = CheckboxField;
1509
1536
  exports.NumberField = NumberField;
@@ -1512,6 +1539,7 @@ exports.TextField = TextField;
1512
1539
  exports.checkboxProps = checkboxProps;
1513
1540
  exports.createForm = createForm;
1514
1541
  exports.createFormContext = createFormContext;
1542
+ exports.createFormHooks = createFormHooks;
1515
1543
  exports.emptyValueForSchema = emptyValueForSchema;
1516
1544
  exports.flattenIssues = flattenIssues;
1517
1545
  exports.focusField = focusField;