formstand 0.15.2 → 0.17.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 +287 -0
- package/dist/{createForm-jlzK2lCG.d.cts → createForm-D0xLZH-Q.d.cts} +15 -5
- package/dist/{createForm-jlzK2lCG.d.ts → createForm-D0xLZH-Q.d.ts} +15 -5
- package/dist/devtools/index.d.cts +1 -1
- package/dist/devtools/index.d.ts +1 -1
- package/dist/index.cjs +127 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -6
- package/dist/index.d.ts +44 -6
- package/dist/index.js +126 -12
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -297,7 +297,7 @@ var schemaHasPath = (schema, path) => {
|
|
|
297
297
|
if (head === void 0) return true;
|
|
298
298
|
if (s instanceof zod.z.ZodObject) {
|
|
299
299
|
const shape = s.shape;
|
|
300
|
-
const child = shape[String(head)];
|
|
300
|
+
const child = Object.hasOwn(shape, String(head)) ? shape[String(head)] : void 0;
|
|
301
301
|
if (child !== void 0) return walk(child, rest, budget - 1);
|
|
302
302
|
const catchall = s.def.catchall;
|
|
303
303
|
return catchall !== void 0 && !(catchall instanceof zod.z.ZodNever);
|
|
@@ -317,7 +317,7 @@ var fieldSchemaAtPath = (schema, path) => parsePath(path).reduce((current, segme
|
|
|
317
317
|
if (current === null || hasChecks(current)) return null;
|
|
318
318
|
if (current instanceof zod.z.ZodObject && typeof segment === "string") {
|
|
319
319
|
const shape = current.shape;
|
|
320
|
-
return shape[segment] ?? null;
|
|
320
|
+
return Object.hasOwn(shape, segment) ? shape[segment] ?? null : null;
|
|
321
321
|
}
|
|
322
322
|
if (current instanceof zod.z.ZodArray && typeof segment === "number") {
|
|
323
323
|
return current.element;
|
|
@@ -722,7 +722,7 @@ var createForm = (schema, options) => {
|
|
|
722
722
|
};
|
|
723
723
|
const applyArrayOp = (path, nextArray, mapper, indexInBounds) => {
|
|
724
724
|
const current = getAtPath(store.getState().values, path);
|
|
725
|
-
if (current !== void 0 && !Array.isArray(current)) {
|
|
725
|
+
if (current !== void 0 && current !== null && !Array.isArray(current)) {
|
|
726
726
|
console.warn(
|
|
727
727
|
`[formstand] array op on "${path}" but the value at that path is not an array (got ${typeof current}). Operation skipped.`
|
|
728
728
|
);
|
|
@@ -831,6 +831,7 @@ var createForm = (schema, options) => {
|
|
|
831
831
|
try {
|
|
832
832
|
await onValid(result.data);
|
|
833
833
|
} catch (error) {
|
|
834
|
+
submitOptions?.onError?.(error);
|
|
834
835
|
return { kind: "error", error };
|
|
835
836
|
}
|
|
836
837
|
return { kind: "valid", data: result.data };
|
|
@@ -938,6 +939,17 @@ var createForm = (schema, options) => {
|
|
|
938
939
|
...state,
|
|
939
940
|
...errorChannels(state, { serverErrors: errors })
|
|
940
941
|
})),
|
|
942
|
+
addErrors: (errors) => store.setState((state) => ({
|
|
943
|
+
...state,
|
|
944
|
+
...errorChannels(state, {
|
|
945
|
+
serverErrors: Object.entries(errors).reduce(
|
|
946
|
+
// Computed keys throughout, so a server field named
|
|
947
|
+
// "__proto__" lands as an own property like everywhere else.
|
|
948
|
+
(acc, [key, list]) => list === void 0 || list.length === 0 ? omitKey(acc, key) : { ...acc, [key]: list },
|
|
949
|
+
state.serverErrors
|
|
950
|
+
)
|
|
951
|
+
})
|
|
952
|
+
})),
|
|
941
953
|
clearErrors: (path) => store.setState((state) => {
|
|
942
954
|
const scrub = (map) => path === void 0 ? emptyErrors : omitScope(map, [path]);
|
|
943
955
|
return {
|
|
@@ -1077,7 +1089,14 @@ var createForm = (schema, options) => {
|
|
|
1077
1089
|
),
|
|
1078
1090
|
handleSubmit: (onValid, onInvalid, opts) => async (event) => {
|
|
1079
1091
|
event?.preventDefault();
|
|
1080
|
-
|
|
1092
|
+
const result = await submit(onValid, onInvalid, opts);
|
|
1093
|
+
if (result.kind === "error" && opts?.onError === void 0 && process.env.NODE_ENV !== "production") {
|
|
1094
|
+
console.warn(
|
|
1095
|
+
"[formstand] submit handler threw and nothing observed it \u2014 the form shows no feedback. Pass { onError } to handleSubmit (or setError inside your handler):",
|
|
1096
|
+
result.error
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
return result;
|
|
1081
1100
|
},
|
|
1082
1101
|
diff: () => {
|
|
1083
1102
|
const state = store.getState();
|
|
@@ -1185,6 +1204,9 @@ var persistForm = (form, options) => {
|
|
|
1185
1204
|
const parsed = JSON.parse(raw);
|
|
1186
1205
|
const versioned = options.version !== void 0 && typeof parsed === "object" && parsed !== null && "__v" in parsed;
|
|
1187
1206
|
if (options.version !== void 0 && !versioned) return false;
|
|
1207
|
+
if (options.version === void 0 && typeof parsed === "object" && parsed !== null && Object.hasOwn(parsed, "__v") && Object.hasOwn(parsed, "values")) {
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
1188
1210
|
const wrapper = parsed;
|
|
1189
1211
|
if (versioned && wrapper.__v !== options.version) return false;
|
|
1190
1212
|
if (versioned && wrapper.values === void 0) return false;
|
|
@@ -1248,6 +1270,33 @@ var persistForm = (form, options) => {
|
|
|
1248
1270
|
}
|
|
1249
1271
|
});
|
|
1250
1272
|
};
|
|
1273
|
+
var usePersistForm = (form, options) => {
|
|
1274
|
+
const inner = react.useRef(null);
|
|
1275
|
+
const latestOptions = react.useRef(options);
|
|
1276
|
+
react.useEffect(() => {
|
|
1277
|
+
latestOptions.current = options;
|
|
1278
|
+
});
|
|
1279
|
+
const key = options.key;
|
|
1280
|
+
react.useEffect(() => {
|
|
1281
|
+
const handle = persistForm(form, latestOptions.current);
|
|
1282
|
+
inner.current = handle;
|
|
1283
|
+
return () => {
|
|
1284
|
+
inner.current = null;
|
|
1285
|
+
handle.dispose();
|
|
1286
|
+
};
|
|
1287
|
+
}, [form, key]);
|
|
1288
|
+
return react.useMemo(
|
|
1289
|
+
() => ({
|
|
1290
|
+
restore: () => inner.current?.restore() ?? false,
|
|
1291
|
+
clear: () => inner.current?.clear(),
|
|
1292
|
+
// dispose stops THIS component's watching early; the effect cleanup
|
|
1293
|
+
// still runs its own dispose harmlessly (persistForm's dispose is
|
|
1294
|
+
// idempotent by construction — it only cancels and unsubscribes).
|
|
1295
|
+
dispose: () => inner.current?.dispose()
|
|
1296
|
+
}),
|
|
1297
|
+
[]
|
|
1298
|
+
);
|
|
1299
|
+
};
|
|
1251
1300
|
var useForm = (schema, options) => {
|
|
1252
1301
|
const formRef = react.useRef(null);
|
|
1253
1302
|
if (formRef.current === null) {
|
|
@@ -1324,9 +1373,13 @@ function useField(form, pathArg, options) {
|
|
|
1324
1373
|
if (debounceTimerRef.current !== null) {
|
|
1325
1374
|
clearTimeout(debounceTimerRef.current);
|
|
1326
1375
|
}
|
|
1376
|
+
const scheduled = getAtPath(form.store.getState().values, path);
|
|
1327
1377
|
debounceTimerRef.current = setTimeout(() => {
|
|
1328
|
-
void form.validateFieldAsync(path);
|
|
1329
1378
|
debounceTimerRef.current = null;
|
|
1379
|
+
const state = form.store.getState();
|
|
1380
|
+
if (state.values === state.initialValues) return;
|
|
1381
|
+
if (!Object.is(getAtPath(state.values, path), scheduled)) return;
|
|
1382
|
+
void form.validateFieldAsync(path);
|
|
1330
1383
|
}, debounceMs);
|
|
1331
1384
|
return;
|
|
1332
1385
|
}
|
|
@@ -1390,6 +1443,8 @@ function useField(form, pathArg, options) {
|
|
|
1390
1443
|
initialValue: slice.initialValue,
|
|
1391
1444
|
emptyValue,
|
|
1392
1445
|
error: slice.error,
|
|
1446
|
+
// `?.[0]` covers both the undefined channel and an empty array.
|
|
1447
|
+
firstError: slice.error?.[0],
|
|
1393
1448
|
touched: slice.touched,
|
|
1394
1449
|
dirty: slice.dirty,
|
|
1395
1450
|
isValidating: slice.isValidating,
|
|
@@ -1585,25 +1640,64 @@ function useFieldArray(form, pathArg) {
|
|
|
1585
1640
|
})),
|
|
1586
1641
|
[items, ids]
|
|
1587
1642
|
);
|
|
1643
|
+
const setError = react.useCallback(
|
|
1644
|
+
// Same string-shorthand normalization as useField.setError, so custom
|
|
1645
|
+
// implementations typed for readonly string[] never receive a bare
|
|
1646
|
+
// string.
|
|
1647
|
+
(errors) => form.setError?.(path, typeof errors === "string" ? [errors] : errors),
|
|
1648
|
+
[form, path]
|
|
1649
|
+
);
|
|
1650
|
+
const clearError = react.useCallback(() => form.clearErrors?.(path), [form, path]);
|
|
1588
1651
|
return react.useMemo(
|
|
1589
1652
|
() => ({
|
|
1653
|
+
path,
|
|
1590
1654
|
fields,
|
|
1591
1655
|
items,
|
|
1592
1656
|
length: items.length,
|
|
1593
1657
|
error,
|
|
1658
|
+
firstError: error?.[0],
|
|
1659
|
+
setError,
|
|
1660
|
+
clearError,
|
|
1594
1661
|
push,
|
|
1595
1662
|
remove,
|
|
1596
1663
|
insert,
|
|
1597
1664
|
move,
|
|
1598
1665
|
swap
|
|
1599
1666
|
}),
|
|
1600
|
-
[fields, items, error, push, remove, insert, move, swap]
|
|
1667
|
+
[path, fields, items, error, setError, clearError, push, remove, insert, move, swap]
|
|
1601
1668
|
);
|
|
1602
1669
|
}
|
|
1603
1670
|
var useFormError = (form) => react$1.useStore(
|
|
1604
1671
|
form.store,
|
|
1605
1672
|
shallow.useShallow((state) => state.errors[""])
|
|
1606
1673
|
);
|
|
1674
|
+
function useFormValues(form) {
|
|
1675
|
+
return react$1.useStore(form.store, (state) => state.values);
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
// src/react/useFields.ts
|
|
1679
|
+
function useFields(form, paths) {
|
|
1680
|
+
const fields = paths.map(
|
|
1681
|
+
(path) => (
|
|
1682
|
+
// 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
|
|
1683
|
+
useField(form, path)
|
|
1684
|
+
)
|
|
1685
|
+
);
|
|
1686
|
+
const merged = [...new Set(fields.flatMap((field) => field.error ?? []))];
|
|
1687
|
+
return {
|
|
1688
|
+
fields,
|
|
1689
|
+
error: merged.length > 0 ? merged : void 0,
|
|
1690
|
+
firstError: merged[0],
|
|
1691
|
+
touched: fields.some((field) => field.touched),
|
|
1692
|
+
dirty: fields.some((field) => field.dirty),
|
|
1693
|
+
isValidating: fields.some((field) => field.isValidating),
|
|
1694
|
+
onBlur: () => {
|
|
1695
|
+
fields.forEach((field) => {
|
|
1696
|
+
field.onBlur();
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1607
1701
|
function useIsDirty(form, path) {
|
|
1608
1702
|
return react$1.useStore(
|
|
1609
1703
|
form.store,
|
|
@@ -1624,6 +1718,19 @@ function useIsValid(form, path) {
|
|
|
1624
1718
|
return true;
|
|
1625
1719
|
});
|
|
1626
1720
|
}
|
|
1721
|
+
function useIsValidating(form, path) {
|
|
1722
|
+
return react$1.useStore(form.store, (state) => {
|
|
1723
|
+
if (path === void 0 || path === "") {
|
|
1724
|
+
if (state.isValidatingForm) return true;
|
|
1725
|
+
}
|
|
1726
|
+
for (const k in state.isValidating) {
|
|
1727
|
+
if (state.isValidating[k] === true && (path === void 0 || path === "" || isPathOrChild(k, path))) {
|
|
1728
|
+
return true;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
return false;
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1627
1734
|
var useIsSubmitting = (form) => react$1.useStore(form.store, (state) => state.isSubmitting);
|
|
1628
1735
|
var useSubmitCount = (form) => react$1.useStore(form.store, (state) => state.submitCount);
|
|
1629
1736
|
function useFormSelector(form, selector) {
|
|
@@ -1649,7 +1756,13 @@ var createFormHooks = (form, name) => {
|
|
|
1649
1756
|
const useBoundError = () => useFormError(structural);
|
|
1650
1757
|
const useBoundIsDirty = (path) => useIsDirty(structural, path);
|
|
1651
1758
|
const useBoundIsValid = (path) => useIsValid(structural, path);
|
|
1759
|
+
const useBoundIsValidating = (path) => useIsValidating(structural, path);
|
|
1652
1760
|
const useBoundIsSubmitting = () => useIsSubmitting(structural);
|
|
1761
|
+
const useBoundValues = () => useFormValues(structural);
|
|
1762
|
+
const useBoundFields = (paths) => useFields(
|
|
1763
|
+
structural,
|
|
1764
|
+
paths
|
|
1765
|
+
);
|
|
1653
1766
|
const useBoundSubmitCount = () => useSubmitCount(structural);
|
|
1654
1767
|
const prefix = capitalize(name ?? "");
|
|
1655
1768
|
return {
|
|
@@ -1661,13 +1774,13 @@ var createFormHooks = (form, name) => {
|
|
|
1661
1774
|
[`use${prefix}Error`]: useBoundError,
|
|
1662
1775
|
[`use${prefix}IsDirty`]: useBoundIsDirty,
|
|
1663
1776
|
[`use${prefix}IsValid`]: useBoundIsValid,
|
|
1777
|
+
[`use${prefix}IsValidating`]: useBoundIsValidating,
|
|
1664
1778
|
[`use${prefix}IsSubmitting`]: useBoundIsSubmitting,
|
|
1665
|
-
[`use${prefix}SubmitCount`]: useBoundSubmitCount
|
|
1779
|
+
[`use${prefix}SubmitCount`]: useBoundSubmitCount,
|
|
1780
|
+
[`use${prefix}Values`]: useBoundValues,
|
|
1781
|
+
[`use${prefix}Fields`]: useBoundFields
|
|
1666
1782
|
};
|
|
1667
1783
|
};
|
|
1668
|
-
function useFormValues(form) {
|
|
1669
|
-
return react$1.useStore(form.store, (state) => state.values);
|
|
1670
|
-
}
|
|
1671
1784
|
|
|
1672
1785
|
// src/react/inputProps.ts
|
|
1673
1786
|
var hasFieldError = (error) => error !== void 0 && error.length > 0;
|
|
@@ -1697,6 +1810,7 @@ var numberInputProps = (field) => ({
|
|
|
1697
1810
|
"aria-invalid": ariaInvalid(field),
|
|
1698
1811
|
onChange: (e) => {
|
|
1699
1812
|
const parsed = parseNumberText(e.target.value);
|
|
1813
|
+
if (parsed.kind === "invalid") return;
|
|
1700
1814
|
field.setValue(
|
|
1701
1815
|
parsed.kind === "number" ? parsed.value : field.emptyValue
|
|
1702
1816
|
);
|
|
@@ -2087,6 +2201,7 @@ exports.shouldValidateOn = shouldValidateOn;
|
|
|
2087
2201
|
exports.textInputProps = textInputProps;
|
|
2088
2202
|
exports.useField = useField;
|
|
2089
2203
|
exports.useFieldArray = useFieldArray;
|
|
2204
|
+
exports.useFields = useFields;
|
|
2090
2205
|
exports.useForm = useForm;
|
|
2091
2206
|
exports.useFormError = useFormError;
|
|
2092
2207
|
exports.useFormSelector = useFormSelector;
|
|
@@ -2095,7 +2210,9 @@ exports.useFormValues = useFormValues;
|
|
|
2095
2210
|
exports.useIsDirty = useIsDirty;
|
|
2096
2211
|
exports.useIsSubmitting = useIsSubmitting;
|
|
2097
2212
|
exports.useIsValid = useIsValid;
|
|
2213
|
+
exports.useIsValidating = useIsValidating;
|
|
2098
2214
|
exports.useNumberInput = useNumberInput;
|
|
2215
|
+
exports.usePersistForm = usePersistForm;
|
|
2099
2216
|
exports.useSubmitCount = useSubmitCount;
|
|
2100
2217
|
exports.useVariantField = useVariantField;
|
|
2101
2218
|
exports.validateAsync = validateAsync;
|