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/CHANGELOG.md +83 -0
- package/dist/chunk-MQ6262S3.js +20 -0
- package/dist/chunk-MQ6262S3.js.map +1 -0
- package/dist/index.cjs +288 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +72 -17
- package/dist/index.d.ts +72 -17
- package/dist/index.js +280 -115
- package/dist/index.js.map +1 -1
- package/dist/testing/index.cjs +70 -0
- package/dist/testing/index.cjs.map +1 -0
- package/dist/testing/index.d.cts +11 -0
- package/dist/testing/index.d.ts +11 -0
- package/dist/testing/index.js +49 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.20.0 — 2026-09-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`useMaskedInput(field, { parse, format })`.** The raw-text editing
|
|
8
|
+
pattern behind `useNumberInput`, generalized to any parsed/formatted
|
|
9
|
+
value (phone, currency, locale formats): partial entries stay visible
|
|
10
|
+
as local text without touching the form, keystrokes that parse push
|
|
11
|
+
the REAL value immediately (the schema validates data, never display
|
|
12
|
+
text), `"empty"` writes the field's schema-aware `emptyValue`, blur
|
|
13
|
+
snaps the display to `format(value)`, and an external write wins over
|
|
14
|
+
local text. Recipes gains a phone-mask walkthrough.
|
|
15
|
+
- **`formstand/testing`: drive a form in tests the way the hooks do.**
|
|
16
|
+
`fillField` writes and validates under the CHANGE gate (no-op guard
|
|
17
|
+
included), `blurField` marks touched and validates under the BLUR
|
|
18
|
+
gate, `fillAndBlurField` is the edit-then-leave a user performs,
|
|
19
|
+
`fillFields` applies a fixture object through the full recipe, and
|
|
20
|
+
`submitForm` hands back the `SubmitResult` to switch on. Tests using
|
|
21
|
+
the imperative `form.setValue` were skipping the validation gates by
|
|
22
|
+
design; these helpers restate `useField`'s own recipes over the core
|
|
23
|
+
API, so a store-level test sees exactly what a rendered input would —
|
|
24
|
+
without mounting one. Framework-agnostic; new Testing forms guide.
|
|
25
|
+
- **Variant sub-paths: `useVariantField`'s field argument is a PATH.**
|
|
26
|
+
`useVariantField(form, "payment", "billing.zip")` reaches inside a
|
|
27
|
+
variant-only object and `"span.0"` binds a variant-only tuple
|
|
28
|
+
position — the last shape the typed paths could not carry. The field
|
|
29
|
+
offer is `VariantFieldPath` (exported): every member's `FieldPath`,
|
|
30
|
+
minus paths rooted at a common key (those bind with plain `useField`,
|
|
31
|
+
the discriminant included), riding the same leaf classification,
|
|
32
|
+
tuple positionality, and depth budget as everywhere else. The value
|
|
33
|
+
distributes over the members that resolve the path, widened with
|
|
34
|
+
`| undefined` as before; `VariantFieldValue` now derives through
|
|
35
|
+
`FieldValue`, and the runtime is unchanged (the joined path always
|
|
36
|
+
worked — only its type was unreachable).
|
|
37
|
+
|
|
38
|
+
- **`useFormSteps(form, steps)`: the multi-step story.** One form, one
|
|
39
|
+
schema, and per-step validation scopes: `next()` validates the current
|
|
40
|
+
step's fields (async refines included; the full schema parses, so
|
|
41
|
+
cross-field rules inside a step fire, with errors scoped to the step)
|
|
42
|
+
and advances only on clean, marking errored fields touched exactly
|
|
43
|
+
like a failed submit; `back()` never validates; a forward `goTo()`
|
|
44
|
+
validates every step on the way and lands on the first one that
|
|
45
|
+
fails, so clickable step headers cannot skip past an invalid step.
|
|
46
|
+
`steps` is index-aligned live status (`name`, `visited`, `hasErrors`)
|
|
47
|
+
for progress UIs. The step index is React state on purpose — it is
|
|
48
|
+
navigation, not form data. `createFormHooks` binds it as
|
|
49
|
+
`use{Name}Steps`, and the docs gain a Multi-step forms guide.
|
|
50
|
+
|
|
51
|
+
## 0.19.0 — 2026-09-17
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- **The React 19 form-actions bridge.** `useFormAction(form, onValid,
|
|
56
|
+
options?)` returns an action for `<form action={...}>` that validates
|
|
57
|
+
first and hands the handler parsed `z.output` data (plus the raw
|
|
58
|
+
`FormData` last, for file pickers and uncontrolled co-inputs) under
|
|
59
|
+
the full submit lifecycle — isSubmitting/submitCount, errors written
|
|
60
|
+
and marked touched on failure, `onInvalid`/`onError` mirroring
|
|
61
|
+
`handleSubmit`, the same dev warning when a failure resolves
|
|
62
|
+
unobserved, and a reference-stable identity. `useFormActionState(form,
|
|
63
|
+
action, initialState, options?)` is `useActionState` with
|
|
64
|
+
schema-validated data: the handler's `(prevState, data, formData)`
|
|
65
|
+
shape lets a `"use server"` function slot straight in, and invalid,
|
|
66
|
+
skipped, or throwing submissions keep the previous state, because
|
|
67
|
+
field errors already live in the form store. Client-side integration
|
|
68
|
+
by design: no-JS progressive enhancement bypasses any client wrapper,
|
|
69
|
+
so that path validates server-side with the same zod schema.
|
|
70
|
+
|
|
3
71
|
## 0.18.0 — 2026-09-17
|
|
4
72
|
|
|
5
73
|
### Changed
|
|
@@ -30,6 +98,21 @@
|
|
|
30
98
|
back to the null-initial heuristic. **`isClearableSchema(schema)`** is
|
|
31
99
|
the exported rule behind it, next to `emptyValueForSchema`.
|
|
32
100
|
|
|
101
|
+
## formstand-cli Unreleased
|
|
102
|
+
|
|
103
|
+
### Added
|
|
104
|
+
|
|
105
|
+
- **Containers inside union variants generate.** The generator's last
|
|
106
|
+
TODO class: an object or tuple nested in a variant flattens to its
|
|
107
|
+
scalar leaves, each bound as a dotted variant sub-path
|
|
108
|
+
(`useVariantField(form, path, "billing.zip")`, formstand 0.20+) — in
|
|
109
|
+
both layouts, every emission site (static unions, union rows, union
|
|
110
|
+
row-object fields, unions under nested arrays), with joined labels
|
|
111
|
+
("Billing Zip") and the kit number-props hoists riding along. Depth
|
|
112
|
+
is judged on the full bound path (a too-deep leaf keeps the depth
|
|
113
|
+
TODO), and an array or union inside a variant still degrades — their
|
|
114
|
+
bindings need hooks and row machinery of their own.
|
|
115
|
+
|
|
33
116
|
## formstand-cli 0.18.0 — 2026-09-17
|
|
34
117
|
|
|
35
118
|
### Changed
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/core/mode.ts
|
|
2
|
+
var shouldValidateOn = (trigger, mode, reValidateMode, submitAttempted, touched = false) => {
|
|
3
|
+
const effective = submitAttempted ? reValidateMode : mode;
|
|
4
|
+
switch (effective) {
|
|
5
|
+
case "onChange":
|
|
6
|
+
return trigger === "change";
|
|
7
|
+
case "onBlur":
|
|
8
|
+
return trigger === "blur";
|
|
9
|
+
case "onSubmit":
|
|
10
|
+
return false;
|
|
11
|
+
case "onTouched":
|
|
12
|
+
return trigger === "blur" || touched;
|
|
13
|
+
case "all":
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export { shouldValidateOn };
|
|
19
|
+
//# sourceMappingURL=chunk-MQ6262S3.js.map
|
|
20
|
+
//# sourceMappingURL=chunk-MQ6262S3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/mode.ts"],"names":[],"mappings":";AAWO,IAAM,mBAAmB,CAC9B,OAAA,EACA,MACA,cAAA,EACA,eAAA,EACA,UAAmB,KAAA,KACP;AACZ,EAAA,MAAM,SAAA,GAAY,kBAAkB,cAAA,GAAiB,IAAA;AACrD,EAAA,QAAQ,SAAA;AAAW,IACjB,KAAK,UAAA;AACH,MAAA,OAAO,OAAA,KAAY,QAAA;AAAA,IACrB,KAAK,QAAA;AACH,MAAA,OAAO,OAAA,KAAY,MAAA;AAAA,IACrB,KAAK,UAAA;AACH,MAAA,OAAO,KAAA;AAAA,IACT,KAAK,WAAA;AACH,MAAA,OAAO,YAAY,MAAA,IAAU,OAAA;AAAA,IAC/B,KAAK,KAAA;AACH,MAAA,OAAO,IAAA;AAAA;AAEb","file":"chunk-MQ6262S3.js","sourcesContent":["export type ValidationMode =\n | \"onChange\"\n | \"onBlur\"\n | \"onSubmit\"\n // Validate on blur always; on change only once the field has been touched.\n | \"onTouched\"\n // Validate on both change and blur.\n | \"all\";\n\nexport type ValidationTrigger = \"change\" | \"blur\";\n\nexport const shouldValidateOn = (\n trigger: ValidationTrigger,\n mode: ValidationMode,\n reValidateMode: ValidationMode,\n submitAttempted: boolean,\n touched: boolean = false,\n): boolean => {\n const effective = submitAttempted ? reValidateMode : mode;\n switch (effective) {\n case \"onChange\":\n return trigger === \"change\";\n case \"onBlur\":\n return trigger === \"blur\";\n case \"onSubmit\":\n return false;\n case \"onTouched\":\n return trigger === \"blur\" || touched;\n case \"all\":\n return true;\n }\n};\n"]}
|
package/dist/index.cjs
CHANGED
|
@@ -4,9 +4,9 @@ var vanilla = require('zustand/vanilla');
|
|
|
4
4
|
var middleware = require('zustand/middleware');
|
|
5
5
|
var zod = require('zod');
|
|
6
6
|
var react = require('react');
|
|
7
|
-
var jsxRuntime = require('react/jsx-runtime');
|
|
8
7
|
var react$1 = require('zustand/react');
|
|
9
8
|
var shallow = require('zustand/react/shallow');
|
|
9
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
10
10
|
|
|
11
11
|
// src/core/createForm.ts
|
|
12
12
|
|
|
@@ -1317,6 +1317,286 @@ var useForm = (schema, options) => {
|
|
|
1317
1317
|
}, [form, schema, options.initialValues]);
|
|
1318
1318
|
return form;
|
|
1319
1319
|
};
|
|
1320
|
+
|
|
1321
|
+
// src/react/inputProps.ts
|
|
1322
|
+
var hasFieldError = (error) => error !== void 0 && error.length > 0;
|
|
1323
|
+
var ariaInvalid = (field) => hasFieldError(field.error) ? true : void 0;
|
|
1324
|
+
var numberToInputText = (value) => value === void 0 || value === null || Number.isNaN(value) ? "" : String(value);
|
|
1325
|
+
var parseNumberText = (text) => {
|
|
1326
|
+
if (text.trim() === "") return { kind: "empty" };
|
|
1327
|
+
const parsed = Number(text);
|
|
1328
|
+
return Number.isFinite(parsed) ? { kind: "number", value: parsed } : { kind: "invalid" };
|
|
1329
|
+
};
|
|
1330
|
+
var textInputProps = (field) => ({
|
|
1331
|
+
name: field.path,
|
|
1332
|
+
value: field.value ?? "",
|
|
1333
|
+
"aria-invalid": ariaInvalid(field),
|
|
1334
|
+
onChange: (e) => {
|
|
1335
|
+
const text = e.target.value;
|
|
1336
|
+
field.setValue(text === "" && field.clearable ? field.emptyValue : text);
|
|
1337
|
+
},
|
|
1338
|
+
onBlur: field.onBlur
|
|
1339
|
+
});
|
|
1340
|
+
var numberInputProps = (field) => ({
|
|
1341
|
+
type: "number",
|
|
1342
|
+
name: field.path,
|
|
1343
|
+
value: numberToInputText(field.value),
|
|
1344
|
+
"aria-invalid": ariaInvalid(field),
|
|
1345
|
+
onChange: (e) => {
|
|
1346
|
+
const parsed = parseNumberText(e.target.value);
|
|
1347
|
+
if (parsed.kind === "invalid") return;
|
|
1348
|
+
field.setValue(
|
|
1349
|
+
parsed.kind === "number" ? parsed.value : field.emptyValue
|
|
1350
|
+
);
|
|
1351
|
+
},
|
|
1352
|
+
onBlur: field.onBlur
|
|
1353
|
+
});
|
|
1354
|
+
var dateToInputText = (value) => {
|
|
1355
|
+
if (value === void 0 || value === null || Number.isNaN(value.getTime())) {
|
|
1356
|
+
return "";
|
|
1357
|
+
}
|
|
1358
|
+
const pad = (part) => String(part).padStart(2, "0");
|
|
1359
|
+
return `${String(value.getFullYear()).padStart(4, "0")}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`;
|
|
1360
|
+
};
|
|
1361
|
+
var parseDateText = (text) => {
|
|
1362
|
+
if (text.trim() === "") return { kind: "empty" };
|
|
1363
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
|
|
1364
|
+
if (match === null) return { kind: "invalid" };
|
|
1365
|
+
const [, year, month, day] = match;
|
|
1366
|
+
const y = Number(year);
|
|
1367
|
+
const date = new Date(y, Number(month) - 1, Number(day));
|
|
1368
|
+
if (y < 100) date.setFullYear(y);
|
|
1369
|
+
return date.getFullYear() === y && date.getMonth() === Number(month) - 1 && date.getDate() === Number(day) ? { kind: "date", value: date } : { kind: "invalid" };
|
|
1370
|
+
};
|
|
1371
|
+
var dateInputProps = (field) => ({
|
|
1372
|
+
type: "date",
|
|
1373
|
+
name: field.path,
|
|
1374
|
+
value: dateToInputText(field.value),
|
|
1375
|
+
"aria-invalid": ariaInvalid(field),
|
|
1376
|
+
onChange: (e) => {
|
|
1377
|
+
const parsed = parseDateText(e.target.value);
|
|
1378
|
+
if (parsed.kind !== "date") {
|
|
1379
|
+
field.setValue(field.emptyValue);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
const current = field.value;
|
|
1383
|
+
const next = current instanceof Date && !Number.isNaN(current.getTime()) ? new Date(
|
|
1384
|
+
parsed.value.getFullYear(),
|
|
1385
|
+
parsed.value.getMonth(),
|
|
1386
|
+
parsed.value.getDate(),
|
|
1387
|
+
current.getHours(),
|
|
1388
|
+
current.getMinutes(),
|
|
1389
|
+
current.getSeconds(),
|
|
1390
|
+
current.getMilliseconds()
|
|
1391
|
+
) : parsed.value;
|
|
1392
|
+
field.setValue(next);
|
|
1393
|
+
},
|
|
1394
|
+
onBlur: field.onBlur
|
|
1395
|
+
});
|
|
1396
|
+
var checkboxProps = (field) => ({
|
|
1397
|
+
type: "checkbox",
|
|
1398
|
+
name: field.path,
|
|
1399
|
+
checked: field.value ?? false,
|
|
1400
|
+
"aria-invalid": ariaInvalid(field),
|
|
1401
|
+
onChange: (e) => field.setValue(e.target.checked),
|
|
1402
|
+
onBlur: field.onBlur
|
|
1403
|
+
});
|
|
1404
|
+
var selectProps = (field) => ({
|
|
1405
|
+
name: field.path,
|
|
1406
|
+
value: field.value ?? "",
|
|
1407
|
+
"aria-invalid": ariaInvalid(field),
|
|
1408
|
+
onChange: (e) => {
|
|
1409
|
+
const v = e.target.value;
|
|
1410
|
+
field.setValue(v === "" && field.clearable ? field.emptyValue : v);
|
|
1411
|
+
},
|
|
1412
|
+
onBlur: field.onBlur
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
// src/react/useMaskedInput.ts
|
|
1416
|
+
var useMaskedInput = (field, options) => {
|
|
1417
|
+
const [edit, setEdit] = react.useState({
|
|
1418
|
+
raw: null,
|
|
1419
|
+
pushed: void 0
|
|
1420
|
+
});
|
|
1421
|
+
const externallyChanged = edit.raw !== null && !Object.is(field.value, edit.pushed);
|
|
1422
|
+
if (externallyChanged) {
|
|
1423
|
+
setEdit({ raw: null, pushed: void 0 });
|
|
1424
|
+
}
|
|
1425
|
+
const raw = externallyChanged ? null : edit.raw;
|
|
1426
|
+
return {
|
|
1427
|
+
name: field.path,
|
|
1428
|
+
value: raw ?? (field.value === void 0 || field.value === null ? "" : options.format(field.value)),
|
|
1429
|
+
"aria-invalid": hasFieldError(field.error) ? true : void 0,
|
|
1430
|
+
onChange: (e) => {
|
|
1431
|
+
const text = e.target.value;
|
|
1432
|
+
const parsed = options.parse(text);
|
|
1433
|
+
switch (parsed.kind) {
|
|
1434
|
+
case "value":
|
|
1435
|
+
setEdit({ raw: text, pushed: parsed.value });
|
|
1436
|
+
field.setValue(parsed.value);
|
|
1437
|
+
return;
|
|
1438
|
+
case "empty": {
|
|
1439
|
+
const empty = field.emptyValue;
|
|
1440
|
+
setEdit({ raw: text, pushed: empty });
|
|
1441
|
+
field.setValue(empty);
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
case "invalid":
|
|
1445
|
+
setEdit({ raw: text, pushed: field.value });
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
},
|
|
1449
|
+
onBlur: () => {
|
|
1450
|
+
setEdit({ raw: null, pushed: void 0 });
|
|
1451
|
+
field.onBlur();
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
};
|
|
1455
|
+
function useFormSelector(form, selector) {
|
|
1456
|
+
return react$1.useStore(form.store, selector);
|
|
1457
|
+
}
|
|
1458
|
+
function useFormSelectorShallow(form, selector) {
|
|
1459
|
+
return react$1.useStore(form.store, shallow.useShallow(selector));
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// src/react/useFormSteps.ts
|
|
1463
|
+
var useFormSteps = (form, steps) => {
|
|
1464
|
+
const [step, setStep] = react.useState(0);
|
|
1465
|
+
const [visited, setVisited] = react.useState(
|
|
1466
|
+
() => /* @__PURE__ */ new Set([0])
|
|
1467
|
+
);
|
|
1468
|
+
const latest = react.useRef(steps);
|
|
1469
|
+
react.useEffect(() => {
|
|
1470
|
+
latest.current = steps;
|
|
1471
|
+
});
|
|
1472
|
+
const validateStep = react.useCallback(
|
|
1473
|
+
async (index) => {
|
|
1474
|
+
const definition = latest.current[index];
|
|
1475
|
+
if (definition === void 0) return true;
|
|
1476
|
+
const result = await form.validateFieldsAsync(definition.fields);
|
|
1477
|
+
if (result.kind === "invalid") {
|
|
1478
|
+
Object.keys(result.errors).forEach((key) => {
|
|
1479
|
+
form.setTouched(key, true);
|
|
1480
|
+
});
|
|
1481
|
+
return false;
|
|
1482
|
+
}
|
|
1483
|
+
return true;
|
|
1484
|
+
},
|
|
1485
|
+
[form]
|
|
1486
|
+
);
|
|
1487
|
+
const next = react.useCallback(async () => {
|
|
1488
|
+
const ok = await validateStep(step);
|
|
1489
|
+
if (!ok) return false;
|
|
1490
|
+
const target = Math.min(step + 1, latest.current.length - 1);
|
|
1491
|
+
setStep(target);
|
|
1492
|
+
setVisited((prev) => /* @__PURE__ */ new Set([...prev, target]));
|
|
1493
|
+
return true;
|
|
1494
|
+
}, [validateStep, step]);
|
|
1495
|
+
const back = react.useCallback(() => {
|
|
1496
|
+
setStep((prev) => Math.max(prev - 1, 0));
|
|
1497
|
+
}, []);
|
|
1498
|
+
const goTo = react.useCallback(
|
|
1499
|
+
async (target) => {
|
|
1500
|
+
const count = latest.current.length;
|
|
1501
|
+
const clamped = Math.max(0, Math.min(target, count - 1));
|
|
1502
|
+
if (clamped <= step) {
|
|
1503
|
+
setStep(clamped);
|
|
1504
|
+
return clamped;
|
|
1505
|
+
}
|
|
1506
|
+
const walk = async (from) => {
|
|
1507
|
+
if (from >= clamped) return clamped;
|
|
1508
|
+
return await validateStep(from) ? walk(from + 1) : from;
|
|
1509
|
+
};
|
|
1510
|
+
const landed = await walk(step);
|
|
1511
|
+
setStep(landed);
|
|
1512
|
+
setVisited(
|
|
1513
|
+
(prev) => /* @__PURE__ */ new Set([
|
|
1514
|
+
...prev,
|
|
1515
|
+
...Array.from({ length: landed - step + 1 }, (_, i) => step + i)
|
|
1516
|
+
])
|
|
1517
|
+
);
|
|
1518
|
+
return landed;
|
|
1519
|
+
},
|
|
1520
|
+
[validateStep, step]
|
|
1521
|
+
);
|
|
1522
|
+
const errorFlags = useFormSelectorShallow(
|
|
1523
|
+
form,
|
|
1524
|
+
(state) => steps.map(
|
|
1525
|
+
(definition) => definition.fields.some(
|
|
1526
|
+
(path) => Object.keys(state.errors).some((key) => isPathOrChild(key, path))
|
|
1527
|
+
)
|
|
1528
|
+
)
|
|
1529
|
+
);
|
|
1530
|
+
const statuses = react.useMemo(
|
|
1531
|
+
() => steps.map(
|
|
1532
|
+
(definition, index) => ({
|
|
1533
|
+
name: definition.name,
|
|
1534
|
+
visited: visited.has(index),
|
|
1535
|
+
hasErrors: errorFlags[index] ?? false
|
|
1536
|
+
})
|
|
1537
|
+
),
|
|
1538
|
+
[steps, visited, errorFlags]
|
|
1539
|
+
);
|
|
1540
|
+
return react.useMemo(
|
|
1541
|
+
() => ({
|
|
1542
|
+
step,
|
|
1543
|
+
name: steps[step]?.name,
|
|
1544
|
+
count: steps.length,
|
|
1545
|
+
isFirst: step === 0,
|
|
1546
|
+
isLast: step === steps.length - 1,
|
|
1547
|
+
next,
|
|
1548
|
+
back,
|
|
1549
|
+
goTo,
|
|
1550
|
+
steps: statuses
|
|
1551
|
+
}),
|
|
1552
|
+
[step, steps, next, back, goTo, statuses]
|
|
1553
|
+
);
|
|
1554
|
+
};
|
|
1555
|
+
var warnUnobserved = (result, options, site) => {
|
|
1556
|
+
if (result.kind === "error" && options?.onError === void 0 && process.env.NODE_ENV !== "production") {
|
|
1557
|
+
console.warn(
|
|
1558
|
+
`[formstand] action handler threw and nothing observed it \u2014 the form shows no feedback. Pass { onError } to ${site} (or setError inside your handler):`,
|
|
1559
|
+
result.error
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1563
|
+
var useFormAction = (form, onValid, options) => {
|
|
1564
|
+
const latest = react.useRef({ onValid, options });
|
|
1565
|
+
react.useEffect(() => {
|
|
1566
|
+
latest.current = { onValid, options };
|
|
1567
|
+
});
|
|
1568
|
+
return react.useCallback(
|
|
1569
|
+
async (formData) => {
|
|
1570
|
+
const { onValid: handler, options: opts } = latest.current;
|
|
1571
|
+
const result = await form.submit(
|
|
1572
|
+
(data) => handler(data, formData),
|
|
1573
|
+
opts?.onInvalid,
|
|
1574
|
+
opts?.onError === void 0 ? void 0 : { onError: opts.onError }
|
|
1575
|
+
);
|
|
1576
|
+
warnUnobserved(result, opts, "useFormAction");
|
|
1577
|
+
},
|
|
1578
|
+
[form]
|
|
1579
|
+
);
|
|
1580
|
+
};
|
|
1581
|
+
var useFormActionState = (form, action, initialState, options) => {
|
|
1582
|
+
const [state, formAction, isPending] = react.useActionState(
|
|
1583
|
+
async (prevState, formData) => {
|
|
1584
|
+
const prev = prevState;
|
|
1585
|
+
const nextRef = { current: prev };
|
|
1586
|
+
const result = await form.submit(
|
|
1587
|
+
async (data) => {
|
|
1588
|
+
nextRef.current = await action(prev, data, formData);
|
|
1589
|
+
},
|
|
1590
|
+
options?.onInvalid,
|
|
1591
|
+
options?.onError === void 0 ? void 0 : { onError: options.onError }
|
|
1592
|
+
);
|
|
1593
|
+
warnUnobserved(result, options, "useFormActionState");
|
|
1594
|
+
return nextRef.current;
|
|
1595
|
+
},
|
|
1596
|
+
initialState
|
|
1597
|
+
);
|
|
1598
|
+
return [state, formAction, isPending];
|
|
1599
|
+
};
|
|
1320
1600
|
var createFormContext = () => {
|
|
1321
1601
|
const Context = react.createContext(null);
|
|
1322
1602
|
const Provider = ({ form, children }) => /* @__PURE__ */ jsxRuntime.jsx(Context.Provider, { value: form, children });
|
|
@@ -1740,12 +2020,6 @@ function useIsValidating(form, path) {
|
|
|
1740
2020
|
}
|
|
1741
2021
|
var useIsSubmitting = (form) => react$1.useStore(form.store, (state) => state.isSubmitting);
|
|
1742
2022
|
var useSubmitCount = (form) => react$1.useStore(form.store, (state) => state.submitCount);
|
|
1743
|
-
function useFormSelector(form, selector) {
|
|
1744
|
-
return react$1.useStore(form.store, selector);
|
|
1745
|
-
}
|
|
1746
|
-
function useFormSelectorShallow(form, selector) {
|
|
1747
|
-
return react$1.useStore(form.store, shallow.useShallow(selector));
|
|
1748
|
-
}
|
|
1749
2023
|
|
|
1750
2024
|
// src/react/createFormHooks.ts
|
|
1751
2025
|
var capitalize = (name) => name.length === 0 ? name : name.charAt(0).toUpperCase() + name.slice(1);
|
|
@@ -1771,6 +2045,7 @@ var createFormHooks = (form, name) => {
|
|
|
1771
2045
|
paths
|
|
1772
2046
|
);
|
|
1773
2047
|
const useBoundSubmitCount = () => useSubmitCount(structural);
|
|
2048
|
+
const useBoundSteps = (steps) => useFormSteps(form, steps);
|
|
1774
2049
|
const prefix = capitalize(name ?? "");
|
|
1775
2050
|
return {
|
|
1776
2051
|
[`use${prefix}Field`]: useBoundField,
|
|
@@ -1785,103 +2060,10 @@ var createFormHooks = (form, name) => {
|
|
|
1785
2060
|
[`use${prefix}IsSubmitting`]: useBoundIsSubmitting,
|
|
1786
2061
|
[`use${prefix}SubmitCount`]: useBoundSubmitCount,
|
|
1787
2062
|
[`use${prefix}Values`]: useBoundValues,
|
|
1788
|
-
[`use${prefix}Fields`]: useBoundFields
|
|
2063
|
+
[`use${prefix}Fields`]: useBoundFields,
|
|
2064
|
+
[`use${prefix}Steps`]: useBoundSteps
|
|
1789
2065
|
};
|
|
1790
2066
|
};
|
|
1791
|
-
|
|
1792
|
-
// src/react/inputProps.ts
|
|
1793
|
-
var hasFieldError = (error) => error !== void 0 && error.length > 0;
|
|
1794
|
-
var ariaInvalid = (field) => hasFieldError(field.error) ? true : void 0;
|
|
1795
|
-
var numberToInputText = (value) => value === void 0 || value === null || Number.isNaN(value) ? "" : String(value);
|
|
1796
|
-
var parseNumberText = (text) => {
|
|
1797
|
-
if (text.trim() === "") return { kind: "empty" };
|
|
1798
|
-
const parsed = Number(text);
|
|
1799
|
-
return Number.isFinite(parsed) ? { kind: "number", value: parsed } : { kind: "invalid" };
|
|
1800
|
-
};
|
|
1801
|
-
var textInputProps = (field) => ({
|
|
1802
|
-
name: field.path,
|
|
1803
|
-
value: field.value ?? "",
|
|
1804
|
-
"aria-invalid": ariaInvalid(field),
|
|
1805
|
-
onChange: (e) => {
|
|
1806
|
-
const text = e.target.value;
|
|
1807
|
-
field.setValue(text === "" && field.clearable ? field.emptyValue : text);
|
|
1808
|
-
},
|
|
1809
|
-
onBlur: field.onBlur
|
|
1810
|
-
});
|
|
1811
|
-
var numberInputProps = (field) => ({
|
|
1812
|
-
type: "number",
|
|
1813
|
-
name: field.path,
|
|
1814
|
-
value: numberToInputText(field.value),
|
|
1815
|
-
"aria-invalid": ariaInvalid(field),
|
|
1816
|
-
onChange: (e) => {
|
|
1817
|
-
const parsed = parseNumberText(e.target.value);
|
|
1818
|
-
if (parsed.kind === "invalid") return;
|
|
1819
|
-
field.setValue(
|
|
1820
|
-
parsed.kind === "number" ? parsed.value : field.emptyValue
|
|
1821
|
-
);
|
|
1822
|
-
},
|
|
1823
|
-
onBlur: field.onBlur
|
|
1824
|
-
});
|
|
1825
|
-
var dateToInputText = (value) => {
|
|
1826
|
-
if (value === void 0 || value === null || Number.isNaN(value.getTime())) {
|
|
1827
|
-
return "";
|
|
1828
|
-
}
|
|
1829
|
-
const pad = (part) => String(part).padStart(2, "0");
|
|
1830
|
-
return `${String(value.getFullYear()).padStart(4, "0")}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`;
|
|
1831
|
-
};
|
|
1832
|
-
var parseDateText = (text) => {
|
|
1833
|
-
if (text.trim() === "") return { kind: "empty" };
|
|
1834
|
-
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
|
|
1835
|
-
if (match === null) return { kind: "invalid" };
|
|
1836
|
-
const [, year, month, day] = match;
|
|
1837
|
-
const y = Number(year);
|
|
1838
|
-
const date = new Date(y, Number(month) - 1, Number(day));
|
|
1839
|
-
if (y < 100) date.setFullYear(y);
|
|
1840
|
-
return date.getFullYear() === y && date.getMonth() === Number(month) - 1 && date.getDate() === Number(day) ? { kind: "date", value: date } : { kind: "invalid" };
|
|
1841
|
-
};
|
|
1842
|
-
var dateInputProps = (field) => ({
|
|
1843
|
-
type: "date",
|
|
1844
|
-
name: field.path,
|
|
1845
|
-
value: dateToInputText(field.value),
|
|
1846
|
-
"aria-invalid": ariaInvalid(field),
|
|
1847
|
-
onChange: (e) => {
|
|
1848
|
-
const parsed = parseDateText(e.target.value);
|
|
1849
|
-
if (parsed.kind !== "date") {
|
|
1850
|
-
field.setValue(field.emptyValue);
|
|
1851
|
-
return;
|
|
1852
|
-
}
|
|
1853
|
-
const current = field.value;
|
|
1854
|
-
const next = current instanceof Date && !Number.isNaN(current.getTime()) ? new Date(
|
|
1855
|
-
parsed.value.getFullYear(),
|
|
1856
|
-
parsed.value.getMonth(),
|
|
1857
|
-
parsed.value.getDate(),
|
|
1858
|
-
current.getHours(),
|
|
1859
|
-
current.getMinutes(),
|
|
1860
|
-
current.getSeconds(),
|
|
1861
|
-
current.getMilliseconds()
|
|
1862
|
-
) : parsed.value;
|
|
1863
|
-
field.setValue(next);
|
|
1864
|
-
},
|
|
1865
|
-
onBlur: field.onBlur
|
|
1866
|
-
});
|
|
1867
|
-
var checkboxProps = (field) => ({
|
|
1868
|
-
type: "checkbox",
|
|
1869
|
-
name: field.path,
|
|
1870
|
-
checked: field.value ?? false,
|
|
1871
|
-
"aria-invalid": ariaInvalid(field),
|
|
1872
|
-
onChange: (e) => field.setValue(e.target.checked),
|
|
1873
|
-
onBlur: field.onBlur
|
|
1874
|
-
});
|
|
1875
|
-
var selectProps = (field) => ({
|
|
1876
|
-
name: field.path,
|
|
1877
|
-
value: field.value ?? "",
|
|
1878
|
-
"aria-invalid": ariaInvalid(field),
|
|
1879
|
-
onChange: (e) => {
|
|
1880
|
-
const v = e.target.value;
|
|
1881
|
-
field.setValue(v === "" && field.clearable ? field.emptyValue : v);
|
|
1882
|
-
},
|
|
1883
|
-
onBlur: field.onBlur
|
|
1884
|
-
});
|
|
1885
2067
|
var useFieldA11y = (error) => {
|
|
1886
2068
|
const id = react.useId();
|
|
1887
2069
|
const errorId = `${id}-error`;
|
|
@@ -2207,14 +2389,18 @@ exports.useField = useField;
|
|
|
2207
2389
|
exports.useFieldArray = useFieldArray;
|
|
2208
2390
|
exports.useFields = useFields;
|
|
2209
2391
|
exports.useForm = useForm;
|
|
2392
|
+
exports.useFormAction = useFormAction;
|
|
2393
|
+
exports.useFormActionState = useFormActionState;
|
|
2210
2394
|
exports.useFormError = useFormError;
|
|
2211
2395
|
exports.useFormSelector = useFormSelector;
|
|
2212
2396
|
exports.useFormSelectorShallow = useFormSelectorShallow;
|
|
2397
|
+
exports.useFormSteps = useFormSteps;
|
|
2213
2398
|
exports.useFormValues = useFormValues;
|
|
2214
2399
|
exports.useIsDirty = useIsDirty;
|
|
2215
2400
|
exports.useIsSubmitting = useIsSubmitting;
|
|
2216
2401
|
exports.useIsValid = useIsValid;
|
|
2217
2402
|
exports.useIsValidating = useIsValidating;
|
|
2403
|
+
exports.useMaskedInput = useMaskedInput;
|
|
2218
2404
|
exports.useNumberInput = useNumberInput;
|
|
2219
2405
|
exports.usePersistForm = usePersistForm;
|
|
2220
2406
|
exports.useSubmitCount = useSubmitCount;
|