formstand 0.18.0 → 0.20.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
@@ -1,9 +1,11 @@
1
- import { getAtPath, slotAtPath, setAtPath, isPlainObject, hasOwnSlot, valuesEqual, isFieldDirty, useFormSelector, useFormSelectorShallow, parsePath } from './chunk-N7WPXYCU.js';
1
+ import { getAtPath, slotAtPath, setAtPath, isPlainObject, hasOwnSlot, valuesEqual, useFormSelectorShallow, isFieldDirty, useFormSelector, parsePath } from './chunk-N7WPXYCU.js';
2
2
  export { getAtPath, parsePath, setAtPath, useFormSelector, useFormSelectorShallow } from './chunk-N7WPXYCU.js';
3
+ import { shouldValidateOn } from './chunk-MQ6262S3.js';
4
+ export { shouldValidateOn } from './chunk-MQ6262S3.js';
3
5
  import { createStore } from 'zustand/vanilla';
4
6
  import { devtools } from 'zustand/middleware';
5
7
  import { z } from 'zod';
6
- import { useRef, useEffect, useMemo, createContext, useCallback, useState, useContext, useId } from 'react';
8
+ import { useRef, useEffect, useMemo, useState, useCallback, useActionState, createContext, useContext, useId } from 'react';
7
9
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
8
10
  import { useStore } from 'zustand/react';
9
11
  import { useShallow } from 'zustand/react/shallow';
@@ -1008,23 +1010,6 @@ var createForm = (schema, options) => {
1008
1010
  });
1009
1011
  };
1010
1012
 
1011
- // src/core/mode.ts
1012
- var shouldValidateOn = (trigger, mode, reValidateMode, submitAttempted, touched = false) => {
1013
- const effective = submitAttempted ? reValidateMode : mode;
1014
- switch (effective) {
1015
- case "onChange":
1016
- return trigger === "change";
1017
- case "onBlur":
1018
- return trigger === "blur";
1019
- case "onSubmit":
1020
- return false;
1021
- case "onTouched":
1022
- return trigger === "blur" || touched;
1023
- case "all":
1024
- return true;
1025
- }
1026
- };
1027
-
1028
1013
  // src/core/persist.ts
1029
1014
  var kindOf = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
1030
1015
  var conflictsWith = (draft, reference) => {
@@ -1183,6 +1168,278 @@ var useForm = (schema, options) => {
1183
1168
  }, [form, schema, options.initialValues]);
1184
1169
  return form;
1185
1170
  };
1171
+
1172
+ // src/react/inputProps.ts
1173
+ var hasFieldError = (error) => error !== void 0 && error.length > 0;
1174
+ var ariaInvalid = (field) => hasFieldError(field.error) ? true : void 0;
1175
+ var numberToInputText = (value) => value === void 0 || value === null || Number.isNaN(value) ? "" : String(value);
1176
+ var parseNumberText = (text) => {
1177
+ if (text.trim() === "") return { kind: "empty" };
1178
+ const parsed = Number(text);
1179
+ return Number.isFinite(parsed) ? { kind: "number", value: parsed } : { kind: "invalid" };
1180
+ };
1181
+ var textInputProps = (field) => ({
1182
+ name: field.path,
1183
+ value: field.value ?? "",
1184
+ "aria-invalid": ariaInvalid(field),
1185
+ onChange: (e) => {
1186
+ const text = e.target.value;
1187
+ field.setValue(text === "" && field.clearable ? field.emptyValue : text);
1188
+ },
1189
+ onBlur: field.onBlur
1190
+ });
1191
+ var numberInputProps = (field) => ({
1192
+ type: "number",
1193
+ name: field.path,
1194
+ value: numberToInputText(field.value),
1195
+ "aria-invalid": ariaInvalid(field),
1196
+ onChange: (e) => {
1197
+ const parsed = parseNumberText(e.target.value);
1198
+ if (parsed.kind === "invalid") return;
1199
+ field.setValue(
1200
+ parsed.kind === "number" ? parsed.value : field.emptyValue
1201
+ );
1202
+ },
1203
+ onBlur: field.onBlur
1204
+ });
1205
+ var dateToInputText = (value) => {
1206
+ if (value === void 0 || value === null || Number.isNaN(value.getTime())) {
1207
+ return "";
1208
+ }
1209
+ const pad = (part) => String(part).padStart(2, "0");
1210
+ return `${String(value.getFullYear()).padStart(4, "0")}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`;
1211
+ };
1212
+ var parseDateText = (text) => {
1213
+ if (text.trim() === "") return { kind: "empty" };
1214
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
1215
+ if (match === null) return { kind: "invalid" };
1216
+ const [, year, month, day] = match;
1217
+ const y = Number(year);
1218
+ const date = new Date(y, Number(month) - 1, Number(day));
1219
+ if (y < 100) date.setFullYear(y);
1220
+ return date.getFullYear() === y && date.getMonth() === Number(month) - 1 && date.getDate() === Number(day) ? { kind: "date", value: date } : { kind: "invalid" };
1221
+ };
1222
+ var dateInputProps = (field) => ({
1223
+ type: "date",
1224
+ name: field.path,
1225
+ value: dateToInputText(field.value),
1226
+ "aria-invalid": ariaInvalid(field),
1227
+ onChange: (e) => {
1228
+ const parsed = parseDateText(e.target.value);
1229
+ if (parsed.kind !== "date") {
1230
+ field.setValue(field.emptyValue);
1231
+ return;
1232
+ }
1233
+ const current = field.value;
1234
+ const next = current instanceof Date && !Number.isNaN(current.getTime()) ? new Date(
1235
+ parsed.value.getFullYear(),
1236
+ parsed.value.getMonth(),
1237
+ parsed.value.getDate(),
1238
+ current.getHours(),
1239
+ current.getMinutes(),
1240
+ current.getSeconds(),
1241
+ current.getMilliseconds()
1242
+ ) : parsed.value;
1243
+ field.setValue(next);
1244
+ },
1245
+ onBlur: field.onBlur
1246
+ });
1247
+ var checkboxProps = (field) => ({
1248
+ type: "checkbox",
1249
+ name: field.path,
1250
+ checked: field.value ?? false,
1251
+ "aria-invalid": ariaInvalid(field),
1252
+ onChange: (e) => field.setValue(e.target.checked),
1253
+ onBlur: field.onBlur
1254
+ });
1255
+ var selectProps = (field) => ({
1256
+ name: field.path,
1257
+ value: field.value ?? "",
1258
+ "aria-invalid": ariaInvalid(field),
1259
+ onChange: (e) => {
1260
+ const v = e.target.value;
1261
+ field.setValue(v === "" && field.clearable ? field.emptyValue : v);
1262
+ },
1263
+ onBlur: field.onBlur
1264
+ });
1265
+
1266
+ // src/react/useMaskedInput.ts
1267
+ var useMaskedInput = (field, options) => {
1268
+ const [edit, setEdit] = useState({
1269
+ raw: null,
1270
+ pushed: void 0
1271
+ });
1272
+ const externallyChanged = edit.raw !== null && !Object.is(field.value, edit.pushed);
1273
+ if (externallyChanged) {
1274
+ setEdit({ raw: null, pushed: void 0 });
1275
+ }
1276
+ const raw = externallyChanged ? null : edit.raw;
1277
+ return {
1278
+ name: field.path,
1279
+ value: raw ?? (field.value === void 0 || field.value === null ? "" : options.format(field.value)),
1280
+ "aria-invalid": hasFieldError(field.error) ? true : void 0,
1281
+ onChange: (e) => {
1282
+ const text = e.target.value;
1283
+ const parsed = options.parse(text);
1284
+ switch (parsed.kind) {
1285
+ case "value":
1286
+ setEdit({ raw: text, pushed: parsed.value });
1287
+ field.setValue(parsed.value);
1288
+ return;
1289
+ case "empty": {
1290
+ const empty = field.emptyValue;
1291
+ setEdit({ raw: text, pushed: empty });
1292
+ field.setValue(empty);
1293
+ return;
1294
+ }
1295
+ case "invalid":
1296
+ setEdit({ raw: text, pushed: field.value });
1297
+ return;
1298
+ }
1299
+ },
1300
+ onBlur: () => {
1301
+ setEdit({ raw: null, pushed: void 0 });
1302
+ field.onBlur();
1303
+ }
1304
+ };
1305
+ };
1306
+ var useFormSteps = (form, steps) => {
1307
+ const [step, setStep] = useState(0);
1308
+ const [visited, setVisited] = useState(
1309
+ () => /* @__PURE__ */ new Set([0])
1310
+ );
1311
+ const latest = useRef(steps);
1312
+ useEffect(() => {
1313
+ latest.current = steps;
1314
+ });
1315
+ const validateStep = useCallback(
1316
+ async (index) => {
1317
+ const definition = latest.current[index];
1318
+ if (definition === void 0) return true;
1319
+ const result = await form.validateFieldsAsync(definition.fields);
1320
+ if (result.kind === "invalid") {
1321
+ Object.keys(result.errors).forEach((key) => {
1322
+ form.setTouched(key, true);
1323
+ });
1324
+ return false;
1325
+ }
1326
+ return true;
1327
+ },
1328
+ [form]
1329
+ );
1330
+ const next = useCallback(async () => {
1331
+ const ok = await validateStep(step);
1332
+ if (!ok) return false;
1333
+ const target = Math.min(step + 1, latest.current.length - 1);
1334
+ setStep(target);
1335
+ setVisited((prev) => /* @__PURE__ */ new Set([...prev, target]));
1336
+ return true;
1337
+ }, [validateStep, step]);
1338
+ const back = useCallback(() => {
1339
+ setStep((prev) => Math.max(prev - 1, 0));
1340
+ }, []);
1341
+ const goTo = useCallback(
1342
+ async (target) => {
1343
+ const count = latest.current.length;
1344
+ const clamped = Math.max(0, Math.min(target, count - 1));
1345
+ if (clamped <= step) {
1346
+ setStep(clamped);
1347
+ return clamped;
1348
+ }
1349
+ const walk = async (from) => {
1350
+ if (from >= clamped) return clamped;
1351
+ return await validateStep(from) ? walk(from + 1) : from;
1352
+ };
1353
+ const landed = await walk(step);
1354
+ setStep(landed);
1355
+ setVisited(
1356
+ (prev) => /* @__PURE__ */ new Set([
1357
+ ...prev,
1358
+ ...Array.from({ length: landed - step + 1 }, (_, i) => step + i)
1359
+ ])
1360
+ );
1361
+ return landed;
1362
+ },
1363
+ [validateStep, step]
1364
+ );
1365
+ const errorFlags = useFormSelectorShallow(
1366
+ form,
1367
+ (state) => steps.map(
1368
+ (definition) => definition.fields.some(
1369
+ (path) => Object.keys(state.errors).some((key) => isPathOrChild(key, path))
1370
+ )
1371
+ )
1372
+ );
1373
+ const statuses = useMemo(
1374
+ () => steps.map(
1375
+ (definition, index) => ({
1376
+ name: definition.name,
1377
+ visited: visited.has(index),
1378
+ hasErrors: errorFlags[index] ?? false
1379
+ })
1380
+ ),
1381
+ [steps, visited, errorFlags]
1382
+ );
1383
+ return useMemo(
1384
+ () => ({
1385
+ step,
1386
+ name: steps[step]?.name,
1387
+ count: steps.length,
1388
+ isFirst: step === 0,
1389
+ isLast: step === steps.length - 1,
1390
+ next,
1391
+ back,
1392
+ goTo,
1393
+ steps: statuses
1394
+ }),
1395
+ [step, steps, next, back, goTo, statuses]
1396
+ );
1397
+ };
1398
+ var warnUnobserved = (result, options, site) => {
1399
+ if (result.kind === "error" && options?.onError === void 0 && process.env.NODE_ENV !== "production") {
1400
+ console.warn(
1401
+ `[formstand] action handler threw and nothing observed it \u2014 the form shows no feedback. Pass { onError } to ${site} (or setError inside your handler):`,
1402
+ result.error
1403
+ );
1404
+ }
1405
+ };
1406
+ var useFormAction = (form, onValid, options) => {
1407
+ const latest = useRef({ onValid, options });
1408
+ useEffect(() => {
1409
+ latest.current = { onValid, options };
1410
+ });
1411
+ return useCallback(
1412
+ async (formData) => {
1413
+ const { onValid: handler, options: opts } = latest.current;
1414
+ const result = await form.submit(
1415
+ (data) => handler(data, formData),
1416
+ opts?.onInvalid,
1417
+ opts?.onError === void 0 ? void 0 : { onError: opts.onError }
1418
+ );
1419
+ warnUnobserved(result, opts, "useFormAction");
1420
+ },
1421
+ [form]
1422
+ );
1423
+ };
1424
+ var useFormActionState = (form, action, initialState, options) => {
1425
+ const [state, formAction, isPending] = useActionState(
1426
+ async (prevState, formData) => {
1427
+ const prev = prevState;
1428
+ const nextRef = { current: prev };
1429
+ const result = await form.submit(
1430
+ async (data) => {
1431
+ nextRef.current = await action(prev, data, formData);
1432
+ },
1433
+ options?.onInvalid,
1434
+ options?.onError === void 0 ? void 0 : { onError: options.onError }
1435
+ );
1436
+ warnUnobserved(result, options, "useFormActionState");
1437
+ return nextRef.current;
1438
+ },
1439
+ initialState
1440
+ );
1441
+ return [state, formAction, isPending];
1442
+ };
1186
1443
  var createFormContext = () => {
1187
1444
  const Context = createContext(null);
1188
1445
  const Provider = ({ form, children }) => /* @__PURE__ */ jsx(Context.Provider, { value: form, children });
@@ -1631,6 +1888,7 @@ var createFormHooks = (form, name) => {
1631
1888
  paths
1632
1889
  );
1633
1890
  const useBoundSubmitCount = () => useSubmitCount(structural);
1891
+ const useBoundSteps = (steps) => useFormSteps(form, steps);
1634
1892
  const prefix = capitalize(name ?? "");
1635
1893
  return {
1636
1894
  [`use${prefix}Field`]: useBoundField,
@@ -1645,103 +1903,10 @@ var createFormHooks = (form, name) => {
1645
1903
  [`use${prefix}IsSubmitting`]: useBoundIsSubmitting,
1646
1904
  [`use${prefix}SubmitCount`]: useBoundSubmitCount,
1647
1905
  [`use${prefix}Values`]: useBoundValues,
1648
- [`use${prefix}Fields`]: useBoundFields
1906
+ [`use${prefix}Fields`]: useBoundFields,
1907
+ [`use${prefix}Steps`]: useBoundSteps
1649
1908
  };
1650
1909
  };
1651
-
1652
- // src/react/inputProps.ts
1653
- var hasFieldError = (error) => error !== void 0 && error.length > 0;
1654
- var ariaInvalid = (field) => hasFieldError(field.error) ? true : void 0;
1655
- var numberToInputText = (value) => value === void 0 || value === null || Number.isNaN(value) ? "" : String(value);
1656
- var parseNumberText = (text) => {
1657
- if (text.trim() === "") return { kind: "empty" };
1658
- const parsed = Number(text);
1659
- return Number.isFinite(parsed) ? { kind: "number", value: parsed } : { kind: "invalid" };
1660
- };
1661
- var textInputProps = (field) => ({
1662
- name: field.path,
1663
- value: field.value ?? "",
1664
- "aria-invalid": ariaInvalid(field),
1665
- onChange: (e) => {
1666
- const text = e.target.value;
1667
- field.setValue(text === "" && field.clearable ? field.emptyValue : text);
1668
- },
1669
- onBlur: field.onBlur
1670
- });
1671
- var numberInputProps = (field) => ({
1672
- type: "number",
1673
- name: field.path,
1674
- value: numberToInputText(field.value),
1675
- "aria-invalid": ariaInvalid(field),
1676
- onChange: (e) => {
1677
- const parsed = parseNumberText(e.target.value);
1678
- if (parsed.kind === "invalid") return;
1679
- field.setValue(
1680
- parsed.kind === "number" ? parsed.value : field.emptyValue
1681
- );
1682
- },
1683
- onBlur: field.onBlur
1684
- });
1685
- var dateToInputText = (value) => {
1686
- if (value === void 0 || value === null || Number.isNaN(value.getTime())) {
1687
- return "";
1688
- }
1689
- const pad = (part) => String(part).padStart(2, "0");
1690
- return `${String(value.getFullYear()).padStart(4, "0")}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`;
1691
- };
1692
- var parseDateText = (text) => {
1693
- if (text.trim() === "") return { kind: "empty" };
1694
- const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
1695
- if (match === null) return { kind: "invalid" };
1696
- const [, year, month, day] = match;
1697
- const y = Number(year);
1698
- const date = new Date(y, Number(month) - 1, Number(day));
1699
- if (y < 100) date.setFullYear(y);
1700
- return date.getFullYear() === y && date.getMonth() === Number(month) - 1 && date.getDate() === Number(day) ? { kind: "date", value: date } : { kind: "invalid" };
1701
- };
1702
- var dateInputProps = (field) => ({
1703
- type: "date",
1704
- name: field.path,
1705
- value: dateToInputText(field.value),
1706
- "aria-invalid": ariaInvalid(field),
1707
- onChange: (e) => {
1708
- const parsed = parseDateText(e.target.value);
1709
- if (parsed.kind !== "date") {
1710
- field.setValue(field.emptyValue);
1711
- return;
1712
- }
1713
- const current = field.value;
1714
- const next = current instanceof Date && !Number.isNaN(current.getTime()) ? new Date(
1715
- parsed.value.getFullYear(),
1716
- parsed.value.getMonth(),
1717
- parsed.value.getDate(),
1718
- current.getHours(),
1719
- current.getMinutes(),
1720
- current.getSeconds(),
1721
- current.getMilliseconds()
1722
- ) : parsed.value;
1723
- field.setValue(next);
1724
- },
1725
- onBlur: field.onBlur
1726
- });
1727
- var checkboxProps = (field) => ({
1728
- type: "checkbox",
1729
- name: field.path,
1730
- checked: field.value ?? false,
1731
- "aria-invalid": ariaInvalid(field),
1732
- onChange: (e) => field.setValue(e.target.checked),
1733
- onBlur: field.onBlur
1734
- });
1735
- var selectProps = (field) => ({
1736
- name: field.path,
1737
- value: field.value ?? "",
1738
- "aria-invalid": ariaInvalid(field),
1739
- onChange: (e) => {
1740
- const v = e.target.value;
1741
- field.setValue(v === "" && field.clearable ? field.emptyValue : v);
1742
- },
1743
- onBlur: field.onBlur
1744
- });
1745
1910
  var useFieldA11y = (error) => {
1746
1911
  const id = useId();
1747
1912
  const errorId = `${id}-error`;
@@ -2034,6 +2199,6 @@ var focusField = (path, root) => {
2034
2199
  return path === "" ? (root !== void 0 || document.querySelectorAll("form").length <= 1) && focusFirstOf(namedControls(scope).filter(isFocusCandidate)) : focusFirstOf(pathCandidates(scope, [path]).filter(isFocusCandidate));
2035
2200
  };
2036
2201
 
2037
- export { CheckboxField, DateField, NumberField, SelectField, TextField, checkboxProps, createForm, createFormContext, createFormHooks, dateInputProps, dateToInputText, emptyValueForSchema, flattenIssues, focusField, focusFirstError, hasFieldError, isAsyncRequiredError, isClearableSchema, numberInputProps, numberToInputText, parseDateText, parseNumberText, persistForm, selectProps, shouldValidateOn, textInputProps, useField, useFieldArray, useFields, useForm, useFormError, useFormValues, useIsDirty, useIsSubmitting, useIsValid, useIsValidating, useNumberInput, usePersistForm, useSubmitCount, useVariantField, validateAsync, validateSync };
2202
+ export { CheckboxField, DateField, NumberField, SelectField, TextField, checkboxProps, createForm, createFormContext, createFormHooks, dateInputProps, dateToInputText, emptyValueForSchema, flattenIssues, focusField, focusFirstError, hasFieldError, isAsyncRequiredError, isClearableSchema, numberInputProps, numberToInputText, parseDateText, parseNumberText, persistForm, selectProps, textInputProps, useField, useFieldArray, useFields, useForm, useFormAction, useFormActionState, useFormError, useFormSteps, useFormValues, useIsDirty, useIsSubmitting, useIsValid, useIsValidating, useMaskedInput, useNumberInput, usePersistForm, useSubmitCount, useVariantField, validateAsync, validateSync };
2038
2203
  //# sourceMappingURL=index.js.map
2039
2204
  //# sourceMappingURL=index.js.map