formstand 0.25.0 → 0.26.1
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 +205 -0
- package/dist/chunk-XJ6W5CKB.js +22 -0
- package/dist/chunk-XJ6W5CKB.js.map +1 -0
- package/dist/{createForm-CMVALnc3.d.cts → createForm-BXDd9Tg4.d.cts} +18 -7
- package/dist/{createForm-CMVALnc3.d.ts → createForm-BXDd9Tg4.d.ts} +18 -7
- package/dist/devtools/index.cjs +40 -56
- package/dist/devtools/index.cjs.map +1 -1
- package/dist/devtools/index.js +41 -23
- package/dist/devtools/index.js.map +1 -1
- package/dist/index.cjs +535 -163
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -11
- package/dist/index.d.ts +30 -11
- package/dist/index.js +682 -158
- package/dist/index.js.map +1 -1
- package/dist/testing/index.cjs +7 -11
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +4 -4
- package/dist/testing/index.d.ts +4 -4
- package/dist/testing/index.js +7 -11
- package/dist/testing/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-EQIS5IVH.js +0 -172
- package/dist/chunk-EQIS5IVH.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,210 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.26.1 — 2026-11-13
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `watchField` listeners receive the previous snapshot as a second
|
|
8
|
+
argument, `listener(snapshot, prev)`, the way `watchValue` passes the
|
|
9
|
+
previous value.
|
|
10
|
+
- `useFieldArray` returns `validateAsync()`, the awaitable twin of
|
|
11
|
+
`validate()`; `FieldArrayFormApi` gains the optional
|
|
12
|
+
`validateFieldAsync` member that backs it.
|
|
13
|
+
- `useVariantField` and `useFields` (and their bound forms from
|
|
14
|
+
`createFormHooks`) take the same `options` as `useField`, so a variant
|
|
15
|
+
or composite field can debounce its change validation.
|
|
16
|
+
- `FormActionOptions.force`, mirroring `SubmitOptions.force`.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- `validateOnMount` judges every baseline the form takes: the pass runs
|
|
21
|
+
again after `reset()` (unless `keepErrors`) and after `adoptValues()`,
|
|
22
|
+
so a persisted draft adopted over the initial values no longer opens a
|
|
23
|
+
`useIsValid` gate for an invalid draft.
|
|
24
|
+
- `validateFieldsAsync` pruned live errors with a stale full-form map
|
|
25
|
+
when a batch mixed a full-parse path with an index into an empty array
|
|
26
|
+
and the values changed in flight; the prune now rides only while the
|
|
27
|
+
whole values reference held.
|
|
28
|
+
- `restore()` disowns in-flight async passes the way `reset()` and
|
|
29
|
+
`adoptValues()` do, so a pass started before the restore cannot commit
|
|
30
|
+
a verdict onto the restored state.
|
|
31
|
+
- `persistForm`'s gap fill fills a nullable-but-required key the draft
|
|
32
|
+
lacks, no longer splices one union variant's key into another, and no
|
|
33
|
+
longer resurrects a `z.record` key the draft removed.
|
|
34
|
+
- A manual `load()` no longer writes the draft it just read straight
|
|
35
|
+
back to storage.
|
|
36
|
+
- The auto-apply skips a draft the form already holds, so
|
|
37
|
+
`usePersistForm` with an inline `storage` object (a resubscribe per
|
|
38
|
+
render) no longer re-adopts the draft every render, wiping errors and
|
|
39
|
+
dirty state and looping with `useFormValues`; StrictMode's second
|
|
40
|
+
mount adopts nothing either.
|
|
41
|
+
- `useFieldArray`'s change and blur triggers report a throwing async
|
|
42
|
+
array refine through `console.error`, the way `useField`'s triggers
|
|
43
|
+
do, instead of leaving an unhandled rejection.
|
|
44
|
+
- `useFieldArray.update(index, item)` past the end (or with a
|
|
45
|
+
non-integer index) is refused with a warning, like the index ops; it
|
|
46
|
+
used to write `null` holes.
|
|
47
|
+
- `useFormSteps.next()` and `goTo()` judge a step on its live values: a
|
|
48
|
+
step value that changed while an async rule was in flight is validated
|
|
49
|
+
again instead of advancing on the stale verdict. With no steps defined
|
|
50
|
+
`next()` no longer sets the index to -1.
|
|
51
|
+
- `dateToInputText` reads a date-only string (a persisted draft's
|
|
52
|
+
`"2026-03-01"`) as the calendar date it spells; the Date constructor
|
|
53
|
+
parsed it as UTC midnight, which rendered the previous day west of UTC.
|
|
54
|
+
- The typed variant hooks bind a union nested inside another union's
|
|
55
|
+
variant (`useVariantField(form, "shape.detail", "x")`); `UnionValueAt`
|
|
56
|
+
walked common keys only, so neither spelling compiled.
|
|
57
|
+
- `CheckboxField` without a `label` no longer renders an empty `<label>`
|
|
58
|
+
around the input.
|
|
59
|
+
- The devtools panel keeps its held snapshot across a collapse (0.26.0
|
|
60
|
+
moved the held state into the open branch), and its diff shows a
|
|
61
|
+
field cleared to `undefined` as `"undefined"` instead of `{}`.
|
|
62
|
+
- `fillAndBlurField` returns the fill's result when the blur gate did
|
|
63
|
+
not validate (`mode: "onChange"`, or after a submit), instead of
|
|
64
|
+
`undefined` for a pass it had started.
|
|
65
|
+
- `persistForm`'s gap fill walks through lazy, intersection and
|
|
66
|
+
preprocess schemas again (the container check above stopped at them),
|
|
67
|
+
so a plain required key under those roots is filled as in 0.26.0.
|
|
68
|
+
- `useField`'s `emptyValue` and `clearable` for a key shared by union
|
|
69
|
+
variants follow the variant the live discriminant selects, so clearing
|
|
70
|
+
a nullable field on the active variant writes `null` instead of the
|
|
71
|
+
first variant's `undefined`.
|
|
72
|
+
- A write the path walker refuses (a string key into an array, a key
|
|
73
|
+
through a `Date`) no longer rebuilds the values spine, so watchers do
|
|
74
|
+
not fire, the op log is kept and spine server verdicts stay, matching
|
|
75
|
+
the "value left unchanged" warning.
|
|
76
|
+
- A field-scoped pass that ran the full parse takes the parse's current
|
|
77
|
+
message for an out-of-scope key it already held, so a cross-field rule
|
|
78
|
+
with a dynamic message no longer shows the stale text.
|
|
79
|
+
- `usePersistForm` forwards every storage call to the storage of the
|
|
80
|
+
latest render through a proxy instead of resubscribing when the
|
|
81
|
+
`storage` identity changes: an inline storage object resubscribed on
|
|
82
|
+
every render, and each outgoing subscription flushed, which collapsed
|
|
83
|
+
the debounce to nothing and re-read the draft each render.
|
|
84
|
+
- `useFormSteps` reads the step the user is on when `next()`, `back()`
|
|
85
|
+
or `goTo()` is called, not the step of the render the closure came
|
|
86
|
+
from, so two awaited `next()` calls from one handler advance twice and
|
|
87
|
+
a `back()` pressed during an in-flight advance wins at once. The three
|
|
88
|
+
are reference-stable across renders now.
|
|
89
|
+
- `dateToInputText` renders `""` for a date-shaped string that spells no
|
|
90
|
+
real day (`"2026-02-31"`) instead of the Date constructor's reading.
|
|
91
|
+
- The devtools panel renders `NaN` and the infinities as their own
|
|
92
|
+
spelling in the fields table and the diff instead of `null`, and
|
|
93
|
+
forgets its held snapshot when the `form` prop changes to another
|
|
94
|
+
form instead of restoring the old form's state into the new one.
|
|
95
|
+
|
|
96
|
+
## 0.26.0 — 2026-11-12
|
|
97
|
+
|
|
98
|
+
### Added
|
|
99
|
+
|
|
100
|
+
- `PersistHandle.flush()` writes the pending debounced draft now, for
|
|
101
|
+
the moments that cannot wait out the debounce (a route change, a
|
|
102
|
+
`pagehide` handler). `dispose()` flushes too; see Fixed below.
|
|
103
|
+
- `submit` and `handleSubmit` take an options object in the second
|
|
104
|
+
position: `submit(onValid, { onInvalid, onError, force })`, the shape
|
|
105
|
+
`useFormAction`'s `FormActionOptions` always had, so `submit(onValid,
|
|
106
|
+
undefined, { force: true })` is no longer the only spelling.
|
|
107
|
+
`SubmitOptions` gains `onInvalid`; a positional `onInvalid` wins when
|
|
108
|
+
both are given. The positional shape stays.
|
|
109
|
+
- `ErrorPath<TValues, D>` is exported: the path type `setError`,
|
|
110
|
+
`clearErrors`, `validateField` and `validateFieldAsync` take
|
|
111
|
+
(`FieldPath` plus the root `""`), for wrappers that forward those
|
|
112
|
+
calls.
|
|
113
|
+
- `createFormHooks` binds the three hooks it skipped while the docs
|
|
114
|
+
promised every hook: `use{Name}Action` and `use{Name}ActionState`
|
|
115
|
+
(the form-actions bridges) and `use{Name}Persist` (`usePersistForm`),
|
|
116
|
+
with the `BoundUseAction`, `BoundUseActionState` and `BoundUsePersist`
|
|
117
|
+
types exported.
|
|
118
|
+
- `FieldSnapshot` (from `getFieldState` and `watchField`) carries
|
|
119
|
+
`initialValue` and `firstError`, matching `useField`.
|
|
120
|
+
- `fillField`, `blurField` and `fillAndBlurField` from `formstand/testing`
|
|
121
|
+
return the `FieldValidationResult` when the gate validated
|
|
122
|
+
(`undefined` when it did not), so a test can await a pending async
|
|
123
|
+
refine instead of guessing a delay.
|
|
124
|
+
|
|
125
|
+
### Fixed
|
|
126
|
+
|
|
127
|
+
- A throwing async refinement (a rejected fetch inside `refine(async
|
|
128
|
+
...)`) left `isValidatingForm` or `isValidating[path]` at `true` for
|
|
129
|
+
good: none of the async passes released their flag when the parse
|
|
130
|
+
rejected, so every spinner and submit gate built on them stuck after
|
|
131
|
+
one network failure. The passes now release the flags they own before
|
|
132
|
+
the rejection travels on to whoever awaited them. The passes nobody
|
|
133
|
+
awaits, `validateOnMount` and the change and blur triggers inside
|
|
134
|
+
`useField`, report the error through `console.error` instead of
|
|
135
|
+
leaving an unhandled rejection.
|
|
136
|
+
- Switching a discriminated union's variant through its discriminant
|
|
137
|
+
(`useField(form, "payment.method").setValue("paypal")`, which is how
|
|
138
|
+
generated forms bind it) left the old variant's schema errors in place
|
|
139
|
+
until a full `validate()` ran, so `useIsValid` read `false` for a form
|
|
140
|
+
the schema accepted. A field-scoped pass that runs the full parse (any
|
|
141
|
+
path under a union or a refined object) now also drops the schema
|
|
142
|
+
errors elsewhere that the parse no longer reports. It still adds
|
|
143
|
+
nothing outside its scope.
|
|
144
|
+
- `setErrors({ field: [] })`, an `updateState` patch of either error
|
|
145
|
+
channel, and `restore()` kept empty lists as entries, where `setError`
|
|
146
|
+
and `addErrors` dropped them, so one state read valid to `useIsValid`,
|
|
147
|
+
errored to `useFormSteps`, and `[]` from `useField(...).error`. Every
|
|
148
|
+
writer now drops an empty list.
|
|
149
|
+
- A throwing `onInvalid` resolved `{ kind: "error" }` without calling
|
|
150
|
+
`onError`; it does now, like a throwing `onValid`.
|
|
151
|
+
- `persistForm`: unmounting inside the debounce window lost the last
|
|
152
|
+
edits, because `dispose()` cancelled the pending write. It writes the
|
|
153
|
+
draft first now.
|
|
154
|
+
- `persistForm`: `clear()` followed by `reset()` (the usual after-submit
|
|
155
|
+
pair) wrote the fresh baseline straight back as a draft, which the
|
|
156
|
+
next visit then adopted over whatever initial values the app had
|
|
157
|
+
meanwhile computed. After `clear()`, a rebase that leaves the form
|
|
158
|
+
pristine no longer writes; the next real edit does.
|
|
159
|
+
- `persistForm`: a draft stored before the schema gained a key erased
|
|
160
|
+
that key's initial value from `values` and `initialValues`, so
|
|
161
|
+
`reset()` could not bring it back and the user saw zod's generic
|
|
162
|
+
message instead of yours. Required keys the draft lacks are now filled
|
|
163
|
+
from the initial values (nested objects included); optional keys stay
|
|
164
|
+
absent, since an optional the user cleared looks the same.
|
|
165
|
+
- `persistForm`'s shape guard refused legitimate drafts: `z.coerce.number()`
|
|
166
|
+
behind a `""` initial value, and unions whose variants shape the same
|
|
167
|
+
key differently. The guard now asks the schema whether a kind change
|
|
168
|
+
is legitimate at the path before refusing, and a dev build logs why a
|
|
169
|
+
draft that was present got ignored (version mismatch, shape conflict
|
|
170
|
+
and its path, corrupt JSON), where `load()` used to return a silent
|
|
171
|
+
`false`.
|
|
172
|
+
- `usePersistForm` ignored a change of `options.storage` and kept writing
|
|
173
|
+
to the first storage it saw; it resubscribes on it, as it does for
|
|
174
|
+
`key`.
|
|
175
|
+
- `DateField` and `dateInputProps` threw `value.getTime is not a
|
|
176
|
+
function` at render after a persisted draft loaded a `Date` back as
|
|
177
|
+
the string JSON made of it (persist does not revive Dates, by design),
|
|
178
|
+
and the rewritten draft made the crash stick across visits. The input
|
|
179
|
+
now shows the string's calendar date; the schema still rejects it at
|
|
180
|
+
validation.
|
|
181
|
+
- `useFormSteps.goTo(NaN)` (a `Number("")` from an empty option)
|
|
182
|
+
recursed without bound and froze the tab. A target that is not a
|
|
183
|
+
finite number now resolves the current index.
|
|
184
|
+
- `parseNumberText`, behind `NumberField`, `numberInputProps` and
|
|
185
|
+
`useNumberInput`, accepted every JavaScript number literal, so typing
|
|
186
|
+
`0x10` stored 16. It reads plain decimal notation only.
|
|
187
|
+
- `useMaskedInput` cleared a required string field to `undefined` when
|
|
188
|
+
its key was omitted from the initial values, because the initial
|
|
189
|
+
value's type was the only signal. The current value counts too, so the
|
|
190
|
+
field clears to `""` and reports your message instead of zod's.
|
|
191
|
+
- `useField`'s `debounceMs` also delayed the blur validation. Blur now
|
|
192
|
+
drops a pending timer and validates at once.
|
|
193
|
+
- `useFields` returned a fresh object, `fields` array and `onBlur` on
|
|
194
|
+
every render while its sibling hooks memoize; it keeps its identity
|
|
195
|
+
until one of its fields changes. A paths tuple that changes length now
|
|
196
|
+
throws the hook's own message, ahead of React's hooks-count error.
|
|
197
|
+
- `FormstandDevtools` labelled a server error as "(showing)" or "(behind
|
|
198
|
+
a schema error)" by walking the flat schema-error map as a path, so a
|
|
199
|
+
root verdict always read as behind and a nested key always as
|
|
200
|
+
showing. It reads the key directly now.
|
|
201
|
+
- `FormstandDevtools` threw on a `bigint` value (`z.bigint()`), in the
|
|
202
|
+
value preview and in the diff; a bigint renders as its literal
|
|
203
|
+
spelling (`10n`).
|
|
204
|
+
- `FormstandDevtools` walked every leaf through `getFieldState` and ran
|
|
205
|
+
`diff()` on every store write while collapsed; a collapsed panel now
|
|
206
|
+
keeps only its store subscription.
|
|
207
|
+
|
|
3
208
|
## 0.25.0 — 2026-11-12
|
|
4
209
|
|
|
5
210
|
### Added
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useStore } from 'zustand/react';
|
|
2
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
3
|
+
|
|
4
|
+
// src/react/useFormSelector.ts
|
|
5
|
+
function useFormSelector(form, selector) {
|
|
6
|
+
return useStore(form.store, selector);
|
|
7
|
+
}
|
|
8
|
+
function useFormSelectorShallow(form, selector) {
|
|
9
|
+
return useStore(form.store, useShallow(selector));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/core/env.ts
|
|
13
|
+
var isDevBuild = () => typeof process === "undefined" || process.env?.["NODE_ENV"] !== "production";
|
|
14
|
+
var isDevToolsBuild = () => {
|
|
15
|
+
if (typeof process === "undefined") return false;
|
|
16
|
+
const env = process.env?.["NODE_ENV"];
|
|
17
|
+
return typeof env === "string" && env !== "production";
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export { isDevBuild, isDevToolsBuild, useFormSelector, useFormSelectorShallow };
|
|
21
|
+
//# sourceMappingURL=chunk-XJ6W5CKB.js.map
|
|
22
|
+
//# sourceMappingURL=chunk-XJ6W5CKB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/react/useFormSelector.ts","../src/core/env.ts"],"names":[],"mappings":";;;;AAiCO,SAAS,eAAA,CACd,MACA,QAAA,EACG;AACH,EAAA,OAAO,QAAA,CAAS,IAAA,CAAK,KAAA,EAAO,QAAQ,CAAA;AACtC;AAcO,SAAS,sBAAA,CACd,MACA,QAAA,EACG;AACH,EAAA,OAAO,QAAA,CAAS,IAAA,CAAK,KAAA,EAAO,UAAA,CAAW,QAAQ,CAAC,CAAA;AAClD;;;AC/CO,IAAM,UAAA,GAAa,MACxB,OAAO,OAAA,KAAY,eACnB,OAAA,CAAQ,GAAA,GAAM,UAAU,CAAA,KAAM;AAWzB,IAAM,kBAAkB,MAAe;AAC5C,EAAA,IAAI,OAAO,OAAA,KAAY,WAAA,EAAa,OAAO,KAAA;AAC3C,EAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,GAAA,GAAM,UAAU,CAAA;AACpC,EAAA,OAAO,OAAO,GAAA,KAAQ,QAAA,IAAY,GAAA,KAAQ,YAAA;AAC5C","file":"chunk-XJ6W5CKB.js","sourcesContent":["import type { z } from \"zod\";\nimport type { StoreApi } from \"zustand/vanilla\";\nimport { useStore } from \"zustand/react\";\nimport { useShallow } from \"zustand/react/shallow\";\nimport type { Form } from \"../core/createForm\";\nimport type { DefaultPathDepth, PathDepth } from \"../core/fieldPath\";\nimport type { FormState } from \"../core/types\";\n\ntype ReadonlyStore<T> = Pick<\n StoreApi<T>,\n \"getState\" | \"getInitialState\" | \"subscribe\"\n>;\n\nexport type FormStateApi = Readonly<{\n store: ReadonlyStore<FormState<unknown>>;\n}>;\n\nexport function useFormSelector<\n TSchema extends z.ZodType,\n U,\n D extends PathDepth = DefaultPathDepth,\n>(\n form: Form<TSchema, D>,\n selector: (state: FormState<z.input<TSchema>>) => U,\n): U;\n// The `schema?: undefined` brand forces Form<TSchema> (which has a real\n// `schema` property) to bind only the typed overload above. Without\n// it, Form<TSchema> would structurally satisfy FormStateApi too and a\n// bad selector could silently widen to FormState<unknown>.\nexport function useFormSelector<U>(\n form: FormStateApi & { readonly schema?: undefined },\n selector: (state: FormState<unknown>) => U,\n): U;\nexport function useFormSelector<U>(\n form: FormStateApi,\n selector: (state: FormState<unknown>) => U,\n): U {\n return useStore(form.store, selector);\n}\n\nexport function useFormSelectorShallow<\n TSchema extends z.ZodType,\n U,\n D extends PathDepth = DefaultPathDepth,\n>(\n form: Form<TSchema, D>,\n selector: (state: FormState<z.input<TSchema>>) => U,\n): U;\nexport function useFormSelectorShallow<U>(\n form: FormStateApi & { readonly schema?: undefined },\n selector: (state: FormState<unknown>) => U,\n): U;\nexport function useFormSelectorShallow<U>(\n form: FormStateApi,\n selector: (state: FormState<unknown>) => U,\n): U {\n return useStore(form.store, useShallow(selector));\n}\n","// The dev-build test the library consults before emitting a console\n// warning. Bundlers that define NODE_ENV fold it to a constant and drop the\n// dev-only branches; the `typeof process` guard keeps the SAME expression\n// from throwing in a runtime that defines no `process` at all (a CDN/ESM\n// consumer, a test runner without the define). That last case matters most\n// on the submit path: handleSubmit promises never to reject, and the\n// warning that fires only when a save failed was the one line that could\n// break the promise exactly on the failure path. It FAILS OPEN on purpose:\n// an unknowable environment gets the warnings, which cost nothing but a\n// console line.\nexport const isDevBuild = (): boolean =>\n typeof process === \"undefined\" ||\n process.env?.[\"NODE_ENV\"] !== \"production\";\n\n// The gate for the two sites that EXPOSE form state: the FormstandDevtools\n// panel (renders every value and error into the page) and the zustand\n// devtools middleware (streams every write to the Redux extension). These\n// must FAIL CLOSED: isDevBuild's \"no process means dev\" reading let a\n// CDN/ESM, Deno, or worker production build mount the panel and keep the\n// middleware live for any end user with the extension installed. Only a\n// readable NODE_ENV that is a string other than \"production\" counts as\n// development; no process, no env, or an unset NODE_ENV all read as\n// production here.\nexport const isDevToolsBuild = (): boolean => {\n if (typeof process === \"undefined\") return false;\n const env = process.env?.[\"NODE_ENV\"];\n return typeof env === \"string\" && env !== \"production\";\n};\n"]}
|
|
@@ -130,6 +130,7 @@ type SubmitHandler<TSchema extends z.ZodType> = (data: z.output<TSchema>) => voi
|
|
|
130
130
|
type InvalidSubmitHandler = (errors: ErrorMap) => void;
|
|
131
131
|
type SubmitOptions = Readonly<{
|
|
132
132
|
force?: boolean;
|
|
133
|
+
onInvalid?: InvalidSubmitHandler;
|
|
133
134
|
onError?: (error: unknown) => void;
|
|
134
135
|
}>;
|
|
135
136
|
type SubmitResult<TOutput> = Readonly<{
|
|
@@ -152,7 +153,9 @@ type ResetOptions = Readonly<{
|
|
|
152
153
|
}>;
|
|
153
154
|
type FieldSnapshot<TValue> = Readonly<{
|
|
154
155
|
value: TValue;
|
|
156
|
+
initialValue: TValue;
|
|
155
157
|
error: readonly string[] | undefined;
|
|
158
|
+
firstError: string | undefined;
|
|
156
159
|
touched: boolean;
|
|
157
160
|
dirty: boolean;
|
|
158
161
|
isValidating: boolean;
|
|
@@ -164,7 +167,7 @@ type Form<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = R
|
|
|
164
167
|
getState: () => FormState<z.input<TSchema>>;
|
|
165
168
|
subscribe: (listener: (state: FormState<z.input<TSchema>>, prev: FormState<z.input<TSchema>>) => void) => () => void;
|
|
166
169
|
getField: <P extends FieldPath<z.input<TSchema>, D>>(path: P) => FieldValue<z.input<TSchema>, P>;
|
|
167
|
-
watchField: <P extends FieldPath<z.input<TSchema>, D>>(path: P, listener: (snapshot: FieldSnapshot<FieldValue<z.input<TSchema>, P>>) => void) => () => void;
|
|
170
|
+
watchField: <P extends FieldPath<z.input<TSchema>, D>>(path: P, listener: (snapshot: FieldSnapshot<FieldValue<z.input<TSchema>, P>>, previous: FieldSnapshot<FieldValue<z.input<TSchema>, P>>) => void) => () => void;
|
|
168
171
|
watchValue: <P extends FieldPath<z.input<TSchema>, D>>(path: P, listener: (value: FieldValue<z.input<TSchema>, P>, previous: FieldValue<z.input<TSchema>, P>) => void) => () => void;
|
|
169
172
|
watchValues: (listener: (values: z.input<TSchema>, previous: z.input<TSchema>) => void) => () => void;
|
|
170
173
|
setValue: <P extends FieldPath<z.input<TSchema>, D>>(path: P, value: FieldValue<z.input<TSchema>, P>) => void;
|
|
@@ -188,15 +191,23 @@ type Form<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = R
|
|
|
188
191
|
validateAsync: () => Promise<SettledValidationResult<z.output<TSchema>>>;
|
|
189
192
|
validateFieldAsync: (path: ErrorPath<z.input<TSchema>, D>) => Promise<SettledFieldValidationResult>;
|
|
190
193
|
validateFieldsAsync: (paths: readonly FieldPath<z.input<TSchema>, D>[]) => Promise<SettledFieldsValidationResult>;
|
|
191
|
-
submit:
|
|
194
|
+
submit: {
|
|
195
|
+
(onValid: SubmitHandler<TSchema>, onInvalid?: InvalidSubmitHandler, options?: SubmitOptions): Promise<SubmitResult<z.output<TSchema>>>;
|
|
196
|
+
(onValid: SubmitHandler<TSchema>, options: SubmitOptions): Promise<SubmitResult<z.output<TSchema>>>;
|
|
197
|
+
};
|
|
192
198
|
arrayPush: <P extends ArrayFieldPath<z.input<TSchema>, D>>(path: P, item: ArrayItemOf<FieldValue<z.input<TSchema>, P>>) => void;
|
|
193
199
|
arrayRemove: (path: ArrayFieldPath<z.input<TSchema>, D>, index: number) => void;
|
|
194
200
|
arrayInsert: <P extends ArrayFieldPath<z.input<TSchema>, D>>(path: P, index: number, item: ArrayItemOf<FieldValue<z.input<TSchema>, P>>) => void;
|
|
195
201
|
arrayMove: (path: ArrayFieldPath<z.input<TSchema>, D>, from: number, to: number) => void;
|
|
196
202
|
arraySwap: (path: ArrayFieldPath<z.input<TSchema>, D>, a: number, b: number) => void;
|
|
197
|
-
handleSubmit:
|
|
198
|
-
|
|
199
|
-
|
|
203
|
+
handleSubmit: {
|
|
204
|
+
(onValid: SubmitHandler<TSchema>, onInvalid?: InvalidSubmitHandler, options?: SubmitOptions): (event?: {
|
|
205
|
+
preventDefault: () => void;
|
|
206
|
+
}) => Promise<SubmitResult<z.output<TSchema>>>;
|
|
207
|
+
(onValid: SubmitHandler<TSchema>, options: SubmitOptions): (event?: {
|
|
208
|
+
preventDefault: () => void;
|
|
209
|
+
}) => Promise<SubmitResult<z.output<TSchema>>>;
|
|
210
|
+
};
|
|
200
211
|
diff: () => Readonly<Record<string, unknown>>;
|
|
201
212
|
dirtyFields: () => readonly string[];
|
|
202
213
|
snapshot: () => FormState<z.input<TSchema>>;
|
|
@@ -204,5 +215,5 @@ type Form<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = R
|
|
|
204
215
|
}>;
|
|
205
216
|
declare const createForm: <TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth>(schema: TSchema, options: CreateFormOptions<TSchema, D>) => Form<TSchema, NoInfer<D>>;
|
|
206
217
|
|
|
207
|
-
export { createForm as
|
|
208
|
-
export type { ArrayFieldPath as A, BoolMap as B, CreateFormOptions as C, DefaultPathDepth as D, ErrorMap as E, Form as F, InvalidSubmitHandler as I, PathDepth as P, ReadonlyStoreApi as R, SettledFieldValidationResult as S, ValidationMode as V, FormState as a, FieldValidationResult as b, FieldPath as c, FieldValue as d, ArrayItemOf as e,
|
|
218
|
+
export { createForm as q, emptyValueForSchema as r, flattenIssues as s, isAsyncRequiredError as t, isClearableSchema as u, shouldValidateOn as v, validateAsync as w, validateSync as x };
|
|
219
|
+
export type { ArrayFieldPath as A, BoolMap as B, CreateFormOptions as C, DefaultPathDepth as D, ErrorMap as E, Form as F, InvalidSubmitHandler as I, PathDepth as P, ReadonlyStoreApi as R, SettledFieldValidationResult as S, ValidationMode as V, FormState as a, FieldValidationResult as b, FieldPath as c, FieldValue as d, ArrayItemOf as e, ErrorPath as f, FieldSnapshot as g, FieldsValidationResult as h, ResetOptions as i, SettledFieldsValidationResult as j, SettledValidationResult as k, SubmitHandler as l, SubmitOptions as m, SubmitResult as n, ValidationResult as o, ValidationTrigger as p };
|
|
@@ -130,6 +130,7 @@ type SubmitHandler<TSchema extends z.ZodType> = (data: z.output<TSchema>) => voi
|
|
|
130
130
|
type InvalidSubmitHandler = (errors: ErrorMap) => void;
|
|
131
131
|
type SubmitOptions = Readonly<{
|
|
132
132
|
force?: boolean;
|
|
133
|
+
onInvalid?: InvalidSubmitHandler;
|
|
133
134
|
onError?: (error: unknown) => void;
|
|
134
135
|
}>;
|
|
135
136
|
type SubmitResult<TOutput> = Readonly<{
|
|
@@ -152,7 +153,9 @@ type ResetOptions = Readonly<{
|
|
|
152
153
|
}>;
|
|
153
154
|
type FieldSnapshot<TValue> = Readonly<{
|
|
154
155
|
value: TValue;
|
|
156
|
+
initialValue: TValue;
|
|
155
157
|
error: readonly string[] | undefined;
|
|
158
|
+
firstError: string | undefined;
|
|
156
159
|
touched: boolean;
|
|
157
160
|
dirty: boolean;
|
|
158
161
|
isValidating: boolean;
|
|
@@ -164,7 +167,7 @@ type Form<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = R
|
|
|
164
167
|
getState: () => FormState<z.input<TSchema>>;
|
|
165
168
|
subscribe: (listener: (state: FormState<z.input<TSchema>>, prev: FormState<z.input<TSchema>>) => void) => () => void;
|
|
166
169
|
getField: <P extends FieldPath<z.input<TSchema>, D>>(path: P) => FieldValue<z.input<TSchema>, P>;
|
|
167
|
-
watchField: <P extends FieldPath<z.input<TSchema>, D>>(path: P, listener: (snapshot: FieldSnapshot<FieldValue<z.input<TSchema>, P>>) => void) => () => void;
|
|
170
|
+
watchField: <P extends FieldPath<z.input<TSchema>, D>>(path: P, listener: (snapshot: FieldSnapshot<FieldValue<z.input<TSchema>, P>>, previous: FieldSnapshot<FieldValue<z.input<TSchema>, P>>) => void) => () => void;
|
|
168
171
|
watchValue: <P extends FieldPath<z.input<TSchema>, D>>(path: P, listener: (value: FieldValue<z.input<TSchema>, P>, previous: FieldValue<z.input<TSchema>, P>) => void) => () => void;
|
|
169
172
|
watchValues: (listener: (values: z.input<TSchema>, previous: z.input<TSchema>) => void) => () => void;
|
|
170
173
|
setValue: <P extends FieldPath<z.input<TSchema>, D>>(path: P, value: FieldValue<z.input<TSchema>, P>) => void;
|
|
@@ -188,15 +191,23 @@ type Form<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = R
|
|
|
188
191
|
validateAsync: () => Promise<SettledValidationResult<z.output<TSchema>>>;
|
|
189
192
|
validateFieldAsync: (path: ErrorPath<z.input<TSchema>, D>) => Promise<SettledFieldValidationResult>;
|
|
190
193
|
validateFieldsAsync: (paths: readonly FieldPath<z.input<TSchema>, D>[]) => Promise<SettledFieldsValidationResult>;
|
|
191
|
-
submit:
|
|
194
|
+
submit: {
|
|
195
|
+
(onValid: SubmitHandler<TSchema>, onInvalid?: InvalidSubmitHandler, options?: SubmitOptions): Promise<SubmitResult<z.output<TSchema>>>;
|
|
196
|
+
(onValid: SubmitHandler<TSchema>, options: SubmitOptions): Promise<SubmitResult<z.output<TSchema>>>;
|
|
197
|
+
};
|
|
192
198
|
arrayPush: <P extends ArrayFieldPath<z.input<TSchema>, D>>(path: P, item: ArrayItemOf<FieldValue<z.input<TSchema>, P>>) => void;
|
|
193
199
|
arrayRemove: (path: ArrayFieldPath<z.input<TSchema>, D>, index: number) => void;
|
|
194
200
|
arrayInsert: <P extends ArrayFieldPath<z.input<TSchema>, D>>(path: P, index: number, item: ArrayItemOf<FieldValue<z.input<TSchema>, P>>) => void;
|
|
195
201
|
arrayMove: (path: ArrayFieldPath<z.input<TSchema>, D>, from: number, to: number) => void;
|
|
196
202
|
arraySwap: (path: ArrayFieldPath<z.input<TSchema>, D>, a: number, b: number) => void;
|
|
197
|
-
handleSubmit:
|
|
198
|
-
|
|
199
|
-
|
|
203
|
+
handleSubmit: {
|
|
204
|
+
(onValid: SubmitHandler<TSchema>, onInvalid?: InvalidSubmitHandler, options?: SubmitOptions): (event?: {
|
|
205
|
+
preventDefault: () => void;
|
|
206
|
+
}) => Promise<SubmitResult<z.output<TSchema>>>;
|
|
207
|
+
(onValid: SubmitHandler<TSchema>, options: SubmitOptions): (event?: {
|
|
208
|
+
preventDefault: () => void;
|
|
209
|
+
}) => Promise<SubmitResult<z.output<TSchema>>>;
|
|
210
|
+
};
|
|
200
211
|
diff: () => Readonly<Record<string, unknown>>;
|
|
201
212
|
dirtyFields: () => readonly string[];
|
|
202
213
|
snapshot: () => FormState<z.input<TSchema>>;
|
|
@@ -204,5 +215,5 @@ type Form<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = R
|
|
|
204
215
|
}>;
|
|
205
216
|
declare const createForm: <TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth>(schema: TSchema, options: CreateFormOptions<TSchema, D>) => Form<TSchema, NoInfer<D>>;
|
|
206
217
|
|
|
207
|
-
export { createForm as
|
|
208
|
-
export type { ArrayFieldPath as A, BoolMap as B, CreateFormOptions as C, DefaultPathDepth as D, ErrorMap as E, Form as F, InvalidSubmitHandler as I, PathDepth as P, ReadonlyStoreApi as R, SettledFieldValidationResult as S, ValidationMode as V, FormState as a, FieldValidationResult as b, FieldPath as c, FieldValue as d, ArrayItemOf as e,
|
|
218
|
+
export { createForm as q, emptyValueForSchema as r, flattenIssues as s, isAsyncRequiredError as t, isClearableSchema as u, shouldValidateOn as v, validateAsync as w, validateSync as x };
|
|
219
|
+
export type { ArrayFieldPath as A, BoolMap as B, CreateFormOptions as C, DefaultPathDepth as D, ErrorMap as E, Form as F, InvalidSubmitHandler as I, PathDepth as P, ReadonlyStoreApi as R, SettledFieldValidationResult as S, ValidationMode as V, FormState as a, FieldValidationResult as b, FieldPath as c, FieldValue as d, ArrayItemOf as e, ErrorPath as f, FieldSnapshot as g, FieldsValidationResult as h, ResetOptions as i, SettledFieldsValidationResult as j, SettledValidationResult as k, SubmitHandler as l, SubmitOptions as m, SubmitResult as n, ValidationResult as o, ValidationTrigger as p };
|
package/dist/devtools/index.cjs
CHANGED
|
@@ -13,40 +13,6 @@ var isDevToolsBuild = () => {
|
|
|
13
13
|
const env = process.env?.["NODE_ENV"];
|
|
14
14
|
return typeof env === "string" && env !== "production";
|
|
15
15
|
};
|
|
16
|
-
|
|
17
|
-
// src/core/path.ts
|
|
18
|
-
var isIndexSegment = (s) => {
|
|
19
|
-
const n = Number(s);
|
|
20
|
-
return Number.isInteger(n) && n >= 0 && String(n) === s;
|
|
21
|
-
};
|
|
22
|
-
var PARSE_CACHE_MAX = 4096;
|
|
23
|
-
var parseCache = /* @__PURE__ */ new Map();
|
|
24
|
-
var parsePath = (path) => {
|
|
25
|
-
const cached = parseCache.get(path);
|
|
26
|
-
if (cached !== void 0) {
|
|
27
|
-
parseCache.delete(path);
|
|
28
|
-
parseCache.set(path, cached);
|
|
29
|
-
return cached;
|
|
30
|
-
}
|
|
31
|
-
const segments = Object.freeze(
|
|
32
|
-
path === "" ? [] : path.split(".").map((s) => isIndexSegment(s) ? Number(s) : s)
|
|
33
|
-
);
|
|
34
|
-
if (parseCache.size >= PARSE_CACHE_MAX) {
|
|
35
|
-
const oldest = parseCache.keys().next().value;
|
|
36
|
-
if (oldest !== void 0) parseCache.delete(oldest);
|
|
37
|
-
}
|
|
38
|
-
parseCache.set(path, segments);
|
|
39
|
-
return segments;
|
|
40
|
-
};
|
|
41
|
-
var isPlainRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
42
|
-
var readSegment = (obj, segment) => {
|
|
43
|
-
if (obj == null) return void 0;
|
|
44
|
-
if (Array.isArray(obj)) {
|
|
45
|
-
return typeof segment === "number" ? obj[segment] : void 0;
|
|
46
|
-
}
|
|
47
|
-
return isPlainRecord(obj) && Object.hasOwn(obj, String(segment)) ? obj[String(segment)] : void 0;
|
|
48
|
-
};
|
|
49
|
-
var getAtPath = (obj, path) => parsePath(path).reduce(readSegment, obj);
|
|
50
16
|
function useFormSelectorShallow(form, selector) {
|
|
51
17
|
return react$1.useStore(form.store, shallow.useShallow(selector));
|
|
52
18
|
}
|
|
@@ -163,10 +129,15 @@ var styles = {
|
|
|
163
129
|
cursor: "pointer"
|
|
164
130
|
}
|
|
165
131
|
};
|
|
132
|
+
var jsonReplacer = (_key, value) => typeof value === "bigint" ? `${String(value)}n` : value === void 0 ? "undefined" : typeof value === "number" && !Number.isFinite(value) ? String(value) : value;
|
|
166
133
|
var preview = (value) => {
|
|
167
134
|
if (value === void 0) return "undefined";
|
|
168
135
|
if (value instanceof Date) return value.toISOString();
|
|
169
|
-
|
|
136
|
+
if (typeof value === "bigint") return `${String(value)}n`;
|
|
137
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
138
|
+
return String(value);
|
|
139
|
+
}
|
|
140
|
+
const json = JSON.stringify(value, jsonReplacer);
|
|
170
141
|
return json === void 0 ? String(value) : json.length > 60 ? `${json.slice(0, 57)}\u2026` : json;
|
|
171
142
|
};
|
|
172
143
|
var flags = (row) => [
|
|
@@ -193,15 +164,14 @@ var Section = ({
|
|
|
193
164
|
/* @__PURE__ */ jsxRuntime.jsx("p", { style: styles.sectionTitle, children: title }),
|
|
194
165
|
children
|
|
195
166
|
] });
|
|
196
|
-
var
|
|
167
|
+
var OpenPanel = ({
|
|
197
168
|
form,
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
169
|
+
state,
|
|
170
|
+
label,
|
|
171
|
+
held,
|
|
172
|
+
onHold,
|
|
173
|
+
onClose
|
|
201
174
|
}) => {
|
|
202
|
-
const state = useDevtoolsState(form);
|
|
203
|
-
const [open, setOpen] = react.useState(defaultOpen);
|
|
204
|
-
const [held, setHeld] = react.useState(null);
|
|
205
175
|
const rows = leafPaths(state.values).map((path) => {
|
|
206
176
|
const field = form.getFieldState(path);
|
|
207
177
|
return {
|
|
@@ -218,7 +188,7 @@ var DevtoolsPanel = ({
|
|
|
218
188
|
rows.map((row) => row.path)
|
|
219
189
|
);
|
|
220
190
|
const diff = form.diff();
|
|
221
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
191
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.panel, children: [
|
|
222
192
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.header, children: [
|
|
223
193
|
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: label }),
|
|
224
194
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { style: styles.muted, children: [
|
|
@@ -227,15 +197,7 @@ var DevtoolsPanel = ({
|
|
|
227
197
|
state.isValidatingForm ? " \xB7 validating" : "",
|
|
228
198
|
state.submitCount > 0 ? ` \xB7 submits ${state.submitCount}` : ""
|
|
229
199
|
] }),
|
|
230
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
231
|
-
"button",
|
|
232
|
-
{
|
|
233
|
-
type: "button",
|
|
234
|
-
style: styles.button,
|
|
235
|
-
onClick: () => setOpen(false),
|
|
236
|
-
children: "close"
|
|
237
|
-
}
|
|
238
|
-
)
|
|
200
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: styles.button, onClick: onClose, children: "close" })
|
|
239
201
|
] }),
|
|
240
202
|
/* @__PURE__ */ jsxRuntime.jsx(Section, { title: `fields (${rows.length})`, children: /* @__PURE__ */ jsxRuntime.jsxs("table", { style: styles.table, children: [
|
|
241
203
|
/* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { children: [
|
|
@@ -274,17 +236,17 @@ var DevtoolsPanel = ({
|
|
|
274
236
|
" ",
|
|
275
237
|
/* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.error, children: messages.join(", ") }),
|
|
276
238
|
" ",
|
|
277
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.muted, children:
|
|
239
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.muted, children: Object.hasOwn(state.schemaErrors, key) ? "(behind a schema error)" : "(showing)" })
|
|
278
240
|
] }, key))
|
|
279
241
|
] }),
|
|
280
|
-
/* @__PURE__ */ jsxRuntime.jsx(Section, { title: "diff vs initial", children: /* @__PURE__ */ jsxRuntime.jsx("pre", { style: styles.pre, children: Object.keys(diff).length === 0 ? "clean" : JSON.stringify(diff,
|
|
242
|
+
/* @__PURE__ */ jsxRuntime.jsx(Section, { title: "diff vs initial", children: /* @__PURE__ */ jsxRuntime.jsx("pre", { style: styles.pre, children: Object.keys(diff).length === 0 ? "clean" : JSON.stringify(diff, jsonReplacer, 2) }) }),
|
|
281
243
|
/* @__PURE__ */ jsxRuntime.jsxs(Section, { title: "time travel", children: [
|
|
282
244
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
283
245
|
"button",
|
|
284
246
|
{
|
|
285
247
|
type: "button",
|
|
286
248
|
style: styles.button,
|
|
287
|
-
onClick: () =>
|
|
249
|
+
onClick: () => onHold(form.snapshot()),
|
|
288
250
|
children: "snapshot"
|
|
289
251
|
}
|
|
290
252
|
),
|
|
@@ -304,7 +266,29 @@ var DevtoolsPanel = ({
|
|
|
304
266
|
" ",
|
|
305
267
|
/* @__PURE__ */ jsxRuntime.jsx("span", { style: styles.muted, children: held === null ? "nothing held" : "snapshot held" })
|
|
306
268
|
] })
|
|
307
|
-
] })
|
|
269
|
+
] });
|
|
270
|
+
};
|
|
271
|
+
var DevtoolsPanel = ({
|
|
272
|
+
form,
|
|
273
|
+
position = "bottom-right",
|
|
274
|
+
defaultOpen = false,
|
|
275
|
+
label = "formstand"
|
|
276
|
+
}) => {
|
|
277
|
+
const state = useDevtoolsState(form);
|
|
278
|
+
const [open, setOpen] = react.useState(defaultOpen);
|
|
279
|
+
const [held, setHeld] = react.useState(null);
|
|
280
|
+
const heldForThisForm = held !== null && held.form === form ? held.snapshot : null;
|
|
281
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...styles.root, ...anchorFor(position) }, children: open ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
282
|
+
OpenPanel,
|
|
283
|
+
{
|
|
284
|
+
form,
|
|
285
|
+
state,
|
|
286
|
+
label,
|
|
287
|
+
held: heldForThisForm,
|
|
288
|
+
onHold: (snapshot) => setHeld({ form, snapshot }),
|
|
289
|
+
onClose: () => setOpen(false)
|
|
290
|
+
}
|
|
291
|
+
) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
308
292
|
"button",
|
|
309
293
|
{
|
|
310
294
|
type: "button",
|