formstand 0.9.0 → 0.11.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/README.md CHANGED
@@ -85,10 +85,10 @@ const form = createForm(schema, {
85
85
  | `setError(path, errors)` / `setErrors(map)` / `clearErrors(path?)` | the app-owned **server error channel** (`state.serverErrors`) — validation never touches it; `setError` accepts a single string or an array; `clearErrors(path)` clears both channels at the path and its descendants (`clearErrors("")` clears just the root entry; `clearErrors()` clears everything); `setErrors` replaces the whole server channel |
86
86
  | `setMode` / `setReValidateMode` | switch modes at runtime |
87
87
  | `reset(nextInitial?, options?)` | reset to initial; optional partial overrides (shallow-merged for record roots, replaced wholesale otherwise) and `{ keepErrors, keepTouched, keepSubmitCount }` (no `keepDirty` — dirtiness derives from values vs initial, and reset makes them equal) |
88
- | `resetField(path)` | reset one field to its initial value, clearing its (and descendants') error/touched state (dirtiness clears by definition — the value now equals initial) |
88
+ | `resetField(path)` | reset one field to its initial value, clearing its (and descendants') error/touched state (dirtiness clears by definition — the value now equals initial); a path with no initial counterpart (an appended array row) keeps its value and only clears field state |
89
89
  | `adoptValues(values)` | mid-session rebase: replaces `values` + `initialValues` and clears `errors` and in-flight validation flags (`isValidating`/`isValidatingForm` — the rebase disowns in-flight passes), but **preserves** interaction state (`touched`, `submitCount`, `isSubmitting`, `mode`). Use `reset()` for a full wipe |
90
90
  | `updateState(updater)` | atomic multi-field patch; `errors` is derived from `schemaErrors`/`serverErrors`, so patch the channels — the patch type omits `errors` entirely, and a plain-JS `errors` patch is warned about and ignored |
91
- | `validate()` / `validateField(path)` / `validateFields(paths)` | sync validation; on an async schema they transparently start the async pass instead (`validate`/`validateField` return `{ kind: "pending", promise }`, `validateFields` returns the `Promise<boolean>` itself) |
91
+ | `validate()` / `validateField(path)` / `validateFields(paths)` | sync validation; on an async schema all three transparently start the async pass instead and return `{ kind: "pending", promise }` |
92
92
  | `validateAsync()` / `validateFieldAsync(path)` / `validateFieldsAsync(paths)` | async; supports `async .refine` |
93
93
  | `submit(onValid, onInvalid?, { force? })` → `Promise<SubmitResult>` | full submit flow; resolves `{ kind: "valid", data }`, `{ kind: "invalid", errors }` (errored fields are also marked touched), `{ kind: "skipped" }` when another submit is in flight, or `{ kind: "error", error }` when `onValid` throws/rejects — submit resolves instead of rejecting, so `handleSubmit` never produces an unhandled rejection from an `onValid` throw |
94
94
  | `handleSubmit(onValid, onInvalid?)(event?)` | event handler wrapper that calls `preventDefault` |
@@ -108,7 +108,7 @@ const form = createForm(schema, {
108
108
  - `mode: "all"`: validate on every change and blur.
109
109
  - `reValidateMode` (default `"onChange"`) kicks in once `submitCount > 0`.
110
110
 
111
- For schemas with `async .refine`, sync `validate()` / `validateField()` / `validateFields()` no longer throw: they detect the async requirement, start the async pass themselves, and hand you the in-flight work (`{ kind: "pending", promise }` from `validate`/`validateField`; the `Promise<boolean>` itself from `validateFields`). If the *field being validated* is itself sync, `validateField` still settles synchronously even when other fields carry async refines — field validation parses just that field's subschema when it can (see below).
111
+ For schemas with `async .refine`, sync `validate()` / `validateField()` / `validateFields()` don't throw: they detect the async requirement, start the async pass themselves, and hand you the in-flight work as `{ kind: "pending", promise }` (never a bare truthy `Promise` — a boolean gate can't silently pass). The requirement is also remembered per scope, so later sync calls skip straight to the async pass instead of re-paying a doomed sync parse per keystroke. If the *field being validated* is itself sync, `validateField` still settles synchronously even when other fields carry async refines — field validation parses just that field's subschema when it can (see below).
112
112
 
113
113
  ## React hooks
114
114
 
@@ -165,12 +165,13 @@ users.insert(i, item);
165
165
  users.error; // array-level error (e.g. min(1))
166
166
  ```
167
167
 
168
- `fields` IDs are reconciled against item identity each render, so a row's `id`
169
- stays glued to its item across reorders, inserts, and removes — including
170
- mutations made outside this hook (`form.arrayMove`, `setValue`, `restore`, or a
171
- second `useFieldArray` on the same path). Editing a field keeps its row's `id`
172
- (the row updates instead of remounting); a genuinely new item gets a fresh `id`.
173
- IDs reset when the hook's `path` changes.
168
+ `fields` IDs are shared per `(form, path)` — every hook on the same array sees
169
+ the same ids. Array ops (hook-issued or imperative `form.arrayMove` etc.)
170
+ record their exact index mapping and id derivation replays it, so even rows
171
+ with equal values keep the right ids through ops; whole-array writes
172
+ (`setValue`, `restore`) reconcile by item identity with a positional fallback,
173
+ so editing a field keeps its row's `id` (the row updates instead of
174
+ remounting). A genuinely new item gets a fresh `id`.
174
175
 
175
176
  ### `useFormSelector(form, selector)` / `useFormSelectorShallow(form, selector)`
176
177
 
@@ -347,9 +348,12 @@ decided by the **existing container**: arrays take numeric segments as indices,
347
348
  plain objects take any segment as a string key — so a `z.record` keyed `"0"`
348
349
  reads and writes the record key instead of silently becoming an array. Only
349
350
  when the container doesn't exist yet does the segment type pick what's created
350
- (numeric → array, otherwise object). Two limitations: keys containing `.` are
351
- not addressable, and array writes beyond index 100 000 are refused (a typo'd
352
- index must not allocate gigabytes).
351
+ (numeric → array, otherwise object). Three limitations: keys containing `.`
352
+ are not addressable; array writes beyond index 100 000 are refused (a typo'd
353
+ index must not allocate gigabytes); and the typed `FieldPath` union stops at
354
+ 9 segments by default — the runtime walks any depth, and `createForm`'s
355
+ type-level `pathDepth` option widens the union per form (a deliberate
356
+ compile-time trade; see the [typed paths guide](https://scrumrot.github.io/formstand/guide/typed-paths)).
353
357
 
354
358
  ## Subscriptions (non-React)
355
359
 
@@ -408,7 +412,7 @@ Note `submit` still proceeds when the schema is valid even if server errors are
408
412
 
409
413
  ## Field arrays with nested arrays
410
414
 
411
- Both work — `arrayPush("users.0.tags", tag)` mutates the inner array; outer reorders correctly re-key meta for all nested paths. `useFieldArray`'s stable IDs reset when the hook's path changes (so an inner field array inside a reordered outer item gets fresh IDs).
415
+ Both work — `arrayPush("users.0.tags", tag)` mutates the inner array; outer reorders correctly re-key meta for all nested paths. Inner row ids live per `(form, path)` and reconcile by value when the outer index re-seats a different item at the path — pass a `key` from the outer `fields` so each row's whole subtree moves with its item.
412
416
 
413
417
  ## Optimistic UI
414
418