formstand 0.17.0 → 0.18.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,66 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.18.0 — 2026-09-17
4
+
5
+ ### Changed
6
+
7
+ - **`PersistHandle.restore()` is now `load()`** (breaking). The method's
8
+ DEFAULT application is `adoptValues` — a rebase that reads clean — and
9
+ the old name promised the setValues semantics it only has under
10
+ `apply: "restore"`. The name now describes what it does; the `apply`
11
+ mode names are unchanged. `usePersistForm`'s handle follows.
12
+ - **Cleared inputs write `emptyValue` whenever the schema accepts it.**
13
+ `textInputProps`/`selectProps` (and the bound `TextField`/`SelectField`)
14
+ used to write the empty value only for nullable fields, so clearing
15
+ `z.string().optional()` left `""` behind — a documented asymmetry that
16
+ kept the field dirty against an `undefined` initial and made an optional
17
+ enum select unclearable. Clearing now writes `undefined` for optional
18
+ and defaulted fields and `null` for nullable ones; a REQUIRED string
19
+ cleared to `""` still stays `""`, so the schema judges a visible value
20
+ instead of a hole it rejects. `SelectField`'s empty option is
21
+ accordingly selectable for every clearable field, not just nullable
22
+ ones.
23
+
24
+ ### Added
25
+
26
+ - **`field.clearable`** on `UseFieldReturn`: whether the schema accepts
27
+ the field's `emptyValue` (optional, nullable, or defaulted),
28
+ introspected from the zod schema with the same shallow walk as
29
+ `emptyValue` so the pair can never disagree. Schema-less forms fall
30
+ back to the null-initial heuristic. **`isClearableSchema(schema)`** is
31
+ the exported rule behind it, next to `emptyValueForSchema`.
32
+
33
+ ## formstand-cli 0.18.0 — 2026-09-17
34
+
35
+ ### Changed
36
+
37
+ - **Generated kit adapters clear through `field.clearable`.** The
38
+ emitted text and select bindings wrote the empty value only for
39
+ nullable fields, mirroring the library's old rule; they now write
40
+ `field.emptyValue` whenever `field.clearable` says the schema accepts
41
+ it, so generated optional fields clear to `undefined` instead of
42
+ holding `""`. Kit output therefore needs formstand 0.18; regenerate
43
+ to pick the rule up, or keep the old adapter — both compile.
44
+
45
+ ## formstand-cli 0.17.0 — 2026-09-17
46
+
47
+ ### Added
48
+
49
+ - **Containers inside array rows generate.** The remaining row-shape
50
+ TODO matrix is gone (formstand 0.17 types the paths): a tuple as the
51
+ row item or as a row-object field binds its positions inline at static
52
+ sub-indices (`points.${index}.0`, `segments.${index}.span.1`); a
53
+ discriminated union as a row-object FIELD binds on the holed template
54
+ (`invoices.${p0}.pay`), optional/nullable ones keeping the clearable
55
+ select; an array-of-arrays item extracts an inner Rows component; and
56
+ the same shapes under nested arrays work at any depth, union items
57
+ included (`teams.${p0}.methods.${index}`). Single-file extracts a
58
+ child component per site that needs hooks (rows render inside
59
+ rows.map); the module layout binds in the Row component, which already
60
+ is one. The one shape that still degrades to a TODO is a container
61
+ inside a union variant — binding it needs variant sub-paths the
62
+ library does not type.
63
+
3
64
  ## 0.17.0 — 2026-09-17
4
65
 
5
66
  ### Added
@@ -108,6 +108,7 @@ declare const validateSync: <TSchema extends z.ZodType>(schema: TSchema, values:
108
108
  declare const validateAsync: <TSchema extends z.ZodType>(schema: TSchema, values: z.input<TSchema>) => Promise<SettledValidationResult<z.output<TSchema>>>;
109
109
  declare const isAsyncRequiredError: (e: unknown) => boolean;
110
110
  declare const emptyValueForSchema: (schema: z.ZodType) => null | undefined;
111
+ declare const isClearableSchema: (schema: z.ZodType) => boolean;
111
112
 
112
113
  type ReadonlyStoreApi<T> = Pick<StoreApi<T>, "getState" | "getInitialState" | "subscribe">;
113
114
  type CreateFormOptions<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = Readonly<{
@@ -197,4 +198,4 @@ type Form<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = R
197
198
  }>;
198
199
  declare const createForm: <TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth>(schema: TSchema, options: CreateFormOptions<TSchema, D>) => Form<TSchema, D>;
199
200
 
200
- export { type BoolMap as B, type CreateFormOptions as C, type DefaultPathDepth as D, type ErrorMap as E, type Form as F, type InvalidSubmitHandler as I, type PathDepth as P, type ReadonlyStoreApi as R, type SettledFieldValidationResult as S, type ValidationMode as V, type FieldValidationResult as a, type FormState as b, type FieldPath as c, type FieldValue as d, type FieldSnapshot as e, type FieldsValidationResult as f, type ResetOptions as g, type SettledFieldsValidationResult as h, type SettledValidationResult as i, type SubmitHandler as j, type SubmitOptions as k, type SubmitResult as l, type ValidationResult as m, type ValidationTrigger as n, createForm as o, emptyValueForSchema as p, flattenIssues as q, isAsyncRequiredError as r, shouldValidateOn as s, validateSync as t, validateAsync as v };
201
+ export { type BoolMap as B, type CreateFormOptions as C, type DefaultPathDepth as D, type ErrorMap as E, type Form as F, type InvalidSubmitHandler as I, type PathDepth as P, type ReadonlyStoreApi as R, type SettledFieldValidationResult as S, type ValidationMode as V, type FieldValidationResult as a, type FormState as b, type FieldPath as c, type FieldValue as d, type FieldSnapshot as e, type FieldsValidationResult as f, type ResetOptions as g, type SettledFieldsValidationResult as h, type SettledValidationResult as i, type SubmitHandler as j, type SubmitOptions as k, type SubmitResult as l, type ValidationResult as m, type ValidationTrigger as n, createForm as o, emptyValueForSchema as p, flattenIssues as q, isAsyncRequiredError as r, isClearableSchema as s, shouldValidateOn as t, validateSync as u, validateAsync as v };
@@ -108,6 +108,7 @@ declare const validateSync: <TSchema extends z.ZodType>(schema: TSchema, values:
108
108
  declare const validateAsync: <TSchema extends z.ZodType>(schema: TSchema, values: z.input<TSchema>) => Promise<SettledValidationResult<z.output<TSchema>>>;
109
109
  declare const isAsyncRequiredError: (e: unknown) => boolean;
110
110
  declare const emptyValueForSchema: (schema: z.ZodType) => null | undefined;
111
+ declare const isClearableSchema: (schema: z.ZodType) => boolean;
111
112
 
112
113
  type ReadonlyStoreApi<T> = Pick<StoreApi<T>, "getState" | "getInitialState" | "subscribe">;
113
114
  type CreateFormOptions<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = Readonly<{
@@ -197,4 +198,4 @@ type Form<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = R
197
198
  }>;
198
199
  declare const createForm: <TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth>(schema: TSchema, options: CreateFormOptions<TSchema, D>) => Form<TSchema, D>;
199
200
 
200
- export { type BoolMap as B, type CreateFormOptions as C, type DefaultPathDepth as D, type ErrorMap as E, type Form as F, type InvalidSubmitHandler as I, type PathDepth as P, type ReadonlyStoreApi as R, type SettledFieldValidationResult as S, type ValidationMode as V, type FieldValidationResult as a, type FormState as b, type FieldPath as c, type FieldValue as d, type FieldSnapshot as e, type FieldsValidationResult as f, type ResetOptions as g, type SettledFieldsValidationResult as h, type SettledValidationResult as i, type SubmitHandler as j, type SubmitOptions as k, type SubmitResult as l, type ValidationResult as m, type ValidationTrigger as n, createForm as o, emptyValueForSchema as p, flattenIssues as q, isAsyncRequiredError as r, shouldValidateOn as s, validateSync as t, validateAsync as v };
201
+ export { type BoolMap as B, type CreateFormOptions as C, type DefaultPathDepth as D, type ErrorMap as E, type Form as F, type InvalidSubmitHandler as I, type PathDepth as P, type ReadonlyStoreApi as R, type SettledFieldValidationResult as S, type ValidationMode as V, type FieldValidationResult as a, type FormState as b, type FieldPath as c, type FieldValue as d, type FieldSnapshot as e, type FieldsValidationResult as f, type ResetOptions as g, type SettledFieldsValidationResult as h, type SettledValidationResult as i, type SubmitHandler as j, type SubmitOptions as k, type SubmitResult as l, type ValidationResult as m, type ValidationTrigger as n, createForm as o, emptyValueForSchema as p, flattenIssues as q, isAsyncRequiredError as r, isClearableSchema as s, shouldValidateOn as t, validateSync as u, validateAsync as v };
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { z } from 'zod';
3
- import { F as Form } from '../createForm-D0xLZH-Q.cjs';
3
+ import { F as Form } from '../createForm-B73p1TgL.cjs';
4
4
  import 'zustand/vanilla';
5
5
 
6
6
  type DevtoolsPosition = "bottom-right" | "bottom-left" | "top-right" | "top-left";
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { z } from 'zod';
3
- import { F as Form } from '../createForm-D0xLZH-Q.js';
3
+ import { F as Form } from '../createForm-B73p1TgL.js';
4
4
  import 'zustand/vanilla';
5
5
 
6
6
  type DevtoolsPosition = "bottom-right" | "bottom-left" | "top-right" | "top-left";
package/dist/index.cjs CHANGED
@@ -276,6 +276,7 @@ var emptyValueForSchema = (schema) => {
276
276
  };
277
277
  return walk(schema, false);
278
278
  };
279
+ var isClearableSchema = (schema) => schema instanceof zod.z.ZodOptional || schema instanceof zod.z.ZodNullable || schema instanceof zod.z.ZodDefault || schema instanceof zod.z.ZodPrefault;
279
280
  var unwrapForWalk = (s) => {
280
281
  if (s instanceof zod.z.ZodOptional || s instanceof zod.z.ZodNullable) {
281
282
  return s.unwrap();
@@ -1197,7 +1198,7 @@ var persistForm = (form, options) => {
1197
1198
  return void 0;
1198
1199
  }
1199
1200
  };
1200
- const restore = () => {
1201
+ const load = () => {
1201
1202
  try {
1202
1203
  const raw = storage === null ? null : storage.getItem(options.key);
1203
1204
  if (raw === null) return false;
@@ -1242,7 +1243,7 @@ var persistForm = (form, options) => {
1242
1243
  }
1243
1244
  };
1244
1245
  if (apply !== "manual") {
1245
- restore();
1246
+ load();
1246
1247
  }
1247
1248
  const unsubscribe = form.watchValues((values) => {
1248
1249
  cancelPending();
@@ -1256,7 +1257,7 @@ var persistForm = (form, options) => {
1256
1257
  }
1257
1258
  });
1258
1259
  return Object.freeze({
1259
- restore,
1260
+ load,
1260
1261
  clear: () => {
1261
1262
  cancelPending();
1262
1263
  try {
@@ -1287,7 +1288,7 @@ var usePersistForm = (form, options) => {
1287
1288
  }, [form, key]);
1288
1289
  return react.useMemo(
1289
1290
  () => ({
1290
- restore: () => inner.current?.restore() ?? false,
1291
+ load: () => inner.current?.load() ?? false,
1291
1292
  clear: () => inner.current?.clear(),
1292
1293
  // dispose stops THIS component's watching early; the effect cleanup
1293
1294
  // still runs its own dispose harmlessly (persistForm's dispose is
@@ -1351,12 +1352,17 @@ function useField(form, pathArg, options) {
1351
1352
  const path = slice.path;
1352
1353
  const formSchema = form.schema;
1353
1354
  const initialValue = slice.initialValue;
1354
- const emptyValue = react.useMemo(() => {
1355
+ const { emptyValue, clearable } = react.useMemo(() => {
1355
1356
  if (formSchema !== void 0) {
1356
1357
  const sub = fieldSchemaAtPath(formSchema, path);
1357
- if (sub !== null) return emptyValueForSchema(sub);
1358
+ if (sub !== null) {
1359
+ return {
1360
+ emptyValue: emptyValueForSchema(sub),
1361
+ clearable: isClearableSchema(sub)
1362
+ };
1363
+ }
1358
1364
  }
1359
- return initialValue === null ? null : void 0;
1365
+ return initialValue === null ? { emptyValue: null, clearable: true } : { emptyValue: void 0, clearable: false };
1360
1366
  }, [formSchema, path, initialValue]);
1361
1367
  const debounceMs = options?.debounceMs;
1362
1368
  const debounceTimerRef = react.useRef(null);
@@ -1442,6 +1448,7 @@ function useField(form, pathArg, options) {
1442
1448
  value: slice.value,
1443
1449
  initialValue: slice.initialValue,
1444
1450
  emptyValue,
1451
+ clearable,
1445
1452
  error: slice.error,
1446
1453
  // `?.[0]` covers both the undefined channel and an empty array.
1447
1454
  firstError: slice.error?.[0],
@@ -1456,7 +1463,7 @@ function useField(form, pathArg, options) {
1456
1463
  validateAsync: validateAsync2,
1457
1464
  onBlur
1458
1465
  }),
1459
- [slice, emptyValue, setValue, setTouched, setError, clearError, validate, validateAsync2, onBlur]
1466
+ [slice, emptyValue, clearable, setValue, setTouched, setError, clearError, validate, validateAsync2, onBlur]
1460
1467
  );
1461
1468
  }
1462
1469
 
@@ -1797,9 +1804,7 @@ var textInputProps = (field) => ({
1797
1804
  "aria-invalid": ariaInvalid(field),
1798
1805
  onChange: (e) => {
1799
1806
  const text = e.target.value;
1800
- field.setValue(
1801
- text === "" && field.emptyValue === null ? null : text
1802
- );
1807
+ field.setValue(text === "" && field.clearable ? field.emptyValue : text);
1803
1808
  },
1804
1809
  onBlur: field.onBlur
1805
1810
  });
@@ -1873,9 +1878,7 @@ var selectProps = (field) => ({
1873
1878
  "aria-invalid": ariaInvalid(field),
1874
1879
  onChange: (e) => {
1875
1880
  const v = e.target.value;
1876
- field.setValue(
1877
- v === "" && field.emptyValue === null ? null : v
1878
- );
1881
+ field.setValue(v === "" && field.clearable ? field.emptyValue : v);
1879
1882
  },
1880
1883
  onBlur: field.onBlur
1881
1884
  });
@@ -2082,7 +2085,7 @@ var SelectField = ({
2082
2085
  const field = useField(form, path);
2083
2086
  const a11y = useFieldA11y(field.error);
2084
2087
  const select = selectProps(field);
2085
- const clearable = field.emptyValue === null;
2088
+ const clearable = field.clearable;
2086
2089
  const hasExplicitEmptyOption = options.some((opt) => opt.value === "");
2087
2090
  const showEmptyOption = !hasExplicitEmptyOption && (clearable || select.value === "" || placeholder !== void 0);
2088
2091
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -2189,6 +2192,7 @@ exports.focusFirstError = focusFirstError;
2189
2192
  exports.getAtPath = getAtPath;
2190
2193
  exports.hasFieldError = hasFieldError;
2191
2194
  exports.isAsyncRequiredError = isAsyncRequiredError;
2195
+ exports.isClearableSchema = isClearableSchema;
2192
2196
  exports.numberInputProps = numberInputProps;
2193
2197
  exports.numberToInputText = numberToInputText;
2194
2198
  exports.parseDateText = parseDateText;