formstand 0.5.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 +41 -1
- package/dist/index.cjs +164 -127
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -53
- package/dist/index.d.ts +90 -53
- package/dist/index.js +165 -129
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
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
|
+
|
|
30
|
+
## 0.6.0 — 2026-07-09
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- `useIsDirty(form, path?)` and `useIsValid(form, path?)`: an optional
|
|
35
|
+
typed path scopes the flag to a subtree with the library's usual prefix
|
|
36
|
+
semantics (`"shipping"` covers `shipping.city`; for validity the path's
|
|
37
|
+
own key counts, so array-level errors match their array's path).
|
|
38
|
+
Subscriptions stay boolean-only — the component re-renders when the flag
|
|
39
|
+
flips, not on every keystroke like `useField(...).dirty`. Omitting the
|
|
40
|
+
path keeps the whole-form behavior; schema-less `FieldFormApi`-style
|
|
41
|
+
forms take plain string paths.
|
|
42
|
+
|
|
43
|
+
## formstand-cli 0.2.2 — 2026-07-09
|
|
4
44
|
|
|
5
45
|
- Generated array hooks drop the explicit item type
|
|
6
46
|
(`useFieldArray(form, "items")`): formstand ≥ 0.5 infers it from the
|
package/dist/index.cjs
CHANGED
|
@@ -985,29 +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
|
-
var useIsDirty = (form) => react$1.useStore(
|
|
999
|
-
form.store,
|
|
1000
|
-
(state) => isFieldDirty(state.values, state.initialValues)
|
|
1001
|
-
);
|
|
1002
|
-
var useIsValid = (form) => react$1.useStore(form.store, (state) => {
|
|
1003
|
-
for (const k in state.errors) {
|
|
1004
|
-
const errs = state.errors[k];
|
|
1005
|
-
if (errs !== void 0 && errs.length > 0) return false;
|
|
1006
|
-
}
|
|
1007
|
-
return true;
|
|
1008
|
-
});
|
|
1009
|
-
var useIsSubmitting = (form) => react$1.useStore(form.store, (state) => state.isSubmitting);
|
|
1010
|
-
var useSubmitCount = (form) => react$1.useStore(form.store, (state) => state.submitCount);
|
|
1011
988
|
function useField(form, pathArg, options) {
|
|
1012
989
|
const slice = react$1.useStore(
|
|
1013
990
|
form.store,
|
|
@@ -1137,6 +1114,169 @@ function useField(form, pathArg, options) {
|
|
|
1137
1114
|
[slice, emptyValue, setValue, setTouched, setError, clearError, validate, validateAsync2, onBlur]
|
|
1138
1115
|
);
|
|
1139
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
|
+
};
|
|
1140
1280
|
|
|
1141
1281
|
// src/react/inputProps.ts
|
|
1142
1282
|
var ariaInvalid = (field) => field.error !== void 0 && field.error.length > 0 ? true : void 0;
|
|
@@ -1391,110 +1531,6 @@ var focusField = (path, root) => {
|
|
|
1391
1531
|
const controls = focusableControls(root ?? document);
|
|
1392
1532
|
return path === "" ? (root !== void 0 || document.querySelectorAll("form").length <= 1) && focusFirstOf(controls) : focusFirstOf(controls.filter((el) => nameMatchesAny(el, [path])));
|
|
1393
1533
|
};
|
|
1394
|
-
var EMPTY_ID_STATE = { items: [], ids: [], counter: 0 };
|
|
1395
|
-
var EMPTY_ITEMS = [];
|
|
1396
|
-
var reconcileIds = (prev, nextItems) => {
|
|
1397
|
-
if (prev.items === nextItems) return prev;
|
|
1398
|
-
if (prev.items.length === nextItems.length && prev.items.every((item, i) => Object.is(item, nextItems[i]))) {
|
|
1399
|
-
return prev;
|
|
1400
|
-
}
|
|
1401
|
-
const buckets = prev.items.reduce((acc, item, i) => {
|
|
1402
|
-
const bucket = acc.get(item);
|
|
1403
|
-
if (bucket === void 0) {
|
|
1404
|
-
acc.set(item, [i]);
|
|
1405
|
-
} else {
|
|
1406
|
-
bucket.push(i);
|
|
1407
|
-
}
|
|
1408
|
-
return acc;
|
|
1409
|
-
}, /* @__PURE__ */ new Map());
|
|
1410
|
-
const matchedIds = nextItems.map((item) => {
|
|
1411
|
-
const prevIndex = buckets.get(item)?.shift();
|
|
1412
|
-
return prevIndex === void 0 ? null : prev.ids[prevIndex] ?? null;
|
|
1413
|
-
});
|
|
1414
|
-
const leftover = [...buckets.values()].flat().sort((a, b) => a - b).map((i) => prev.ids[i]).filter((id) => id !== void 0);
|
|
1415
|
-
const ordinals = matchedIds.reduce((acc, id) => {
|
|
1416
|
-
const count = acc.length === 0 ? 0 : acc[acc.length - 1] ?? 0;
|
|
1417
|
-
acc.push(id === null ? count + 1 : count);
|
|
1418
|
-
return acc;
|
|
1419
|
-
}, []);
|
|
1420
|
-
const unmatched = ordinals.length === 0 ? 0 : ordinals[ordinals.length - 1] ?? 0;
|
|
1421
|
-
const ids = matchedIds.map((id, i) => {
|
|
1422
|
-
if (id !== null) return id;
|
|
1423
|
-
const ordinal = ordinals[i] ?? 0;
|
|
1424
|
-
return leftover[ordinal - 1] ?? `__zfa_${prev.counter + (ordinal - leftover.length)}`;
|
|
1425
|
-
});
|
|
1426
|
-
return {
|
|
1427
|
-
items: nextItems,
|
|
1428
|
-
ids,
|
|
1429
|
-
counter: prev.counter + Math.max(0, unmatched - leftover.length)
|
|
1430
|
-
};
|
|
1431
|
-
};
|
|
1432
|
-
function useFieldArray(form, pathArg) {
|
|
1433
|
-
const path = react$1.useStore(
|
|
1434
|
-
form.store,
|
|
1435
|
-
(state) => typeof pathArg === "function" ? pathArg(state) : pathArg
|
|
1436
|
-
);
|
|
1437
|
-
const slice = react$1.useStore(
|
|
1438
|
-
form.store,
|
|
1439
|
-
shallow.useShallow((state) => {
|
|
1440
|
-
const value = getAtPath(state.values, path);
|
|
1441
|
-
return {
|
|
1442
|
-
items: Array.isArray(value) ? value : EMPTY_ITEMS,
|
|
1443
|
-
error: state.errors[path]
|
|
1444
|
-
};
|
|
1445
|
-
})
|
|
1446
|
-
);
|
|
1447
|
-
const items = slice.items;
|
|
1448
|
-
const error = slice.error;
|
|
1449
|
-
const [idEntry, setIdEntry] = react.useState({ form, path, state: EMPTY_ID_STATE });
|
|
1450
|
-
const base = idEntry.form === form && idEntry.path === path ? idEntry.state : { ...EMPTY_ID_STATE, counter: idEntry.state.counter };
|
|
1451
|
-
const nextIdState = reconcileIds(base, items);
|
|
1452
|
-
if (idEntry.form !== form || idEntry.path !== path || idEntry.state !== nextIdState) {
|
|
1453
|
-
setIdEntry({ form, path, state: nextIdState });
|
|
1454
|
-
}
|
|
1455
|
-
const ids = nextIdState.ids;
|
|
1456
|
-
const push = react.useCallback(
|
|
1457
|
-
(item) => form.arrayPush(path, item),
|
|
1458
|
-
[form, path]
|
|
1459
|
-
);
|
|
1460
|
-
const remove = react.useCallback(
|
|
1461
|
-
(index) => form.arrayRemove(path, index),
|
|
1462
|
-
[form, path]
|
|
1463
|
-
);
|
|
1464
|
-
const insert = react.useCallback(
|
|
1465
|
-
(index, item) => form.arrayInsert(path, index, item),
|
|
1466
|
-
[form, path]
|
|
1467
|
-
);
|
|
1468
|
-
const move = react.useCallback(
|
|
1469
|
-
(from, to) => form.arrayMove(path, from, to),
|
|
1470
|
-
[form, path]
|
|
1471
|
-
);
|
|
1472
|
-
const swap = react.useCallback(
|
|
1473
|
-
(a, b) => form.arraySwap(path, a, b),
|
|
1474
|
-
[form, path]
|
|
1475
|
-
);
|
|
1476
|
-
const fields = react.useMemo(
|
|
1477
|
-
() => items.map((value, index) => ({
|
|
1478
|
-
id: ids[index] ?? `__zfa_fallback_${index}`,
|
|
1479
|
-
value
|
|
1480
|
-
})),
|
|
1481
|
-
[items, ids]
|
|
1482
|
-
);
|
|
1483
|
-
return react.useMemo(
|
|
1484
|
-
() => ({
|
|
1485
|
-
fields,
|
|
1486
|
-
items,
|
|
1487
|
-
length: items.length,
|
|
1488
|
-
error,
|
|
1489
|
-
push,
|
|
1490
|
-
remove,
|
|
1491
|
-
insert,
|
|
1492
|
-
move,
|
|
1493
|
-
swap
|
|
1494
|
-
}),
|
|
1495
|
-
[fields, items, error, push, remove, insert, move, swap]
|
|
1496
|
-
);
|
|
1497
|
-
}
|
|
1498
1534
|
|
|
1499
1535
|
exports.CheckboxField = CheckboxField;
|
|
1500
1536
|
exports.NumberField = NumberField;
|
|
@@ -1503,6 +1539,7 @@ exports.TextField = TextField;
|
|
|
1503
1539
|
exports.checkboxProps = checkboxProps;
|
|
1504
1540
|
exports.createForm = createForm;
|
|
1505
1541
|
exports.createFormContext = createFormContext;
|
|
1542
|
+
exports.createFormHooks = createFormHooks;
|
|
1506
1543
|
exports.emptyValueForSchema = emptyValueForSchema;
|
|
1507
1544
|
exports.flattenIssues = flattenIssues;
|
|
1508
1545
|
exports.focusField = focusField;
|