react-f0rm 1.1.0 → 1.2.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 +152 -32
- package/dist/devtools/index.cjs.js +1 -1
- package/dist/devtools/index.cjs.js.map +1 -1
- package/dist/devtools/index.d.ts +2 -2
- package/dist/devtools/index.mjs +1 -1
- package/dist/devtools/index.mjs.map +1 -1
- package/dist/errors-CxSjrWJO.cjs.js +2 -0
- package/dist/errors-CxSjrWJO.cjs.js.map +1 -0
- package/dist/errors-CzWtwjO0.mjs +2 -0
- package/dist/errors-CzWtwjO0.mjs.map +1 -0
- package/dist/form-CvmWHUrd.d.ts +423 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +786 -102
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +801 -354
- package/dist/index.umd.js.map +1 -1
- package/dist/index.umd.min.js +2 -2
- package/dist/index.umd.min.js.map +1 -1
- package/dist/persist.cjs.js +2 -0
- package/dist/persist.cjs.js.map +1 -0
- package/dist/persist.d.ts +49 -0
- package/dist/persist.mjs +2 -0
- package/dist/persist.mjs.map +1 -0
- package/dist/resolvers/standard-schema.cjs.js +1 -1
- package/dist/resolvers/standard-schema.cjs.js.map +1 -1
- package/dist/resolvers/standard-schema.d.ts +7 -5
- package/dist/resolvers/standard-schema.mjs +1 -1
- package/dist/resolvers/standard-schema.mjs.map +1 -1
- package/dist/resolvers/yup.cjs.js +1 -1
- package/dist/resolvers/yup.cjs.js.map +1 -1
- package/dist/resolvers/yup.d.ts +1 -1
- package/dist/resolvers/yup.mjs +1 -1
- package/dist/resolvers/yup.mjs.map +1 -1
- package/dist/resolvers/zod.cjs.js +1 -1
- package/dist/resolvers/zod.cjs.js.map +1 -1
- package/dist/resolvers/zod.d.ts +1 -1
- package/dist/resolvers/zod.mjs +1 -1
- package/dist/resolvers/zod.mjs.map +1 -1
- package/dist/server/index.cjs.js +2 -0
- package/dist/server/index.cjs.js.map +1 -0
- package/dist/server/index.d.ts +77 -0
- package/dist/server/index.mjs +2 -0
- package/dist/server/index.mjs.map +1 -0
- package/dist/validate-B1Gdjeaq.mjs +2 -0
- package/dist/validate-B1Gdjeaq.mjs.map +1 -0
- package/dist/validate-CUmNZqg6.d.ts +238 -0
- package/dist/validate-DAfz8Nbb.cjs.js +2 -0
- package/dist/validate-DAfz8Nbb.cjs.js.map +1 -0
- package/dist/values-B1IV-6V4.mjs +2 -0
- package/dist/values-B1IV-6V4.mjs.map +1 -0
- package/dist/values-CDNAYEOB.cjs.js +2 -0
- package/dist/values-CDNAYEOB.cjs.js.map +1 -0
- package/package.json +71 -24
- package/dist/form-BGWPwts2.mjs +0 -2
- package/dist/form-BGWPwts2.mjs.map +0 -1
- package/dist/form-BiDaJLjD.d.ts +0 -826
- package/dist/form-DwuY91QB.cjs.js +0 -2
- package/dist/form-DwuY91QB.cjs.js.map +0 -1
- package/dist/validate-2XUilILy.d.ts +0 -22
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var e=require("@for-fun/event-emitter"),t=require("./values-CDNAYEOB.cjs.js"),r=require("./errors-CxSjrWJO.cjs.js");function n(e,t){return e.startsWith(`${t.slice(0,-1)},`)}function c(t,c){const{name:i,event:o="change",scope:s="branch",callback:a}=c;if(void 0===i)return e.on(t.emitter,o,a);const u=function(e){return Array.isArray(e)&&e.every(e=>"number"!=typeof e)}(i)?i:[i],l=u.map(c=>{const i=r.create(c);return"errors"===o||"touched"===o?function(t,r,n,c){return e.on(t,r,e=>{void 0!==e&&e.key!==n||c()})}(t.emitter,o,i.key,a):function(t,r,c,i,o){const{key:s}=c;return e.on(t,r,e=>{(void 0===e||e.key===s||n(s,e.key)||"branch"===i&&n(e.key,s))&&o()})}(t.emitter,o,i,s,a)});return 1===l.length?l[0]:()=>l.forEach(e=>e())}exports.persistForm=function(e,r){const{key:n}=r,i=r.storage??("undefined"!=typeof localStorage?localStorage:void 0);if(!i)return()=>{};const o=i.getItem(n);if(null!==o)try{const n=r.deserialize??(e=>JSON.parse(e));t.setInitialValues(e,n(o))}catch{}return c(e,{event:"change",callback:()=>{const c=r.serialize?r.serialize(t.getValues(e)):t.getValues(e);try{i.setItem(n,JSON.stringify(c))}catch{}}})};
|
|
2
|
+
//# sourceMappingURL=persist.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persist.cjs.js","sources":["../src/subscribe.ts","../src/persist.ts"],"sourcesContent":["import {on} from '@for-fun/event-emitter';\nimport type {EventEmitter} from '@for-fun/event-emitter';\nimport createPath from './path';\nimport type {Name, Path} from './path';\nimport type {Form, FormEvents} from './form';\n\n/** Subscription granularity for {@link onPathEvent}.\n * - `'leaf'`: the subscriber reads exactly one key ({@link\n * useValueByPath}); only writes at that key or above it can change what\n * it reads.\n * - `'branch'`: the subscriber aggregates a whole subtree below a key\n * ({@link useFieldArray}); descendant writes matter too. */\nexport type WatchScope = 'leaf' | 'branch';\n\n/**\n * Is `key` a strict descendant of `ancestorKey`?\n *\n * Keys are JSON.stringify'd segment arrays ('[\"a\",\"b\"]'), so a descendant\n * key is the ancestor key minus its closing ']' followed by a ','\n * ('[\"a\",\"b\",'). The ',' separator is mandatory: a plain prefix match\n * would let the sibling '[\"tagsX\"]' pass as a descendant of '[\"tags\"]'.\n */\nfunction isDescendant(key: string, ancestorKey: string): boolean {\n return key.startsWith(`${ancestorKey.slice(0, -1)},`);\n}\n\n/**\n * Subscribe to `event`, invoking `cb` only when the emitted path is\n * relevant to `path`.\n *\n * Payload-less broadcasts (reset, setInitialValues) always invoke `cb` --\n * they are global syncs and the correctness fallback. (removeFieldByPath\n * emits with its path: its mutations are bounded to that key, so the path\n * matching below is exact.) When the emit carries a path P:\n * - `'leaf'`: P.key equals `path.key` or is one of its ancestors -- a leaf\n * read falls back to ancestor values (getValueByPath), so ancestor\n * writes must invalidate, while sibling and descendant writes cannot\n * change what the leaf reads.\n * - `'branch'`: `'leaf'` semantics plus P.key being a descendant of\n * `path.key` -- changed descendants re-aggregate the subtree.\n *\n * @param emitter emitter to subscribe to\n * @param event event name\n * @param path the watched path\n * @param scope which writes around `path` are relevant\n * @param cb listener, invoked with no arguments\n * @return unsubscribe function\n */\nexport function onPathEvent(\n emitter: EventEmitter<FormEvents>,\n event: SubscribeEvent,\n path: Path,\n scope: WatchScope,\n cb: () => void\n): () => void {\n const {key} = path;\n return on(emitter, event, (changed?: Path) => {\n if (\n changed === undefined ||\n changed.key === key ||\n isDescendant(key, changed.key) ||\n (scope === 'branch' && isDescendant(changed.key, key))\n ) {\n cb();\n }\n });\n}\n\n/**\n * Subscribe to `event`, invoking `cb` only when the emitted path's key is\n * exactly `key` (or the emit carries no payload -- a global sync).\n *\n * For state stored per exact key (errors, touched) no ancestor or\n * descendant matching is wanted: another field's key must not wake this\n * subscriber.\n *\n * @param emitter emitter to subscribe to\n * @param event event name\n * @param key exact path key to match\n * @param cb listener, invoked with no arguments\n * @return unsubscribe function\n */\nexport function onKeyEvent(\n emitter: EventEmitter<FormEvents>,\n event: SubscribeEvent,\n key: string,\n cb: () => void\n): () => void {\n return on(emitter, event, (changed?: Path) => {\n if (changed === undefined || changed.key === key) cb();\n });\n}\n\n/** Events {@link subscribe} can watch. `'errors'` and `'touched'` are\n * stored per exact key, so they match exact keys ({@link onKeyEvent});\n * `'change'`, `'validating'`, `'submitting'`, `'submitCount'`,\n * `'disabled'` and `'submitSuccessful'` go through\n * {@link onPathEvent}. `'validating'` carries paths (one per async\n * validator round) and matches by path exactly like `'change'`;\n * `'submitting'`, `'submitCount'`, `'disabled'` and\n * `'submitSuccessful'` are payload-less broadcasts, so `name` never\n * narrows them — every subscriber hears every emission. */\nexport type SubscribeEvent =\n | 'change'\n | 'errors'\n | 'touched'\n | 'validating'\n | 'submitting'\n | 'submitCount'\n | 'submitSuccessful'\n | 'disabled'\n | 'loading';\n\n/** Options accepted by {@link subscribe}. */\nexport type SubscribeOptions = {\n /** Path (or list of paths) to watch. Omit to receive every emission of\n * `event`, payload-less broadcasts included. A single segments path\n * (`['tags', 0]`) and a list of names (`['tags', 'user.name']`) are told\n * apart by the same rule `trigger` uses: only a segments path can hold\n * a number. */\n name?: Name | Name[];\n /** Event to watch. Defaults to `'change'`. */\n event?: SubscribeEvent;\n /** Which writes around `name` are relevant — `'leaf'` or `'branch'`.\n * Only meaningful for the path-carrying events `'change'` and\n * `'validating'`: `'errors'`/`'touched'` match exact keys and\n * `'submitting'`/`'submitCount'`/`'disabled'`/`'submitSuccessful'`\n * are payload-less. Defaults to `'branch'` — the intuitive linkage\n * semantics, where subscribing to `'tags'` means the whole branch. */\n scope?: WatchScope;\n /** Invoked with no arguments after each matching emission. Read fresh\n * state through the `get*` readers inside it. */\n callback: () => void;\n};\n\n/** Is `name` a list of names rather than one segments path? Numbers only\n * occur inside a segments path (`['a', 0]`), never as standalone names —\n * the same disambiguation `trigger` applies to its name argument. */\nfunction isNameList(name: Name | Name[]): name is Name[] {\n return (\n Array.isArray(name) &&\n (name as (number | unknown)[]).every(part => typeof part !== 'number')\n );\n}\n\n/**\n * Subscribe to form events imperatively — the non-render counterpart of\n * the `use*` hooks: linkages and side effects (province changed → clear\n * city, autosave, analytics) run without mounting a watching component.\n *\n * Without `name`, `callback` fires on every `event` emission, payload-less\n * broadcasts (reset, setInitialValues) included. With `name`, matching\n * follows the event's shape: `'errors'`/`'touched'` match the exact key\n * ({@link onKeyEvent}) — another field's error never wakes this\n * subscriber — while `'change'`/`'validating'`/`'submitting'`/\n * `'submitCount'`/`'disabled'`/`'submitSuccessful'` go through\n * {@link onPathEvent}, so the default `'branch'` scope wakes a `'tags'`\n * subscriber when any `tags.*` descendant is written. `'validating'`\n * carries a path per validator round and narrows by path like\n * `'change'`; `'disabled'`/`'submitSuccessful'` (like `'submitting'`)\n * are payload-less broadcasts that every named subscriber receives. A\n * `name` array builds one subscription per path and the returned\n * function unsubscribes them all.\n *\n * @param form the form to watch\n * @param options event, name(s), scope and callback\n * @return unsubscribe function\n */\nexport function subscribe(form: Form, options: SubscribeOptions): () => void {\n const {name, event = 'change', scope = 'branch', callback} = options;\n if (name === undefined) return on(form.emitter, event, callback);\n const names = isNameList(name) ? name : [name];\n const unsubscribes = names.map(one => {\n const path = createPath(one);\n return event === 'errors' || event === 'touched'\n ? onKeyEvent(form.emitter, event, path.key, callback)\n : onPathEvent(form.emitter, event, path, scope, callback);\n });\n return unsubscribes.length === 1\n ? unsubscribes[0]\n : () => unsubscribes.forEach(unsubscribe => unsubscribe());\n}\n","/**\n * Local persistence — `react-f0rm/persist`.\n *\n * Ships separately from the main entry (like the resolvers and devtools)\n * so persistence code never lands in bundles that do not use it, and\n * imports nothing but the headless core: the module is React-free and\n * works with any form instance, hook-created or not.\n */\nimport {getValues, setInitialValues} from './form';\nimport type {Form} from './form';\nimport {subscribe} from './subscribe';\n\n/** Storage surface {@link persistForm} needs — the browser's\n * `localStorage`/`sessionStorage` satisfy it as-is; pass a custom object\n * (or a framework adapter) for tests, SSR or non-DOM runtimes. */\nexport type PersistStorage = {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n};\n\n/** Options for {@link persistForm}. */\nexport type PersistOptions = {\n /** Storage key the form snapshot lives under. */\n key: string;\n /** Where to read/write. Defaults to `window.localStorage` when it\n * exists; without one (SSR, Node) persistence becomes a silent no-op\n * and the returned unsubscribe is a no-op too. */\n storage?: PersistStorage;\n /** Transform values before serialization — e.g. strip File/FileList\n * entries (JSON.stringify drops their contents anyway) or pick a\n * subset. Defaults to identity. */\n serialize?: (values: Record<string, any>) => Record<string, any>;\n /** Parse the stored string back into values. Defaults to JSON.parse;\n * a throwing parse (corrupted/foreign payload) is swallowed and the\n * stored snapshot ignored. */\n deserialize?: (raw: string) => Record<string, any>;\n};\n\n/**\n * Persist a form's values to a storage backend (localStorage by default)\n * and hydrate them back on the next session.\n *\n * Call once at form creation, before user interaction: hydration applies\n * the stored snapshot through {@link setInitialValues} — the restored\n * values become the baseline, so the form starts clean, not dirty. Every\n * 'change' event re-writes the snapshot (a keystroke writes the whole\n * values tree, JSON-stringified; browsers handle that fine for typical\n * form sizes, and `serialize` can shrink it).\n *\n * Returns the unsubscribe function — call it to stop persisting (the\n * stored snapshot stays).\n *\n * @param form form instance to persist\n * @param options storage key, backend and (de)serialization hooks\n * @return unsubscribe function (no-op when no storage exists)\n */\nexport function persistForm(form: Form, options: PersistOptions): () => void {\n const {key} = options;\n const storage: PersistStorage | undefined =\n options.storage ??\n (typeof localStorage !== 'undefined' ? localStorage : undefined);\n if (!storage) return () => {};\n\n // Hydrate before subscribing: the restored baseline must exist before\n // the first 'change' write, so the initial snapshot persisted equals\n // the snapshot the user sees.\n const raw = storage.getItem(key);\n if (raw !== null) {\n try {\n const parse = options.deserialize ?? ((s: string) => JSON.parse(s));\n setInitialValues(form, parse(raw));\n } catch {\n // Corrupted or foreign payload under our key: ignore it, start from\n // the form's own initialValues.\n }\n }\n\n return subscribe(form, {\n event: 'change',\n callback: () => {\n const values = options.serialize\n ? options.serialize(getValues(form))\n : getValues(form);\n try {\n storage.setItem(key, JSON.stringify(values));\n } catch {\n // Quota exceeded / storage disabled: persistence is best-effort.\n }\n }\n });\n}\n"],"names":["isDescendant","key","ancestorKey","startsWith","slice","subscribe","form","options","name","event","scope","callback","on","emitter","names","Array","isArray","every","part","isNameList","unsubscribes","map","one","path","createPath","cb","changed","onKeyEvent","onPathEvent","length","forEach","unsubscribe","storage","localStorage","raw","getItem","parse","deserialize","s","JSON","setInitialValues","values","serialize","getValues","setItem","stringify"],"mappings":"iIAsBA,SAASA,EAAaC,EAAaC,GACjC,OAAOD,EAAIE,WAAW,GAAGD,EAAYE,MAAM,GAAG,MAChD,CAgJO,SAASC,EAAUC,EAAYC,GACpC,MAAMC,KAACA,EAAAC,MAAMA,EAAQ,eAAUC,EAAQ,SAAAC,SAAUA,GAAYJ,EAC7D,QAAa,IAATC,EAAoB,OAAOI,EAAAA,GAAGN,EAAKO,QAASJ,EAAOE,GACvD,MAAMG,EAjCR,SAAoBN,GAClB,OACEO,MAAMC,QAAQR,IACbA,EAA8BS,MAAMC,GAAwB,iBAATA,EAExD,CA4BgBC,CAAWX,GAAQA,EAAO,CAACA,GACnCY,EAAeN,EAAMO,IAAIC,IAC7B,MAAMC,EAAOC,EAAAA,OAAWF,GACxB,MAAiB,WAAVb,GAAgC,YAAVA,EA5F1B,SACLI,EACAJ,EACAR,EACAwB,GAEA,OAAOb,KAAGC,EAASJ,EAAQiB,SACT,IAAZA,GAAyBA,EAAQzB,MAAQA,GAAKwB,KAEtD,CAoFQE,CAAWrB,EAAKO,QAASJ,EAAOc,EAAKtB,IAAKU,GA/H3C,SACLE,EACAJ,EACAc,EACAb,EACAe,GAEA,MAAMxB,IAACA,GAAOsB,EACd,OAAOX,KAAGC,EAASJ,EAAQiB,UAEX,IAAZA,GACAA,EAAQzB,MAAQA,GAChBD,EAAaC,EAAKyB,EAAQzB,MACf,WAAVS,GAAsBV,EAAa0B,EAAQzB,IAAKA,KAEjDwB,KAGN,CA8GQG,CAAYtB,EAAKO,QAASJ,EAAOc,EAAMb,EAAOC,KAEpD,OAA+B,IAAxBS,EAAaS,OAChBT,EAAa,GACb,IAAMA,EAAaU,QAAQC,GAAeA,IAChD,qBC7HO,SAAqBzB,EAAYC,GACtC,MAAMN,IAACA,GAAOM,EACRyB,EACJzB,EAAQyB,UACiB,oBAAjBC,aAA+BA,qBACzC,IAAKD,EAAS,MAAO,OAKrB,MAAME,EAAMF,EAAQG,QAAQlC,GAC5B,GAAY,OAARiC,EACF,IACE,MAAME,EAAQ7B,EAAQ8B,aAAA,CAAiBC,GAAcC,KAAKH,MAAME,IAChEE,EAAAA,iBAAiBlC,EAAM8B,EAAMF,GAC/B,CAAA,MAGA,CAGF,OAAO7B,EAAUC,EAAM,CACrBG,MAAO,SACPE,SAAU,KACR,MAAM8B,EAASlC,EAAQmC,UACnBnC,EAAQmC,UAAUC,EAAAA,UAAUrC,IAC5BqC,EAAAA,UAAUrC,GACd,IACE0B,EAAQY,QAAQ3C,EAAKsC,KAAKM,UAAUJ,GACtC,CAAA,MAEA,IAGN"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { FormInstance as Form } from './index.js';
|
|
2
|
+
import '@for-fun/event-emitter';
|
|
3
|
+
|
|
4
|
+
/** Storage surface {@link persistForm} needs — the browser's
|
|
5
|
+
* `localStorage`/`sessionStorage` satisfy it as-is; pass a custom object
|
|
6
|
+
* (or a framework adapter) for tests, SSR or non-DOM runtimes. */
|
|
7
|
+
type PersistStorage = {
|
|
8
|
+
getItem(key: string): string | null;
|
|
9
|
+
setItem(key: string, value: string): void;
|
|
10
|
+
};
|
|
11
|
+
/** Options for {@link persistForm}. */
|
|
12
|
+
type PersistOptions = {
|
|
13
|
+
/** Storage key the form snapshot lives under. */
|
|
14
|
+
key: string;
|
|
15
|
+
/** Where to read/write. Defaults to `window.localStorage` when it
|
|
16
|
+
* exists; without one (SSR, Node) persistence becomes a silent no-op
|
|
17
|
+
* and the returned unsubscribe is a no-op too. */
|
|
18
|
+
storage?: PersistStorage;
|
|
19
|
+
/** Transform values before serialization — e.g. strip File/FileList
|
|
20
|
+
* entries (JSON.stringify drops their contents anyway) or pick a
|
|
21
|
+
* subset. Defaults to identity. */
|
|
22
|
+
serialize?: (values: Record<string, any>) => Record<string, any>;
|
|
23
|
+
/** Parse the stored string back into values. Defaults to JSON.parse;
|
|
24
|
+
* a throwing parse (corrupted/foreign payload) is swallowed and the
|
|
25
|
+
* stored snapshot ignored. */
|
|
26
|
+
deserialize?: (raw: string) => Record<string, any>;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Persist a form's values to a storage backend (localStorage by default)
|
|
30
|
+
* and hydrate them back on the next session.
|
|
31
|
+
*
|
|
32
|
+
* Call once at form creation, before user interaction: hydration applies
|
|
33
|
+
* the stored snapshot through {@link setInitialValues} — the restored
|
|
34
|
+
* values become the baseline, so the form starts clean, not dirty. Every
|
|
35
|
+
* 'change' event re-writes the snapshot (a keystroke writes the whole
|
|
36
|
+
* values tree, JSON-stringified; browsers handle that fine for typical
|
|
37
|
+
* form sizes, and `serialize` can shrink it).
|
|
38
|
+
*
|
|
39
|
+
* Returns the unsubscribe function — call it to stop persisting (the
|
|
40
|
+
* stored snapshot stays).
|
|
41
|
+
*
|
|
42
|
+
* @param form form instance to persist
|
|
43
|
+
* @param options storage key, backend and (de)serialization hooks
|
|
44
|
+
* @return unsubscribe function (no-op when no storage exists)
|
|
45
|
+
*/
|
|
46
|
+
declare function persistForm(form: Form, options: PersistOptions): () => void;
|
|
47
|
+
|
|
48
|
+
export { persistForm };
|
|
49
|
+
export type { PersistOptions, PersistStorage };
|
package/dist/persist.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{on as e}from"@for-fun/event-emitter";import{s as t,g as r}from"./values-B1IV-6V4.mjs";import{c as n}from"./errors-CzWtwjO0.mjs";function o(e,t){return e.startsWith(`${t.slice(0,-1)},`)}function c(t,r){const{name:c,event:i="change",scope:a="branch",callback:s}=r;if(void 0===c)return e(t.emitter,i,s);const u=function(e){return Array.isArray(e)&&e.every(e=>"number"!=typeof e)}(c)?c:[c],f=u.map(r=>{const c=n(r);return"errors"===i||"touched"===i?function(t,r,n,o){return e(t,r,e=>{void 0!==e&&e.key!==n||o()})}(t.emitter,i,c.key,s):function(t,r,n,c,i){const{key:a}=n;return e(t,r,e=>{(void 0===e||e.key===a||o(a,e.key)||"branch"===c&&o(e.key,a))&&i()})}(t.emitter,i,c,a,s)});return 1===f.length?f[0]:()=>f.forEach(e=>e())}function i(e,n){const{key:o}=n,i=n.storage??("undefined"!=typeof localStorage?localStorage:void 0);if(!i)return()=>{};const a=i.getItem(o);if(null!==a)try{const r=n.deserialize??(e=>JSON.parse(e));t(e,r(a))}catch{}return c(e,{event:"change",callback:()=>{const t=n.serialize?n.serialize(r(e)):r(e);try{i.setItem(o,JSON.stringify(t))}catch{}}})}export{i as persistForm};
|
|
2
|
+
//# sourceMappingURL=persist.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persist.mjs","sources":["../src/subscribe.ts","../src/persist.ts"],"sourcesContent":["import {on} from '@for-fun/event-emitter';\nimport type {EventEmitter} from '@for-fun/event-emitter';\nimport createPath from './path';\nimport type {Name, Path} from './path';\nimport type {Form, FormEvents} from './form';\n\n/** Subscription granularity for {@link onPathEvent}.\n * - `'leaf'`: the subscriber reads exactly one key ({@link\n * useValueByPath}); only writes at that key or above it can change what\n * it reads.\n * - `'branch'`: the subscriber aggregates a whole subtree below a key\n * ({@link useFieldArray}); descendant writes matter too. */\nexport type WatchScope = 'leaf' | 'branch';\n\n/**\n * Is `key` a strict descendant of `ancestorKey`?\n *\n * Keys are JSON.stringify'd segment arrays ('[\"a\",\"b\"]'), so a descendant\n * key is the ancestor key minus its closing ']' followed by a ','\n * ('[\"a\",\"b\",'). The ',' separator is mandatory: a plain prefix match\n * would let the sibling '[\"tagsX\"]' pass as a descendant of '[\"tags\"]'.\n */\nfunction isDescendant(key: string, ancestorKey: string): boolean {\n return key.startsWith(`${ancestorKey.slice(0, -1)},`);\n}\n\n/**\n * Subscribe to `event`, invoking `cb` only when the emitted path is\n * relevant to `path`.\n *\n * Payload-less broadcasts (reset, setInitialValues) always invoke `cb` --\n * they are global syncs and the correctness fallback. (removeFieldByPath\n * emits with its path: its mutations are bounded to that key, so the path\n * matching below is exact.) When the emit carries a path P:\n * - `'leaf'`: P.key equals `path.key` or is one of its ancestors -- a leaf\n * read falls back to ancestor values (getValueByPath), so ancestor\n * writes must invalidate, while sibling and descendant writes cannot\n * change what the leaf reads.\n * - `'branch'`: `'leaf'` semantics plus P.key being a descendant of\n * `path.key` -- changed descendants re-aggregate the subtree.\n *\n * @param emitter emitter to subscribe to\n * @param event event name\n * @param path the watched path\n * @param scope which writes around `path` are relevant\n * @param cb listener, invoked with no arguments\n * @return unsubscribe function\n */\nexport function onPathEvent(\n emitter: EventEmitter<FormEvents>,\n event: SubscribeEvent,\n path: Path,\n scope: WatchScope,\n cb: () => void\n): () => void {\n const {key} = path;\n return on(emitter, event, (changed?: Path) => {\n if (\n changed === undefined ||\n changed.key === key ||\n isDescendant(key, changed.key) ||\n (scope === 'branch' && isDescendant(changed.key, key))\n ) {\n cb();\n }\n });\n}\n\n/**\n * Subscribe to `event`, invoking `cb` only when the emitted path's key is\n * exactly `key` (or the emit carries no payload -- a global sync).\n *\n * For state stored per exact key (errors, touched) no ancestor or\n * descendant matching is wanted: another field's key must not wake this\n * subscriber.\n *\n * @param emitter emitter to subscribe to\n * @param event event name\n * @param key exact path key to match\n * @param cb listener, invoked with no arguments\n * @return unsubscribe function\n */\nexport function onKeyEvent(\n emitter: EventEmitter<FormEvents>,\n event: SubscribeEvent,\n key: string,\n cb: () => void\n): () => void {\n return on(emitter, event, (changed?: Path) => {\n if (changed === undefined || changed.key === key) cb();\n });\n}\n\n/** Events {@link subscribe} can watch. `'errors'` and `'touched'` are\n * stored per exact key, so they match exact keys ({@link onKeyEvent});\n * `'change'`, `'validating'`, `'submitting'`, `'submitCount'`,\n * `'disabled'` and `'submitSuccessful'` go through\n * {@link onPathEvent}. `'validating'` carries paths (one per async\n * validator round) and matches by path exactly like `'change'`;\n * `'submitting'`, `'submitCount'`, `'disabled'` and\n * `'submitSuccessful'` are payload-less broadcasts, so `name` never\n * narrows them — every subscriber hears every emission. */\nexport type SubscribeEvent =\n | 'change'\n | 'errors'\n | 'touched'\n | 'validating'\n | 'submitting'\n | 'submitCount'\n | 'submitSuccessful'\n | 'disabled'\n | 'loading';\n\n/** Options accepted by {@link subscribe}. */\nexport type SubscribeOptions = {\n /** Path (or list of paths) to watch. Omit to receive every emission of\n * `event`, payload-less broadcasts included. A single segments path\n * (`['tags', 0]`) and a list of names (`['tags', 'user.name']`) are told\n * apart by the same rule `trigger` uses: only a segments path can hold\n * a number. */\n name?: Name | Name[];\n /** Event to watch. Defaults to `'change'`. */\n event?: SubscribeEvent;\n /** Which writes around `name` are relevant — `'leaf'` or `'branch'`.\n * Only meaningful for the path-carrying events `'change'` and\n * `'validating'`: `'errors'`/`'touched'` match exact keys and\n * `'submitting'`/`'submitCount'`/`'disabled'`/`'submitSuccessful'`\n * are payload-less. Defaults to `'branch'` — the intuitive linkage\n * semantics, where subscribing to `'tags'` means the whole branch. */\n scope?: WatchScope;\n /** Invoked with no arguments after each matching emission. Read fresh\n * state through the `get*` readers inside it. */\n callback: () => void;\n};\n\n/** Is `name` a list of names rather than one segments path? Numbers only\n * occur inside a segments path (`['a', 0]`), never as standalone names —\n * the same disambiguation `trigger` applies to its name argument. */\nfunction isNameList(name: Name | Name[]): name is Name[] {\n return (\n Array.isArray(name) &&\n (name as (number | unknown)[]).every(part => typeof part !== 'number')\n );\n}\n\n/**\n * Subscribe to form events imperatively — the non-render counterpart of\n * the `use*` hooks: linkages and side effects (province changed → clear\n * city, autosave, analytics) run without mounting a watching component.\n *\n * Without `name`, `callback` fires on every `event` emission, payload-less\n * broadcasts (reset, setInitialValues) included. With `name`, matching\n * follows the event's shape: `'errors'`/`'touched'` match the exact key\n * ({@link onKeyEvent}) — another field's error never wakes this\n * subscriber — while `'change'`/`'validating'`/`'submitting'`/\n * `'submitCount'`/`'disabled'`/`'submitSuccessful'` go through\n * {@link onPathEvent}, so the default `'branch'` scope wakes a `'tags'`\n * subscriber when any `tags.*` descendant is written. `'validating'`\n * carries a path per validator round and narrows by path like\n * `'change'`; `'disabled'`/`'submitSuccessful'` (like `'submitting'`)\n * are payload-less broadcasts that every named subscriber receives. A\n * `name` array builds one subscription per path and the returned\n * function unsubscribes them all.\n *\n * @param form the form to watch\n * @param options event, name(s), scope and callback\n * @return unsubscribe function\n */\nexport function subscribe(form: Form, options: SubscribeOptions): () => void {\n const {name, event = 'change', scope = 'branch', callback} = options;\n if (name === undefined) return on(form.emitter, event, callback);\n const names = isNameList(name) ? name : [name];\n const unsubscribes = names.map(one => {\n const path = createPath(one);\n return event === 'errors' || event === 'touched'\n ? onKeyEvent(form.emitter, event, path.key, callback)\n : onPathEvent(form.emitter, event, path, scope, callback);\n });\n return unsubscribes.length === 1\n ? unsubscribes[0]\n : () => unsubscribes.forEach(unsubscribe => unsubscribe());\n}\n","/**\n * Local persistence — `react-f0rm/persist`.\n *\n * Ships separately from the main entry (like the resolvers and devtools)\n * so persistence code never lands in bundles that do not use it, and\n * imports nothing but the headless core: the module is React-free and\n * works with any form instance, hook-created or not.\n */\nimport {getValues, setInitialValues} from './form';\nimport type {Form} from './form';\nimport {subscribe} from './subscribe';\n\n/** Storage surface {@link persistForm} needs — the browser's\n * `localStorage`/`sessionStorage` satisfy it as-is; pass a custom object\n * (or a framework adapter) for tests, SSR or non-DOM runtimes. */\nexport type PersistStorage = {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n};\n\n/** Options for {@link persistForm}. */\nexport type PersistOptions = {\n /** Storage key the form snapshot lives under. */\n key: string;\n /** Where to read/write. Defaults to `window.localStorage` when it\n * exists; without one (SSR, Node) persistence becomes a silent no-op\n * and the returned unsubscribe is a no-op too. */\n storage?: PersistStorage;\n /** Transform values before serialization — e.g. strip File/FileList\n * entries (JSON.stringify drops their contents anyway) or pick a\n * subset. Defaults to identity. */\n serialize?: (values: Record<string, any>) => Record<string, any>;\n /** Parse the stored string back into values. Defaults to JSON.parse;\n * a throwing parse (corrupted/foreign payload) is swallowed and the\n * stored snapshot ignored. */\n deserialize?: (raw: string) => Record<string, any>;\n};\n\n/**\n * Persist a form's values to a storage backend (localStorage by default)\n * and hydrate them back on the next session.\n *\n * Call once at form creation, before user interaction: hydration applies\n * the stored snapshot through {@link setInitialValues} — the restored\n * values become the baseline, so the form starts clean, not dirty. Every\n * 'change' event re-writes the snapshot (a keystroke writes the whole\n * values tree, JSON-stringified; browsers handle that fine for typical\n * form sizes, and `serialize` can shrink it).\n *\n * Returns the unsubscribe function — call it to stop persisting (the\n * stored snapshot stays).\n *\n * @param form form instance to persist\n * @param options storage key, backend and (de)serialization hooks\n * @return unsubscribe function (no-op when no storage exists)\n */\nexport function persistForm(form: Form, options: PersistOptions): () => void {\n const {key} = options;\n const storage: PersistStorage | undefined =\n options.storage ??\n (typeof localStorage !== 'undefined' ? localStorage : undefined);\n if (!storage) return () => {};\n\n // Hydrate before subscribing: the restored baseline must exist before\n // the first 'change' write, so the initial snapshot persisted equals\n // the snapshot the user sees.\n const raw = storage.getItem(key);\n if (raw !== null) {\n try {\n const parse = options.deserialize ?? ((s: string) => JSON.parse(s));\n setInitialValues(form, parse(raw));\n } catch {\n // Corrupted or foreign payload under our key: ignore it, start from\n // the form's own initialValues.\n }\n }\n\n return subscribe(form, {\n event: 'change',\n callback: () => {\n const values = options.serialize\n ? options.serialize(getValues(form))\n : getValues(form);\n try {\n storage.setItem(key, JSON.stringify(values));\n } catch {\n // Quota exceeded / storage disabled: persistence is best-effort.\n }\n }\n });\n}\n"],"names":["isDescendant","key","ancestorKey","startsWith","slice","subscribe","form","options","name","event","scope","callback","on","emitter","names","Array","isArray","every","part","isNameList","unsubscribes","map","one","path","createPath","cb","changed","onKeyEvent","onPathEvent","length","forEach","unsubscribe","persistForm","storage","localStorage","raw","getItem","parse","deserialize","s","JSON","setInitialValues","values","serialize","getValues","setItem","stringify"],"mappings":"uIAsBA,SAASA,EAAaC,EAAaC,GACjC,OAAOD,EAAIE,WAAW,GAAGD,EAAYE,MAAM,GAAG,MAChD,CAgJO,SAASC,EAAUC,EAAYC,GACpC,MAAMC,KAACA,EAAAC,MAAMA,EAAQ,eAAUC,EAAQ,SAAAC,SAAUA,GAAYJ,EAC7D,QAAa,IAATC,EAAoB,OAAOI,EAAGN,EAAKO,QAASJ,EAAOE,GACvD,MAAMG,EAjCR,SAAoBN,GAClB,OACEO,MAAMC,QAAQR,IACbA,EAA8BS,MAAMC,GAAwB,iBAATA,EAExD,CA4BgBC,CAAWX,GAAQA,EAAO,CAACA,GACnCY,EAAeN,EAAMO,IAAIC,IAC7B,MAAMC,EAAOC,EAAWF,GACxB,MAAiB,WAAVb,GAAgC,YAAVA,EA5F1B,SACLI,EACAJ,EACAR,EACAwB,GAEA,OAAOb,EAAGC,EAASJ,EAAQiB,SACT,IAAZA,GAAyBA,EAAQzB,MAAQA,GAAKwB,KAEtD,CAoFQE,CAAWrB,EAAKO,QAASJ,EAAOc,EAAKtB,IAAKU,GA/H3C,SACLE,EACAJ,EACAc,EACAb,EACAe,GAEA,MAAMxB,IAACA,GAAOsB,EACd,OAAOX,EAAGC,EAASJ,EAAQiB,UAEX,IAAZA,GACAA,EAAQzB,MAAQA,GAChBD,EAAaC,EAAKyB,EAAQzB,MACf,WAAVS,GAAsBV,EAAa0B,EAAQzB,IAAKA,KAEjDwB,KAGN,CA8GQG,CAAYtB,EAAKO,QAASJ,EAAOc,EAAMb,EAAOC,KAEpD,OAA+B,IAAxBS,EAAaS,OAChBT,EAAa,GACb,IAAMA,EAAaU,QAAQC,GAAeA,IAChD,CC7HO,SAASC,EAAY1B,EAAYC,GACtC,MAAMN,IAACA,GAAOM,EACR0B,EACJ1B,EAAQ0B,UACiB,oBAAjBC,aAA+BA,qBACzC,IAAKD,EAAS,MAAO,OAKrB,MAAME,EAAMF,EAAQG,QAAQnC,GAC5B,GAAY,OAARkC,EACF,IACE,MAAME,EAAQ9B,EAAQ+B,aAAA,CAAiBC,GAAcC,KAAKH,MAAME,IAChEE,EAAiBnC,EAAM+B,EAAMF,GAC/B,CAAA,MAGA,CAGF,OAAO9B,EAAUC,EAAM,CACrBG,MAAO,SACPE,SAAU,KACR,MAAM+B,EAASnC,EAAQoC,UACnBpC,EAAQoC,UAAUC,EAAUtC,IAC5BsC,EAAUtC,GACd,IACE2B,EAAQY,QAAQ5C,EAAKuC,KAAKM,UAAUJ,GACtC,CAAA,MAEA,IAGN"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var t=require("../
|
|
1
|
+
"use strict";require("@for-fun/event-emitter");var t=require("../errors-CxSjrWJO.cjs.js");function e(t){return{type:"standard",message:t?.message||"Validation failed"}}function r(t){const e=t.path||[],r=[];for(const t of e){const e="object"==typeof t&&null!==t?t.key:t;r.push(String(e))}return r}function n(t,e,r){let n=t;for(let t=0;t<e.length-1;t++){const r=e[t];let o=n[r];if(void 0===o&&(o=n[r]={}),!s(o))return;n=o}const o=e[e.length-1],a=n[o];void 0===a?n[o]=[r]:Array.isArray(a)&&a.push(r)}function s(t){return!!t&&"object"==typeof t&&!Array.isArray(t)}function o(t){let e=!1;const r={};return Object.entries(t).forEach(([t,n])=>{if(s(n)){const s=o(n);s&&(r[t]=s,e=!0)}else r[t]=n,e=!0}),e?r:void 0}exports.hasStandardProps=function(t){return!!t&&"object"==typeof t&&"function"==typeof t["~standard"]?.validate},exports.standardSchemaFormValidator=function(s){return async a=>{const i=await s["~standard"].validate(a),{issues:u}=i;if(!u?.length)return{[t.VALIDATION_OUTCOME]:!0,values:"value"in i?i.value:void 0};const c={};for(const s of u){const o=r(s);if(o.length)n(c,o,e(s));else{const r=c[t.FORM_ERROR]??=[];Array.isArray(r)&&r.push(e(s))}}return{[t.VALIDATION_OUTCOME]:!0,errors:o(c)||{}}}},exports.standardSchemaResolver=function(t){return async r=>{const n=await t["~standard"].validate(r);if(n.issues?.length)return n.issues.map(e)}};
|
|
2
2
|
//# sourceMappingURL=standard-schema.cjs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"standard-schema.cjs.js","sources":["../../src/resolvers/standard-schema.ts"],"sourcesContent":["import {VALIDATION_OUTCOME} from '../form';\nimport type {FieldError, ValidationOutcome} from '../form';\nimport type {Validator} from '../hooks/validate';\n\n/**\n * Minimal copy of the Standard Schema v1 interfaces\n * (https://standardschema.dev) so this module has zero runtime and type\n * dependencies on any schema library. Implemented by zod v3.24+/v4,\n * valibot v1, arktype and others.\n */\nexport
|
|
1
|
+
{"version":3,"file":"standard-schema.cjs.js","sources":["../../src/resolvers/standard-schema.ts"],"sourcesContent":["import {FORM_ERROR, VALIDATION_OUTCOME} from '../form';\nimport type {FieldError, ValidationOutcome} from '../form';\nimport type {Validator} from '../hooks/validate';\n\n/**\n * Minimal copy of the Standard Schema v1 interfaces\n * (https://standardschema.dev) so this module has zero runtime and type\n * dependencies on any schema library. Implemented by zod v3.24+/v4,\n * valibot v1, arktype and others.\n */\nexport type StandardSchemaIssue = {\n readonly message: string;\n readonly path?:\n ReadonlyArray<PropertyKey | {readonly key: PropertyKey}> | undefined;\n};\n\nexport type StandardSchemaV1<Input = unknown, Output = Input> = {\n readonly '~standard': {\n readonly version: 1;\n readonly vendor: string;\n readonly validate: (\n value: Input\n ) =>\n | {readonly value: Output; readonly issues?: undefined}\n | {readonly issues: ReadonlyArray<StandardSchemaIssue>}\n | Promise<\n | {readonly value: Output; readonly issues?: undefined}\n | {readonly issues: ReadonlyArray<StandardSchemaIssue>}\n >;\n };\n};\n\n/**\n * Does the schema implement the Standard Schema v1 props?\n */\nexport function hasStandardProps(schema: any): schema is StandardSchemaV1 {\n return (\n !!schema &&\n typeof schema === 'object' &&\n typeof schema['~standard']?.validate === 'function'\n );\n}\n\nfunction toFieldError(issue: StandardSchemaIssue | undefined): FieldError {\n return {type: 'standard', message: issue?.message || 'Validation failed'};\n}\n\n/**\n * Field-level Standard Schema adapter: validate a single value with any\n * schema implementing '~standard' and map every issue to a FieldError,\n * so a value breaking several rules surfaces all of them (setErrorByPath\n * stores the array; error/errorObject readers still see the first).\n *\n * @param schema a Standard Schema v1 (zod v3.24+/v4, valibot v1, arktype...)\n * @return field validator compatible with useField's validate option\n */\nexport function standardSchemaResolver(schema: StandardSchemaV1): Validator {\n return async (value: any) => {\n const result = await schema['~standard'].validate(value);\n if (!result.issues?.length) return undefined;\n return result.issues.map(toFieldError);\n };\n}\n\n/**\n * Form-level Standard Schema adapter: validate the whole values object with\n * any schema implementing '~standard' and return a ValidationOutcome. On\n * failure `errors` carries the nested shape Options.validate expects\n * ({a: {b: FieldError[]}}; ensureValidate flattens it back to per-field\n * errors, keeping every issue of a path). Issues without a path are\n * form-level errors and land on the FORM_ERROR key, whose value (the\n * reserved _form field name) is exported from this library so consumers\n * read the errors back via getError(form, FORM_ERROR). On success `values`\n * carries the schema's parsed output (coerce/transform results included),\n * which the form stores as its parsedValues baseline — the layer getValues\n * reads above initialValues, mirroring how react-hook-form's zodResolver\n * and TanStack's standardSchemaValidators use the parsed value.\n *\n * @param schema a Standard Schema v1 (zod v3.24+/v4, valibot v1, arktype...)\n * @return form-level validator for createForm({validate: ...})\n */\nexport function standardSchemaFormValidator<T extends Record<string, any>>(\n schema: StandardSchemaV1<T, any>\n): (values: T) => Promise<ValidationOutcome<T>> {\n return async (values: T) => {\n const result = await schema['~standard'].validate(values);\n const {issues} = result;\n if (!issues?.length) {\n // Success: expose the schema's parsed output. `in` keeps the union\n // narrowed (the success variant is the one carrying `value`).\n return {\n [VALIDATION_OUTCOME]: true,\n values: 'value' in result ? result.value : undefined\n };\n }\n const errors: Record<string, any> = {};\n for (const issue of issues) {\n const segments = toPathSegments(issue);\n if (segments.length) {\n assignAtPath(errors, segments, toFieldError(issue));\n } else {\n // Pathless issues are all form-level: they accumulate on the\n // FORM_ERROR slot instead of the first shadowing the rest. (A nested\n // path literally named like FORM_ERROR would have made the slot a\n // branch — skip then.)\n const slot = (errors[FORM_ERROR] ??= []);\n if (Array.isArray(slot)) slot.push(toFieldError(issue));\n }\n }\n return {[VALIDATION_OUTCOME]: true, errors: pruneEmpty(errors) || {}};\n };\n}\n\n/**\n * Stringify an issue path: PropertyKey or {key} path segments → strings.\n */\nfunction toPathSegments(issue: StandardSchemaIssue): string[] {\n const path = issue.path || [];\n const segments: string[] = [];\n for (const segment of path) {\n const key =\n typeof segment === 'object' && segment !== null\n ? (segment as {key: PropertyKey}).key\n : segment;\n segments.push(String(key));\n }\n return segments;\n}\n\n/**\n * Append the error at a nested path. Leaves are FieldError[] arrays, so\n * several issues on one field accumulate in issue order; an issue whose\n * path conflicts with an existing leaf or crosses it is skipped.\n */\nfunction assignAtPath(\n root: Record<string, any>,\n segments: string[],\n error: FieldError\n): void {\n let node = root;\n for (let i = 0; i < segments.length - 1; i++) {\n const segment = segments[i];\n let next = node[segment];\n if (next === undefined) {\n next = node[segment] = {};\n }\n if (!isBranch(next)) return;\n node = next;\n }\n const leaf = segments[segments.length - 1];\n const slot = node[leaf];\n if (slot === undefined) node[leaf] = [error];\n else if (Array.isArray(slot)) slot.push(error);\n}\n\n/**\n * A branch is a plain container built while nesting; the leaves it carries\n * are the FieldError[] arrays assignAtPath appends.\n */\nfunction isBranch(value: any): value is Record<string, any> {\n return !!value && typeof value === 'object' && !Array.isArray(value);\n}\n\n/**\n * Drop empty branch objects left behind by conflicting issue paths.\n */\nfunction pruneEmpty(\n node: Record<string, any>\n): Record<string, any> | undefined {\n let hasLeaf = false;\n const result: Record<string, any> = {};\n Object.entries(node).forEach(([key, value]) => {\n if (isBranch(value)) {\n const pruned = pruneEmpty(value);\n if (pruned) {\n result[key] = pruned;\n hasLeaf = true;\n }\n } else {\n result[key] = value;\n hasLeaf = true;\n }\n });\n return hasLeaf ? result : undefined;\n}\n"],"names":["toFieldError","issue","type","message","toPathSegments","path","segments","segment","key","push","String","assignAtPath","root","error","node","i","length","next","isBranch","leaf","slot","Array","isArray","value","pruneEmpty","hasLeaf","result","Object","entries","forEach","pruned","schema","validate","async","values","issues","VALIDATION_OUTCOME","errors","FORM_ERROR","map"],"mappings":"0FA2CA,SAASA,EAAaC,GACpB,MAAO,CAACC,KAAM,WAAYC,QAASF,GAAOE,SAAW,oBACvD,CAuEA,SAASC,EAAeH,GACtB,MAAMI,EAAOJ,EAAMI,MAAQ,GACrBC,EAAqB,GAC3B,IAAA,MAAWC,KAAWF,EAAM,CAC1B,MAAMG,EACe,iBAAZD,GAAoC,OAAZA,EAC1BA,EAA+BC,IAChCD,EACND,EAASG,KAAKC,OAAOF,GACvB,CACA,OAAOF,CACT,CAOA,SAASK,EACPC,EACAN,EACAO,GAEA,IAAIC,EAAOF,EACX,IAAA,IAASG,EAAI,EAAGA,EAAIT,EAASU,OAAS,EAAGD,IAAK,CAC5C,MAAMR,EAAUD,EAASS,GACzB,IAAIE,EAAOH,EAAKP,GAIhB,QAHa,IAATU,IACFA,EAAOH,EAAKP,GAAW,CAAA,IAEpBW,EAASD,GAAO,OACrBH,EAAOG,CACT,CACA,MAAME,EAAOb,EAASA,EAASU,OAAS,GAClCI,EAAON,EAAKK,QACL,IAATC,EAAoBN,EAAKK,GAAQ,CAACN,GAC7BQ,MAAMC,QAAQF,IAAOA,EAAKX,KAAKI,EAC1C,CAMA,SAASK,EAASK,GAChB,QAASA,GAA0B,iBAAVA,IAAuBF,MAAMC,QAAQC,EAChE,CAKA,SAASC,EACPV,GAEA,IAAIW,GAAU,EACd,MAAMC,EAA8B,CAAA,EAapC,OAZAC,OAAOC,QAAQd,GAAMe,QAAQ,EAAErB,EAAKe,MAClC,GAAIL,EAASK,GAAQ,CACnB,MAAMO,EAASN,EAAWD,GACtBO,IACFJ,EAAOlB,GAAOsB,EACdL,GAAU,EAEd,MACEC,EAAOlB,GAAOe,EACdE,GAAU,IAGPA,EAAUC,OAAS,CAC5B,0BArJO,SAA0BK,GAC/B,QACIA,GACgB,iBAAXA,GACkC,mBAAlCA,EAAO,cAAcC,QAEhC,sCAwCO,SACLD,GAEA,OAAOE,MAAOC,IACZ,MAAMR,QAAeK,EAAO,aAAaC,SAASE,IAC5CC,OAACA,GAAUT,EACjB,IAAKS,GAAQnB,OAGX,MAAO,CACL,CAACoB,EAAAA,qBAAqB,EACtBF,OAAQ,UAAWR,EAASA,EAAOH,WAAQ,GAG/C,MAAMc,EAA8B,CAAA,EACpC,IAAA,MAAWpC,KAASkC,EAAQ,CAC1B,MAAM7B,EAAWF,EAAeH,GAChC,GAAIK,EAASU,OACXL,EAAa0B,EAAQ/B,EAAUN,EAAaC,QACvC,CAKL,MAAMmB,EAAQiB,EAAOC,EAAAA,cAAgB,GACjCjB,MAAMC,QAAQF,MAAYX,KAAKT,EAAaC,GAClD,CACF,CACA,MAAO,CAAC,CAACmC,EAAAA,qBAAqB,EAAMC,OAAQb,EAAWa,IAAW,IAEtE,iCAvDO,SAAgCN,GACrC,OAAOE,MAAOV,IACZ,MAAMG,QAAeK,EAAO,aAAaC,SAAST,GAClD,GAAKG,EAAOS,QAAQnB,OACpB,OAAOU,EAAOS,OAAOI,IAAIvC,GAE7B"}
|
|
@@ -8,13 +8,13 @@ import '@for-fun/event-emitter';
|
|
|
8
8
|
* dependencies on any schema library. Implemented by zod v3.24+/v4,
|
|
9
9
|
* valibot v1, arktype and others.
|
|
10
10
|
*/
|
|
11
|
-
|
|
11
|
+
type StandardSchemaIssue = {
|
|
12
12
|
readonly message: string;
|
|
13
13
|
readonly path?: ReadonlyArray<PropertyKey | {
|
|
14
14
|
readonly key: PropertyKey;
|
|
15
15
|
}> | undefined;
|
|
16
|
-
}
|
|
17
|
-
|
|
16
|
+
};
|
|
17
|
+
type StandardSchemaV1<Input = unknown, Output = Input> = {
|
|
18
18
|
readonly '~standard': {
|
|
19
19
|
readonly version: 1;
|
|
20
20
|
readonly vendor: string;
|
|
@@ -30,7 +30,7 @@ interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
|
30
30
|
readonly issues: ReadonlyArray<StandardSchemaIssue>;
|
|
31
31
|
}>;
|
|
32
32
|
};
|
|
33
|
-
}
|
|
33
|
+
};
|
|
34
34
|
/**
|
|
35
35
|
* Does the schema implement the Standard Schema v1 props?
|
|
36
36
|
*/
|
|
@@ -51,7 +51,9 @@ declare function standardSchemaResolver(schema: StandardSchemaV1): Validator;
|
|
|
51
51
|
* failure `errors` carries the nested shape Options.validate expects
|
|
52
52
|
* ({a: {b: FieldError[]}}; ensureValidate flattens it back to per-field
|
|
53
53
|
* errors, keeping every issue of a path). Issues without a path are
|
|
54
|
-
* form-level errors and land on the
|
|
54
|
+
* form-level errors and land on the FORM_ERROR key, whose value (the
|
|
55
|
+
* reserved _form field name) is exported from this library so consumers
|
|
56
|
+
* read the errors back via getError(form, FORM_ERROR). On success `values`
|
|
55
57
|
* carries the schema's parsed output (coerce/transform results included),
|
|
56
58
|
* which the form stores as its parsedValues baseline — the layer getValues
|
|
57
59
|
* reads above initialValues, mirroring how react-hook-form's zodResolver
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{V as t}from"../
|
|
1
|
+
import"@for-fun/event-emitter";import{V as t,F as n}from"../errors-CzWtwjO0.mjs";function e(t){return!!t&&"object"==typeof t&&"function"==typeof t["~standard"]?.validate}function r(t){return{type:"standard",message:t?.message||"Validation failed"}}function o(t){return async n=>{const e=await t["~standard"].validate(n);if(e.issues?.length)return e.issues.map(r)}}function s(e){return async o=>{const s=await e["~standard"].validate(o),{issues:u}=s;if(!u?.length)return{[t]:!0,values:"value"in s?s.value:void 0};const f={};for(const t of u){const e=a(t);if(e.length)i(f,e,r(t));else{const e=f[n]??=[];Array.isArray(e)&&e.push(r(t))}}return{[t]:!0,errors:c(f)||{}}}}function a(t){const n=t.path||[],e=[];for(const t of n){const n="object"==typeof t&&null!==t?t.key:t;e.push(String(n))}return e}function i(t,n,e){let r=t;for(let t=0;t<n.length-1;t++){const e=n[t];let o=r[e];if(void 0===o&&(o=r[e]={}),!u(o))return;r=o}const o=n[n.length-1],s=r[o];void 0===s?r[o]=[e]:Array.isArray(s)&&s.push(e)}function u(t){return!!t&&"object"==typeof t&&!Array.isArray(t)}function c(t){let n=!1;const e={};return Object.entries(t).forEach(([t,r])=>{if(u(r)){const o=c(r);o&&(e[t]=o,n=!0)}else e[t]=r,n=!0}),n?e:void 0}export{e as hasStandardProps,s as standardSchemaFormValidator,o as standardSchemaResolver};
|
|
2
2
|
//# sourceMappingURL=standard-schema.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"standard-schema.mjs","sources":["../../src/resolvers/standard-schema.ts"],"sourcesContent":["import {VALIDATION_OUTCOME} from '../form';\nimport type {FieldError, ValidationOutcome} from '../form';\nimport type {Validator} from '../hooks/validate';\n\n/**\n * Minimal copy of the Standard Schema v1 interfaces\n * (https://standardschema.dev) so this module has zero runtime and type\n * dependencies on any schema library. Implemented by zod v3.24+/v4,\n * valibot v1, arktype and others.\n */\nexport
|
|
1
|
+
{"version":3,"file":"standard-schema.mjs","sources":["../../src/resolvers/standard-schema.ts"],"sourcesContent":["import {FORM_ERROR, VALIDATION_OUTCOME} from '../form';\nimport type {FieldError, ValidationOutcome} from '../form';\nimport type {Validator} from '../hooks/validate';\n\n/**\n * Minimal copy of the Standard Schema v1 interfaces\n * (https://standardschema.dev) so this module has zero runtime and type\n * dependencies on any schema library. Implemented by zod v3.24+/v4,\n * valibot v1, arktype and others.\n */\nexport type StandardSchemaIssue = {\n readonly message: string;\n readonly path?:\n ReadonlyArray<PropertyKey | {readonly key: PropertyKey}> | undefined;\n};\n\nexport type StandardSchemaV1<Input = unknown, Output = Input> = {\n readonly '~standard': {\n readonly version: 1;\n readonly vendor: string;\n readonly validate: (\n value: Input\n ) =>\n | {readonly value: Output; readonly issues?: undefined}\n | {readonly issues: ReadonlyArray<StandardSchemaIssue>}\n | Promise<\n | {readonly value: Output; readonly issues?: undefined}\n | {readonly issues: ReadonlyArray<StandardSchemaIssue>}\n >;\n };\n};\n\n/**\n * Does the schema implement the Standard Schema v1 props?\n */\nexport function hasStandardProps(schema: any): schema is StandardSchemaV1 {\n return (\n !!schema &&\n typeof schema === 'object' &&\n typeof schema['~standard']?.validate === 'function'\n );\n}\n\nfunction toFieldError(issue: StandardSchemaIssue | undefined): FieldError {\n return {type: 'standard', message: issue?.message || 'Validation failed'};\n}\n\n/**\n * Field-level Standard Schema adapter: validate a single value with any\n * schema implementing '~standard' and map every issue to a FieldError,\n * so a value breaking several rules surfaces all of them (setErrorByPath\n * stores the array; error/errorObject readers still see the first).\n *\n * @param schema a Standard Schema v1 (zod v3.24+/v4, valibot v1, arktype...)\n * @return field validator compatible with useField's validate option\n */\nexport function standardSchemaResolver(schema: StandardSchemaV1): Validator {\n return async (value: any) => {\n const result = await schema['~standard'].validate(value);\n if (!result.issues?.length) return undefined;\n return result.issues.map(toFieldError);\n };\n}\n\n/**\n * Form-level Standard Schema adapter: validate the whole values object with\n * any schema implementing '~standard' and return a ValidationOutcome. On\n * failure `errors` carries the nested shape Options.validate expects\n * ({a: {b: FieldError[]}}; ensureValidate flattens it back to per-field\n * errors, keeping every issue of a path). Issues without a path are\n * form-level errors and land on the FORM_ERROR key, whose value (the\n * reserved _form field name) is exported from this library so consumers\n * read the errors back via getError(form, FORM_ERROR). On success `values`\n * carries the schema's parsed output (coerce/transform results included),\n * which the form stores as its parsedValues baseline — the layer getValues\n * reads above initialValues, mirroring how react-hook-form's zodResolver\n * and TanStack's standardSchemaValidators use the parsed value.\n *\n * @param schema a Standard Schema v1 (zod v3.24+/v4, valibot v1, arktype...)\n * @return form-level validator for createForm({validate: ...})\n */\nexport function standardSchemaFormValidator<T extends Record<string, any>>(\n schema: StandardSchemaV1<T, any>\n): (values: T) => Promise<ValidationOutcome<T>> {\n return async (values: T) => {\n const result = await schema['~standard'].validate(values);\n const {issues} = result;\n if (!issues?.length) {\n // Success: expose the schema's parsed output. `in` keeps the union\n // narrowed (the success variant is the one carrying `value`).\n return {\n [VALIDATION_OUTCOME]: true,\n values: 'value' in result ? result.value : undefined\n };\n }\n const errors: Record<string, any> = {};\n for (const issue of issues) {\n const segments = toPathSegments(issue);\n if (segments.length) {\n assignAtPath(errors, segments, toFieldError(issue));\n } else {\n // Pathless issues are all form-level: they accumulate on the\n // FORM_ERROR slot instead of the first shadowing the rest. (A nested\n // path literally named like FORM_ERROR would have made the slot a\n // branch — skip then.)\n const slot = (errors[FORM_ERROR] ??= []);\n if (Array.isArray(slot)) slot.push(toFieldError(issue));\n }\n }\n return {[VALIDATION_OUTCOME]: true, errors: pruneEmpty(errors) || {}};\n };\n}\n\n/**\n * Stringify an issue path: PropertyKey or {key} path segments → strings.\n */\nfunction toPathSegments(issue: StandardSchemaIssue): string[] {\n const path = issue.path || [];\n const segments: string[] = [];\n for (const segment of path) {\n const key =\n typeof segment === 'object' && segment !== null\n ? (segment as {key: PropertyKey}).key\n : segment;\n segments.push(String(key));\n }\n return segments;\n}\n\n/**\n * Append the error at a nested path. Leaves are FieldError[] arrays, so\n * several issues on one field accumulate in issue order; an issue whose\n * path conflicts with an existing leaf or crosses it is skipped.\n */\nfunction assignAtPath(\n root: Record<string, any>,\n segments: string[],\n error: FieldError\n): void {\n let node = root;\n for (let i = 0; i < segments.length - 1; i++) {\n const segment = segments[i];\n let next = node[segment];\n if (next === undefined) {\n next = node[segment] = {};\n }\n if (!isBranch(next)) return;\n node = next;\n }\n const leaf = segments[segments.length - 1];\n const slot = node[leaf];\n if (slot === undefined) node[leaf] = [error];\n else if (Array.isArray(slot)) slot.push(error);\n}\n\n/**\n * A branch is a plain container built while nesting; the leaves it carries\n * are the FieldError[] arrays assignAtPath appends.\n */\nfunction isBranch(value: any): value is Record<string, any> {\n return !!value && typeof value === 'object' && !Array.isArray(value);\n}\n\n/**\n * Drop empty branch objects left behind by conflicting issue paths.\n */\nfunction pruneEmpty(\n node: Record<string, any>\n): Record<string, any> | undefined {\n let hasLeaf = false;\n const result: Record<string, any> = {};\n Object.entries(node).forEach(([key, value]) => {\n if (isBranch(value)) {\n const pruned = pruneEmpty(value);\n if (pruned) {\n result[key] = pruned;\n hasLeaf = true;\n }\n } else {\n result[key] = value;\n hasLeaf = true;\n }\n });\n return hasLeaf ? result : undefined;\n}\n"],"names":["hasStandardProps","schema","validate","toFieldError","issue","type","message","standardSchemaResolver","async","value","result","issues","length","map","standardSchemaFormValidator","values","VALIDATION_OUTCOME","errors","segments","toPathSegments","assignAtPath","slot","FORM_ERROR","Array","isArray","push","pruneEmpty","path","segment","key","String","root","error","node","i","next","isBranch","leaf","hasLeaf","Object","entries","forEach","pruned"],"mappings":"iFAmCO,SAASA,EAAiBC,GAC/B,QACIA,GACgB,iBAAXA,GACkC,mBAAlCA,EAAO,cAAcC,QAEhC,CAEA,SAASC,EAAaC,GACpB,MAAO,CAACC,KAAM,WAAYC,QAASF,GAAOE,SAAW,oBACvD,CAWO,SAASC,EAAuBN,GACrC,OAAOO,MAAOC,IACZ,MAAMC,QAAeT,EAAO,aAAaC,SAASO,GAClD,GAAKC,EAAOC,QAAQC,OACpB,OAAOF,EAAOC,OAAOE,IAAIV,GAE7B,CAmBO,SAASW,EACdb,GAEA,OAAOO,MAAOO,IACZ,MAAML,QAAeT,EAAO,aAAaC,SAASa,IAC5CJ,OAACA,GAAUD,EACjB,IAAKC,GAAQC,OAGX,MAAO,CACLI,CAACA,IAAqB,EACtBD,OAAQ,UAAWL,EAASA,EAAOD,WAAQ,GAG/C,MAAMQ,EAA8B,CAAA,EACpC,IAAA,MAAWb,KAASO,EAAQ,CAC1B,MAAMO,EAAWC,EAAef,GAChC,GAAIc,EAASN,OACXQ,EAAaH,EAAQC,EAAUf,EAAaC,QACvC,CAKL,MAAMiB,EAAQJ,EAAOK,KAAgB,GACjCC,MAAMC,QAAQH,MAAYI,KAAKtB,EAAaC,GAClD,CACF,CACA,MAAO,CAACY,CAACA,IAAqB,EAAMC,OAAQS,EAAWT,IAAW,IAEtE,CAKA,SAASE,EAAef,GACtB,MAAMuB,EAAOvB,EAAMuB,MAAQ,GACrBT,EAAqB,GAC3B,IAAA,MAAWU,KAAWD,EAAM,CAC1B,MAAME,EACe,iBAAZD,GAAoC,OAAZA,EAC1BA,EAA+BC,IAChCD,EACNV,EAASO,KAAKK,OAAOD,GACvB,CACA,OAAOX,CACT,CAOA,SAASE,EACPW,EACAb,EACAc,GAEA,IAAIC,EAAOF,EACX,IAAA,IAASG,EAAI,EAAGA,EAAIhB,EAASN,OAAS,EAAGsB,IAAK,CAC5C,MAAMN,EAAUV,EAASgB,GACzB,IAAIC,EAAOF,EAAKL,GAIhB,QAHa,IAATO,IACFA,EAAOF,EAAKL,GAAW,CAAA,IAEpBQ,EAASD,GAAO,OACrBF,EAAOE,CACT,CACA,MAAME,EAAOnB,EAASA,EAASN,OAAS,GAClCS,EAAOY,EAAKI,QACL,IAAThB,EAAoBY,EAAKI,GAAQ,CAACL,GAC7BT,MAAMC,QAAQH,IAAOA,EAAKI,KAAKO,EAC1C,CAMA,SAASI,EAAS3B,GAChB,QAASA,GAA0B,iBAAVA,IAAuBc,MAAMC,QAAQf,EAChE,CAKA,SAASiB,EACPO,GAEA,IAAIK,GAAU,EACd,MAAM5B,EAA8B,CAAA,EAapC,OAZA6B,OAAOC,QAAQP,GAAMQ,QAAQ,EAAEZ,EAAKpB,MAClC,GAAI2B,EAAS3B,GAAQ,CACnB,MAAMiC,EAAShB,EAAWjB,GACtBiC,IACFhC,EAAOmB,GAAOa,EACdJ,GAAU,EAEd,MACE5B,EAAOmB,GAAOpB,EACd6B,GAAU,IAGPA,EAAU5B,OAAS,CAC5B"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var r=require("./standard-schema.cjs.js");require("../
|
|
1
|
+
"use strict";var r=require("./standard-schema.cjs.js");require("@for-fun/event-emitter"),require("../errors-CxSjrWJO.cjs.js"),exports.yupResolver=function(e){return r.hasStandardProps(e)?r.standardSchemaResolver(e):async r=>{try{return void await e.validate(r,{abortEarly:!1})}catch(r){return(Array.isArray(r?.inner)&&r.inner.length?r.inner:[r]).map(r=>({type:r?.type||"custom",message:r?.message||"Validation failed"}))}}};
|
|
2
2
|
//# sourceMappingURL=yup.cjs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"yup.cjs.js","sources":["../../src/resolvers/yup.ts"],"sourcesContent":["import type {FieldError} from '../form';\nimport type {Validator} from '../hooks/validate';\nimport {hasStandardProps, standardSchemaResolver} from './standard-schema';\n\nexport function yupResolver(schema: any): Validator {\n // Recent yup versions implement the Standard Schema props — prefer them.\n if (hasStandardProps(schema)) return standardSchemaResolver(schema);\n // Older yup: fall back to the throw-based validate API. abortEarly:false\n // makes yup aggregate every failure into err.inner instead of throwing\n // on the first, so all of a field's errors reach the form.\n return async (value: any) => {\n try {\n await schema.validate(value, {abortEarly: false});\n return undefined;\n } catch (err: any) {\n const issues =\n Array.isArray(err?.inner) && err.inner.length ? err.inner : [err];\n return issues.map((issue: any): FieldError => ({\n type: issue?.type || 'custom',\n message: issue?.message || 'Validation failed'\n }));\n }\n };\n}\n"],"names":["schema","hasStandardProps","standardSchemaResolver","async","value","validate","abortEarly","err","Array","isArray","inner","length","map","issue","type","message"],"mappings":"
|
|
1
|
+
{"version":3,"file":"yup.cjs.js","sources":["../../src/resolvers/yup.ts"],"sourcesContent":["import type {FieldError} from '../form';\nimport type {Validator} from '../hooks/validate';\nimport {hasStandardProps, standardSchemaResolver} from './standard-schema';\n\nexport function yupResolver(schema: any): Validator {\n // Recent yup versions implement the Standard Schema props — prefer them.\n if (hasStandardProps(schema)) return standardSchemaResolver(schema);\n // Older yup: fall back to the throw-based validate API. abortEarly:false\n // makes yup aggregate every failure into err.inner instead of throwing\n // on the first, so all of a field's errors reach the form.\n return async (value: any) => {\n try {\n await schema.validate(value, {abortEarly: false});\n return undefined;\n } catch (err: any) {\n const issues =\n Array.isArray(err?.inner) && err.inner.length ? err.inner : [err];\n return issues.map((issue: any): FieldError => ({\n type: issue?.type || 'custom',\n message: issue?.message || 'Validation failed'\n }));\n }\n };\n}\n"],"names":["schema","hasStandardProps","standardSchemaResolver","async","value","validate","abortEarly","err","Array","isArray","inner","length","map","issue","type","message"],"mappings":"kJAIO,SAAqBA,GAE1B,OAAIC,EAAAA,iBAAiBD,GAAgBE,EAAAA,uBAAuBF,GAIrDG,MAAOC,IACZ,IAEE,kBADMJ,EAAOK,SAASD,EAAO,CAACE,YAAY,GAE5C,OAASC,GAGP,OADEC,MAAMC,QAAQF,GAAKG,QAAUH,EAAIG,MAAMC,OAASJ,EAAIG,MAAQ,CAACH,IACjDK,IAAKC,IAAA,CACjBC,KAAMD,GAAOC,MAAQ,SACrBC,QAASF,GAAOE,SAAW,sBAE/B,EAEJ"}
|
package/dist/resolvers/yup.d.ts
CHANGED
package/dist/resolvers/yup.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{hasStandardProps as r,standardSchemaResolver as
|
|
1
|
+
import{hasStandardProps as r,standardSchemaResolver as e}from"./standard-schema.mjs";import"@for-fun/event-emitter";import"../errors-CzWtwjO0.mjs";function t(t){return r(t)?e(t):async r=>{try{return void await t.validate(r,{abortEarly:!1})}catch(r){return(Array.isArray(r?.inner)&&r.inner.length?r.inner:[r]).map(r=>({type:r?.type||"custom",message:r?.message||"Validation failed"}))}}}export{t as yupResolver};
|
|
2
2
|
//# sourceMappingURL=yup.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"yup.mjs","sources":["../../src/resolvers/yup.ts"],"sourcesContent":["import type {FieldError} from '../form';\nimport type {Validator} from '../hooks/validate';\nimport {hasStandardProps, standardSchemaResolver} from './standard-schema';\n\nexport function yupResolver(schema: any): Validator {\n // Recent yup versions implement the Standard Schema props — prefer them.\n if (hasStandardProps(schema)) return standardSchemaResolver(schema);\n // Older yup: fall back to the throw-based validate API. abortEarly:false\n // makes yup aggregate every failure into err.inner instead of throwing\n // on the first, so all of a field's errors reach the form.\n return async (value: any) => {\n try {\n await schema.validate(value, {abortEarly: false});\n return undefined;\n } catch (err: any) {\n const issues =\n Array.isArray(err?.inner) && err.inner.length ? err.inner : [err];\n return issues.map((issue: any): FieldError => ({\n type: issue?.type || 'custom',\n message: issue?.message || 'Validation failed'\n }));\n }\n };\n}\n"],"names":["yupResolver","schema","hasStandardProps","standardSchemaResolver","async","value","validate","abortEarly","err","Array","isArray","inner","length","map","issue","type","message"],"mappings":"
|
|
1
|
+
{"version":3,"file":"yup.mjs","sources":["../../src/resolvers/yup.ts"],"sourcesContent":["import type {FieldError} from '../form';\nimport type {Validator} from '../hooks/validate';\nimport {hasStandardProps, standardSchemaResolver} from './standard-schema';\n\nexport function yupResolver(schema: any): Validator {\n // Recent yup versions implement the Standard Schema props — prefer them.\n if (hasStandardProps(schema)) return standardSchemaResolver(schema);\n // Older yup: fall back to the throw-based validate API. abortEarly:false\n // makes yup aggregate every failure into err.inner instead of throwing\n // on the first, so all of a field's errors reach the form.\n return async (value: any) => {\n try {\n await schema.validate(value, {abortEarly: false});\n return undefined;\n } catch (err: any) {\n const issues =\n Array.isArray(err?.inner) && err.inner.length ? err.inner : [err];\n return issues.map((issue: any): FieldError => ({\n type: issue?.type || 'custom',\n message: issue?.message || 'Validation failed'\n }));\n }\n };\n}\n"],"names":["yupResolver","schema","hasStandardProps","standardSchemaResolver","async","value","validate","abortEarly","err","Array","isArray","inner","length","map","issue","type","message"],"mappings":"mJAIO,SAASA,EAAYC,GAE1B,OAAIC,EAAiBD,GAAgBE,EAAuBF,GAIrDG,MAAOC,IACZ,IAEE,kBADMJ,EAAOK,SAASD,EAAO,CAACE,YAAY,GAE5C,OAASC,GAGP,OADEC,MAAMC,QAAQF,GAAKG,QAAUH,EAAIG,MAAMC,OAASJ,EAAIG,MAAQ,CAACH,IACjDK,IAAKC,IAAA,CACjBC,KAAMD,GAAOC,MAAQ,SACrBC,QAASF,GAAOE,SAAW,sBAE/B,EAEJ"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var e=require("./standard-schema.cjs.js");require("@for-fun/event-emitter"),require("../errors-CxSjrWJO.cjs.js"),exports.zodResolver=function(s){return e.hasStandardProps(s)?e.standardSchemaResolver(s):async e=>{const r=await s.safeParseAsync(e);if(r.success)return;const{issues:a}=r.error;return a?.length?a.map(e=>({type:e?.code||"custom",message:e?.message||"Validation failed"})):[{type:"custom",message:"Validation failed"}]}};
|
|
2
2
|
//# sourceMappingURL=zod.cjs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zod.cjs.js","sources":["../../src/resolvers/zod.ts"],"sourcesContent":["import type {FieldError} from '../form';\nimport type {Validator} from '../hooks/validate';\nimport {hasStandardProps, standardSchemaResolver} from './standard-schema';\n\nexport function zodResolver(schema: any): Validator {\n // zod v3.24+/v4 schemas carry the Standard Schema props — prefer them.\n if (hasStandardProps(schema)) return standardSchemaResolver(schema);\n // Older zod: fall back to the legacy safeParseAsync API. It aggregates\n // every issue (no abortEarly), so map them all — a value breaking\n // several rules surfaces all of its errors.\n return async (value: any) => {\n const result = await schema.safeParseAsync(value);\n if (result.success) return undefined;\n const {issues} = result.error;\n if (!issues?.length) {\n return [{type: 'custom', message: 'Validation failed'}];\n }\n return issues.map((issue: any): FieldError => ({\n type: issue?.code || 'custom',\n message: issue?.message || 'Validation failed'\n }));\n };\n}\n"],"names":["schema","hasStandardProps","standardSchemaResolver","async","value","result","safeParseAsync","success","issues","error","length","map","issue","type","code","message"],"mappings":"
|
|
1
|
+
{"version":3,"file":"zod.cjs.js","sources":["../../src/resolvers/zod.ts"],"sourcesContent":["import type {FieldError} from '../form';\nimport type {Validator} from '../hooks/validate';\nimport {hasStandardProps, standardSchemaResolver} from './standard-schema';\n\nexport function zodResolver(schema: any): Validator {\n // zod v3.24+/v4 schemas carry the Standard Schema props — prefer them.\n if (hasStandardProps(schema)) return standardSchemaResolver(schema);\n // Older zod: fall back to the legacy safeParseAsync API. It aggregates\n // every issue (no abortEarly), so map them all — a value breaking\n // several rules surfaces all of its errors.\n return async (value: any) => {\n const result = await schema.safeParseAsync(value);\n if (result.success) return undefined;\n const {issues} = result.error;\n if (!issues?.length) {\n return [{type: 'custom', message: 'Validation failed'}];\n }\n return issues.map((issue: any): FieldError => ({\n type: issue?.code || 'custom',\n message: issue?.message || 'Validation failed'\n }));\n };\n}\n"],"names":["schema","hasStandardProps","standardSchemaResolver","async","value","result","safeParseAsync","success","issues","error","length","map","issue","type","code","message"],"mappings":"kJAIO,SAAqBA,GAE1B,OAAIC,EAAAA,iBAAiBD,GAAgBE,EAAAA,uBAAuBF,GAIrDG,MAAOC,IACZ,MAAMC,QAAeL,EAAOM,eAAeF,GAC3C,GAAIC,EAAOE,QAAS,OACpB,MAAMC,OAACA,GAAUH,EAAOI,MACxB,OAAKD,GAAQE,OAGNF,EAAOG,IAAKC,IAAA,CACjBC,KAAMD,GAAOE,MAAQ,SACrBC,QAASH,GAAOG,SAAW,uBAJpB,CAAC,CAACF,KAAM,SAAUE,QAAS,sBAOxC"}
|
package/dist/resolvers/zod.d.ts
CHANGED
package/dist/resolvers/zod.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{hasStandardProps as
|
|
1
|
+
import{hasStandardProps as e,standardSchemaResolver as s}from"./standard-schema.mjs";import"@for-fun/event-emitter";import"../errors-CzWtwjO0.mjs";function t(t){return e(t)?s(t):async e=>{const s=await t.safeParseAsync(e);if(s.success)return;const{issues:r}=s.error;return r?.length?r.map(e=>({type:e?.code||"custom",message:e?.message||"Validation failed"})):[{type:"custom",message:"Validation failed"}]}}export{t as zodResolver};
|
|
2
2
|
//# sourceMappingURL=zod.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zod.mjs","sources":["../../src/resolvers/zod.ts"],"sourcesContent":["import type {FieldError} from '../form';\nimport type {Validator} from '../hooks/validate';\nimport {hasStandardProps, standardSchemaResolver} from './standard-schema';\n\nexport function zodResolver(schema: any): Validator {\n // zod v3.24+/v4 schemas carry the Standard Schema props — prefer them.\n if (hasStandardProps(schema)) return standardSchemaResolver(schema);\n // Older zod: fall back to the legacy safeParseAsync API. It aggregates\n // every issue (no abortEarly), so map them all — a value breaking\n // several rules surfaces all of its errors.\n return async (value: any) => {\n const result = await schema.safeParseAsync(value);\n if (result.success) return undefined;\n const {issues} = result.error;\n if (!issues?.length) {\n return [{type: 'custom', message: 'Validation failed'}];\n }\n return issues.map((issue: any): FieldError => ({\n type: issue?.code || 'custom',\n message: issue?.message || 'Validation failed'\n }));\n };\n}\n"],"names":["zodResolver","schema","hasStandardProps","standardSchemaResolver","async","value","result","safeParseAsync","success","issues","error","length","map","issue","type","code","message"],"mappings":"
|
|
1
|
+
{"version":3,"file":"zod.mjs","sources":["../../src/resolvers/zod.ts"],"sourcesContent":["import type {FieldError} from '../form';\nimport type {Validator} from '../hooks/validate';\nimport {hasStandardProps, standardSchemaResolver} from './standard-schema';\n\nexport function zodResolver(schema: any): Validator {\n // zod v3.24+/v4 schemas carry the Standard Schema props — prefer them.\n if (hasStandardProps(schema)) return standardSchemaResolver(schema);\n // Older zod: fall back to the legacy safeParseAsync API. It aggregates\n // every issue (no abortEarly), so map them all — a value breaking\n // several rules surfaces all of its errors.\n return async (value: any) => {\n const result = await schema.safeParseAsync(value);\n if (result.success) return undefined;\n const {issues} = result.error;\n if (!issues?.length) {\n return [{type: 'custom', message: 'Validation failed'}];\n }\n return issues.map((issue: any): FieldError => ({\n type: issue?.code || 'custom',\n message: issue?.message || 'Validation failed'\n }));\n };\n}\n"],"names":["zodResolver","schema","hasStandardProps","standardSchemaResolver","async","value","result","safeParseAsync","success","issues","error","length","map","issue","type","code","message"],"mappings":"mJAIO,SAASA,EAAYC,GAE1B,OAAIC,EAAiBD,GAAgBE,EAAuBF,GAIrDG,MAAOC,IACZ,MAAMC,QAAeL,EAAOM,eAAeF,GAC3C,GAAIC,EAAOE,QAAS,OACpB,MAAMC,OAACA,GAAUH,EAAOI,MACxB,OAAKD,GAAQE,OAGNF,EAAOG,IAAKC,IAAA,CACjBC,KAAMD,GAAOE,MAAQ,SACrBC,QAASH,GAAOG,SAAW,uBAJpB,CAAC,CAACF,KAAM,SAAUE,QAAS,sBAOxC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var e=require("@for-fun/event-emitter"),i=require("../errors-CxSjrWJO.cjs.js"),t=require("../values-CDNAYEOB.cjs.js"),a=require("../validate-DAfz8Nbb.cjs.js");function n(e,i,t){if(null!=t)if(Array.isArray(t))for(const a of t)n(e,i,a);else if("undefined"!=typeof FileList&&t instanceof FileList)for(let a=0;a<t.length;a++)e.append(i,t.item(a));else"undefined"!=typeof File&&t instanceof File?e.append(i,t,t.name):"undefined"!=typeof Blob&&t instanceof Blob?e.append(i,t):t instanceof Date?e.append(i,t.toISOString()):e.append(i,"object"==typeof t?JSON.stringify(t):String(t))}exports.VALIDATION_OUTCOME=i.VALIDATION_OUTCOME,exports.formDataFromValues=function(e){const i=new FormData;for(const t of Object.keys(e))n(i,t,e[t]);return i},exports.validateValues=async function(n,s){const o=function(a){const n=e.create();e.setMaxListeners(n,0);let s=a?.initialValues??{};"function"==typeof s&&(s=s());const o={emitter:n,...a,mode:a?.mode??"onSubmit",reValidateMode:a?.reValidateMode??"onChange",disabled:a?.disabled??!1,validateDeps:a?.validateDeps?new Set(a.validateDeps.map(e=>i.create(e).key)):void 0,initialValues:{},values:new Map,deleted:new Set,errors:new Map,touched:new Set,validators:new Map,validating:new Set,parsedValues:void 0,isSubmitting:!1,submitCount:0,isSubmitSuccessful:void 0,isLoading:!1};return i.isPromise(s)?(o.isLoading=!0,e.emit(n,"loading"),Promise.resolve(s).then(i=>{o.isLoading=!1,e.emit(n,"loading"),t.setInitialValues(o,i??{})},i=>{o.isLoading=!1,e.emit(n,"loading")})):o.initialValues=s,o}({...s,initialValues:n});return{valid:await a.trigger(o),values:t.getValues(o),errors:i.getErrors(o)}};
|
|
2
|
+
//# sourceMappingURL=index.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs.js","sources":["../../src/server.ts","../../src/form.ts"],"sourcesContent":["/**\n * Server-side validation entry — `react-f0rm/server`.\n *\n * The core (`./form`) is pure TypeScript with zero React imports, so it\n * already runs anywhere Node does; what the server lacked is an entry\n * that never pulls React into the module graph at all — the main entry\n * re-exports the hooks/components, and a Server Action or RSC that only\n * wants to check a payload should not have to depend on them. This module\n * is that entry: values in, one whole-form validation round, structured\n * result out.\n *\n * It plays the role TanStack Form gives `createServerValidate`, minus the\n * action prop: their API wraps validation inside a generated server\n * action, while react-f0rm keeps the values store the single source of\n * truth — {@link validateValues} is a plain function over values (the\n * same contract the client-side `trigger` has over a form instance), so\n * it composes into any server framework's handler instead of owning it.\n *\n * Like the resolvers and devtools it is intentionally NOT re-exported\n * from the main entry: importing `react-f0rm/server` is the only way this\n * code reaches a bundle, so client builds that never validate on the\n * server stay at baseline size.\n */\nimport createForm, {getErrors, getValues, trigger} from './form';\nimport type {FieldErrorEntry, Options, ValidationOutcome} from './form';\n\n// Building a branded ValidationOutcome server-side (schema adapters, or a\n// hand-written validate that returns parsed values) needs the brand symbol\n// itself; importing it from the package root would drag the React graph\n// back in, so it is re-exported here together with its result type.\nexport {VALIDATION_OUTCOME} from './form';\nexport type {ValidationOutcome};\n\n/** The outcome of {@link validateValues}: the error-free flag, the values\n * once the round has landed (schema-coerced where the validator produced\n * parsed values), and every error the round wrote as flat entries. */\nexport type ValidateValuesResult<T extends Record<string, any> = any> = {\n /** Whether the round landed no errors — `trigger`'s boolean. An invalid\n * payload is a normal outcome, never a rejection: unlike\n * `ensureValidate`, server callers learn validity from data instead of\n * catching, because both branches are interesting on the server\n * (persist vs. bounce back to the client). */\n valid: boolean;\n /** The values after the round. When the validator returned a branded\n * {@link ValidationOutcome} with `values`, those parsed values are the\n * baseline `getValues` layers over the input — schema coerce/transform\n * output included — so this is the tree to persist or feed onward, not\n * necessarily the object passed in. Deep-equals the input otherwise. */\n values: T;\n /** Every error the round landed, flattened to `{path, type, message}`\n * entries — the same list {@link getErrors} hands out on the client.\n * Feed it to `setServerErrors` to land a failed round back on the\n * client form (the Server Actions bridge; see the docs' Server Actions\n * guide). */\n errors: FieldErrorEntry[];\n};\n\n/** Append one value under `key` into `fd`. Arrays and FileLists flatten\n * to one entry per item (FormData's multi-entry convention); Files keep\n * their name; Dates become ISO strings; other objects JSON.stringify;\n * booleans/numbers/strings String() as a native form submit would. */\nfunction appendFormDataValue(fd: FormData, key: string, value: any): void {\n if (value == null) return;\n if (Array.isArray(value)) {\n for (const item of value) appendFormDataValue(fd, key, item);\n return;\n }\n if (typeof FileList !== 'undefined' && value instanceof FileList) {\n for (let i = 0; i < value.length; i++) fd.append(key, value.item(i)!);\n return;\n }\n if (typeof File !== 'undefined' && value instanceof File) {\n fd.append(key, value, value.name);\n return;\n }\n if (typeof Blob !== 'undefined' && value instanceof Blob) {\n fd.append(key, value);\n return;\n }\n if (value instanceof Date) {\n fd.append(key, value.toISOString());\n return;\n }\n fd.append(\n key,\n typeof value === 'object' ? JSON.stringify(value) : String(value)\n );\n}\n\n/**\n * Convert a values object into FormData — the transport shape React 19\n * Server Actions and multipart handlers expect. Built to pair with the\n * `<Form action>` prop and `validateValues`: the validated (schema-coerced)\n * values tree lands in the server action as FormData, files included.\n *\n * Array values become multiple entries under the same key (FormData's\n * native multi-value convention); File values keep their name; Dates\n * become ISO strings; plain objects JSON.stringify; null/undefined are\n * skipped.\n */\nexport function formDataFromValues(values: Record<string, any>): FormData {\n const fd = new FormData();\n for (const key of Object.keys(values)) {\n appendFormDataValue(fd, key, values[key]);\n }\n return fd;\n}\n\n/**\n * Validate a payload of values on the server — no form instance, no\n * React.\n *\n * Spins up a throwaway form from `options` (with `initialValues` forced\n * to `values`), runs one whole-form `trigger`, and reads the outcome\n * back. `trigger` never rejects and waits out async validators and any\n * `validateDebounce` window, so a single `await` drains the whole round —\n * the returned `valid`/`errors` are final, not a snapshot mid-flight.\n *\n * Validation comes from `options.validate` — the form-level validator.\n * Field validators register through mounted fields (`useField`), and\n * nothing is mounted on the server, so they cannot participate by\n * construction; pass a schema-backed form validator\n * (`standardSchemaFormValidator(schema)` from\n * `react-f0rm/resolvers/standard-schema`) or a hand-written `validate`\n * instead. `mode`/`reValidateMode` are equally inert here — there are no\n * field events to gate — and may be omitted.\n *\n * When the validator returns a branded {@link ValidationOutcome} whose\n * `values` carry the schema's output, those parsed values become the\n * form's parsedValues baseline, so `result.values` flows coercion and\n * transforms forward (`z.coerce.number()` turning `'42'` into `42`, and\n * friends). Persist that tree; on the client the same schema round runs\n * again on submit, keeping one validation source across the boundary.\n *\n * Safe to call from Node, Server Actions and RSC — the module graph is\n * this file plus the pure core, zero React.\n *\n * @param values the payload to validate; becomes the form's initialValues\n * @param options form options; `validate` is where the rules come from\n * @return the settled round: `valid`, the (possibly parsed) values, and\n * the flat error entries\n */\nexport async function validateValues<T extends Record<string, any> = any>(\n values: T,\n options?: Options<T>\n): Promise<ValidateValuesResult<T>> {\n const form = createForm({...options, initialValues: values});\n const valid = await trigger(form);\n return {valid, values: getValues(form), errors: getErrors(form)};\n}\n","import {\n create as createEmitter,\n emit,\n setMaxListeners\n} from '@for-fun/event-emitter';\nimport type {EventEmitter} from '@for-fun/event-emitter';\nimport createPath from './path';\nimport type {Name, Path} from './path';\nimport type {FieldPath} from './types';\nimport {isPromise} from './util';\nimport {setInitialValues} from './core/values';\nimport type {SetFocusOptions} from './core/focus';\nimport type {VALIDATION_OUTCOME} from './core/errors';\n\n// The implementation is split by concern under ./core (values, errors,\n// touched, dirty, validate, change, submit, focus; module-private shared\n// state lives in ./core/internals, which is deliberately not re-exported).\n// This file keeps the public types and the create factory, and re-exports\n// every public function — the single import surface the rest of the\n// package (hooks, components, server, persist, resolvers) consumes.\nexport type {Name};\nexport type {FieldPath, PathValue} from './types';\n\n/** Dev-only flag, replaced at build time (rollup.config.js `replace`);\n * defined for the test environment in vitest.config.ts. */\ndeclare const __DEV__: boolean;\n\n/** A field error: `type` identifies the error kind ('custom' for plain\n * string errors), `message` is the display text. */\nexport type FieldError = {type: string; message: string};\n\n/** A flattened entry from {@link getErrors}. */\nexport type FieldErrorEntry = {path: string; type: string; message: string};\n\n/** When a field is validated:\n * - `'onSubmit'` (default): only on submit\n * - `'onBlur'`: when the field loses focus\n * - `'onChange'`: on every change\n * - `'onTouched'`: on first blur, then on every change\n * - `'all'`: on both change and blur\n */\nexport type ValidationMode =\n 'onSubmit' | 'onBlur' | 'onChange' | 'onTouched' | 'all';\n\n/** When a field is re-validated after it already has an error:\n * - `'onChange'` (default): on every change\n * - `'onBlur'`: when the field loses focus\n * - `'onSubmit'`: only on submit (no live re-validation)\n */\nexport type ReValidateMode = 'onChange' | 'onBlur' | 'onSubmit';\n\n/** Structured form-level validate result: `errors` uses the same nested\n * shape a plain error record uses, `values` is the schema's parsed output\n * (coerce/transform results included). Either side may be omitted.\n *\n * The brand constant itself lives in the errors module (the leaf module of\n * the core dependency graph — every consumer imports it from there) and is\n * re-exported below with `export *`. */\nexport type ValidationOutcome<T> = {\n [VALIDATION_OUTCOME]: true;\n errors?: Record<string, any>;\n values?: T;\n};\n\n/** What a form-level validate function may return: a plain nested error\n * record (flattened into field errors — the long-standing shape), or a\n * branded {@link ValidationOutcome} whose `values` become the form's\n * parsedValues baseline. */\nexport type ValidateResult<T> =\n | Record<string, any>\n | ValidationOutcome<T>\n | Promise<Record<string, any> | ValidationOutcome<T>>;\n\n/** Context passed to a form-level `validate` function's second argument.\n * `signal` aborts as soon as the round is superseded — a newer round\n * started (which only happens under a positive `validateDebounce`, where\n * kicks merge into windows) — so async validators can cancel their\n * underlying work instead of racing a stale result home. Stale results\n * are dropped independently by the round gate, so validators that ignore\n * the signal stay correct too; the same contract field-level validators\n * get through their own `meta`. */\nexport type FormValidateMeta<T extends Record<string, any> = any> = {\n form: Form<T>;\n signal: AbortSignal;\n};\n\n/** Form-level validator: receives all values (plus {@link\n * FormValidateMeta} as an optional second argument) and returns a\n * {@link ValidateResult} — sync or async — or `undefined`/nothing when\n * valid (the runtime skips falsy results, so implicit-return callbacks\n * type-check). */\nexport type FormValidateFn<T extends Record<string, any> = any> = (\n values: T,\n meta: FormValidateMeta<T>\n) => ValidateResult<T> | undefined;\n\n/**\n * The emitter event table for {@link Form.emitter}: each event's payload\n * tuple. Path-carrying events declare an optional single `Path` payload —\n * emit sites send it for single-field mutations and omit it for bulk\n * payload-less broadcasts (reset, setInitialValues, clear-all), both of\n * which subscribers handle. `focusError` carries the target's path key\n * plus optional {@link SetFocusOptions}.\n */\nexport type FormEvents =\n | ['change', [path?: Path]]\n | ['errors', [path?: Path]]\n | ['touched', [path?: Path]]\n | ['validating', [path?: Path]]\n | ['submitting', []]\n | ['submitCount', []]\n | ['submitSuccessful', []]\n | ['reset', []]\n | ['disabled', []]\n | ['loading', []]\n | ['focusError', [key: string, options?: SetFocusOptions]];\n\nexport type Form<T extends Record<string, any> = any> = {\n emitter: EventEmitter<FormEvents>;\n mode: ValidationMode;\n reValidateMode: ReValidateMode;\n initialValues: T;\n values: Map<string, any>;\n /** Tombstones of unregistered field paths (JSON path keys): reading or\n * merging values must not fall back to initialValues for these paths. */\n deleted: Set<string>;\n /** Every error registered for a field, as a non-empty array (the\n * write-side {@link setErrorByPath} normalizes to this invariant, so\n * readers never need to guard against an empty list). Readers wanting\n * the display error take the first entry ({@link getError}); readers\n * wanting all of them use {@link getFieldErrors}. */\n errors: Map<string, FieldError[]>;\n touched: Set<string>;\n /** Per-field validation kicks, registered by {@link\n * registerValidatorByPath} (`useValidate` is the React-side\n * registration): each is the field's debounce/lock-aware kick —\n * invoking it validates the field's current value. `trigger` /\n * `ensureValidate` run every entry; the user-change gate ({@link\n * userChangeByPath}) runs the entry at the changed path. */\n validators: Map<string, () => void>;\n validating: Set<string>;\n /** Parsed values from the last successful schema validation: the\n * schema's complete output tree (coerced/transformed values included).\n * Sits between initialValues and the values Map in {@link getValues}\n * until `reset`/`setInitialValues` clears it. Never affects dirty\n * state — that compares live edits against initialValues only. */\n parsedValues: T | undefined;\n /** Form-level validator, seeded from {@link Options.validate}. May\n * receive a second {@link FormValidateMeta} argument. */\n validate?: FormValidateFn<T>;\n /** Delay in milliseconds before the form-level `validate` runs; seeded\n * from {@link Options.validateDebounce} and fixed at create time. */\n validateDebounce?: number;\n /** Path keys (JSON-stringified segments) of the fields whose user\n * changes re-run the form-level `validate`; normalized from {@link\n * Options.validateDeps} at create time and fixed thereafter. */\n validateDeps?: ReadonlySet<string>;\n isSubmitting: boolean;\n submitCount: number;\n isSubmitSuccessful: boolean | undefined;\n /** True while an async {@link Options.initialValues} source (a Promise,\n * or a thunk returning one) is still pending — the form starts empty\n * and the resolved values become the baseline via setInitialValues when\n * it lands. Flips through the payload-less 'loading' event\n * (`useIsLoading` / `useFormState().isLoading`). */\n isLoading: boolean;\n /** Form-level default for a bound field's unmount behavior, seeded from\n * {@link Options.shouldUnregister}: `true` (the default) tombstones an\n * unmounted field, `false` keeps its value (react-hook-form's\n * `shouldUnregister` semantics). A field's own `shouldUnregister` option\n * overrides this. */\n shouldUnregister?: boolean;\n /** Form-level disabled flag, OR-ed into every bound field's `disabled`\n * (form flag || the field's own option). Seeded from\n * {@link Options}.disabled at create time and toggled at runtime with\n * {@link setDisabled}, which emits a payload-less 'disabled' event so\n * subscribed fields re-render. */\n disabled: boolean;\n};\n\nexport type Options<T extends Record<string, any> = any> = {\n /**\n * The values baseline. Sync objects seed immediately (SSR renders\n * them). Async sources — a Promise, or a thunk returning a value or\n * Promise (react-hook-form's async `defaultValues` shape) — start the\n * form empty with `isLoading: true` and land the resolved values as\n * the baseline via setInitialValues once they resolve: value\n * subscribers re-sync, dirty/touched state starts clean, and a later\n * `reset()` returns to the resolved baseline. A rejected source flips\n * isLoading back to false, keeps the form empty, and logs the error in\n * DEV — attach a `.catch` on the source itself to handle it. The thunk\n * runs at create time: keep its identity stable (module scope or\n * useMemo) when passing it inline, and note StrictMode double-invokes\n * it in development, like every render-phase call.\n */\n initialValues?: T | Promise<T> | (() => T | Promise<T>);\n /** When fields are validated. Defaults to `'onSubmit'`. See\n * {@link ValidationMode}. */\n mode?: ValidationMode;\n /** When a field is re-validated after it already has an error — it only\n * takes effect once the field has an error. Defaults to `'onChange'`. See\n * {@link ReValidateMode}. */\n reValidateMode?: ReValidateMode;\n /**\n * Form-level validator. Returns a record of errors keyed by field path;\n * nested objects are flattened ('a.b' style) and array values contribute\n * every non-empty string they hold as separate errors (zod `flatten()`\n * formErrors style). Schema adapters instead return a branded\n * {@link ValidationOutcome}: `errors` flattens the same way, `values`\n * (the schema's parsed output) becomes the form's parsedValues baseline\n * that {@link getValues} layers over initialValues.\n */\n validate?: FormValidateFn<T>;\n /**\n * Milliseconds to debounce the form-level `validate`: kicks from\n * `trigger`/`ensureValidate`/submit inside the window merge into one\n * run, and while the timer is pending the form counts as validating,\n * so `trigger` and submit wait the window out — the same contract the\n * per-field `validateDebounce` gives field validators. The merged run\n * reads the values current when its timer fires. Defaults to `0`\n * (validate runs immediately, exactly as before this option existed).\n */\n validateDebounce?: number;\n /** Fields whose user changes re-run the form-level `validate` — the\n * cross-field dependency list (password-confirm mismatch and friends).\n * Each entry is a field path ('password', 'user.email', 'items.0.qty');\n * a user change to a listed field re-runs the form-level `validate`\n * under the same mode/`reValidateMode` gating the field's own\n * validator gets. Omit it and the form-level `validate` only runs on\n * `trigger`/submit, exactly as before this option existed.\n *\n * Opting in also changes what a re-run may clear: each round first\n * drops the errors the previous round wrote (paths it flattened onto),\n * so a dep change that fixes the cross-field error makes it disappear.\n * Errors the round never wrote — field validators', `setServerErrors`,\n * manual `setError` — are never touched. TanStack Form's counterpart is\n * `onChangeListenTo` (v1) / validator `triggers` (v2 alpha). */\n validateDeps?: FieldPath<T>[];\n /**\n * Form-level default for a bound field's unmount behavior. `true` (the\n * default) tombstones an unmounted field — it drops out of\n * `getValues()` instead of reviving its initial value (this library's\n * historical default); `false` keeps the value, matching\n * react-hook-form's `shouldUnregister`. A field's own\n * `useField({shouldUnregister})` option overrides the form-level flag\n * in either direction.\n */\n shouldUnregister?: boolean;\n /** Start the form with every bound field disabled — the flag bound\n * fields OR with their own `disabled` option (a field cannot opt out\n * of a disabled form). Toggle later with {@link setDisabled}.\n * Defaults to `false`. */\n disabled?: boolean;\n};\n\n/**\n * Create form instance\n * @param options\n * @return form instance\n */\nexport default function create<T extends Record<string, any> = any>(\n options?: Options<T>\n): Form<T> {\n const emitter = createEmitter<FormEvents>();\n // A form legitimately accumulates one listener per mounted field per\n // event (useField subscribes change/errors/disabled/focusError…), so\n // the emitter's default max-listener warning would fire in DEV for any\n // form over ~10 fields. Field subscriptions are removed on unmount —\n // there is nothing to leak — so the warning would only be noise: raise\n // the cap to unlimited for form emitters.\n setMaxListeners(emitter, 0);\n // Async initialValues: a thunk is invoked here (create-time, like every\n // other option resolution); a promise-typed result starts the loading\n // cycle below instead of seeding.\n let source: any = options?.initialValues ?? {};\n if (typeof source === 'function') source = (source as () => unknown)();\n const form: Form<T> = {\n emitter,\n ...options,\n mode: options?.mode ?? 'onSubmit',\n reValidateMode: options?.reValidateMode ?? 'onChange',\n disabled: options?.disabled ?? false,\n validateDeps: options?.validateDeps\n ? new Set(options.validateDeps.map(dep => createPath(dep).key))\n : undefined,\n initialValues: {} as T,\n values: new Map(),\n deleted: new Set(),\n errors: new Map(),\n touched: new Set(),\n validators: new Map(),\n validating: new Set(),\n parsedValues: undefined,\n isSubmitting: false,\n submitCount: 0,\n isSubmitSuccessful: undefined,\n isLoading: false\n };\n if (isPromise(source)) {\n // The form starts empty; when the source resolves, its values become\n // the baseline through setInitialValues (payload-less 'change', so\n // every value subscriber re-syncs). The loading flag flips through\n // the 'loading' event before and after — no subscriber exists during\n // the first render, so the synchronous first emit is a safe no-op.\n form.isLoading = true;\n emit(emitter, 'loading');\n Promise.resolve(source).then(\n resolved => {\n form.isLoading = false;\n emit(emitter, 'loading');\n setInitialValues(form, resolved ?? {});\n },\n error => {\n form.isLoading = false;\n emit(emitter, 'loading');\n // The caller's own catch on the source sees the rejection;\n // rethrowing here would only duplicate it as an unhandled\n // promise rejection. Surface it in DEV instead.\n if (__DEV__) {\n // eslint-disable-next-line no-console -- dev-only diagnostics\n console.error('react-f0rm: async initialValues rejected', error);\n }\n }\n );\n } else {\n form.initialValues = source as T;\n }\n return form;\n}\n\nexport * from './core/values';\nexport * from './core/errors';\nexport * from './core/touched';\nexport * from './core/dirty';\nexport * from './core/validate';\nexport * from './core/change';\nexport * from './core/submit';\nexport * from './core/focus';\n"],"names":["appendFormDataValue","fd","key","value","Array","isArray","item","FileList","i","length","append","File","name","Blob","Date","toISOString","JSON","stringify","String","values","FormData","Object","keys","async","options","form","emitter","createEmitter","setMaxListeners","source","initialValues","mode","reValidateMode","disabled","validateDeps","Set","map","dep","createPath","Map","deleted","errors","touched","validators","validating","parsedValues","isSubmitting","submitCount","isSubmitSuccessful","isLoading","isPromise","emit","Promise","resolve","then","resolved","setInitialValues","error","createForm","valid","trigger","getValues","getErrors"],"mappings":"4KA6DA,SAASA,EAAoBC,EAAcC,EAAaC,GACtD,GAAa,MAATA,EACJ,GAAIC,MAAMC,QAAQF,GAChB,IAAA,MAAWG,KAAQH,EAAOH,EAAoBC,EAAIC,EAAKI,QAGzD,GAAwB,oBAAbC,UAA4BJ,aAAiBI,SACtD,IAAA,IAASC,EAAI,EAAGA,EAAIL,EAAMM,OAAQD,IAAKP,EAAGS,OAAOR,EAAKC,EAAMG,KAAKE,QAG/C,oBAATG,MAAwBR,aAAiBQ,KAClDV,EAAGS,OAAOR,EAAKC,EAAOA,EAAMS,MAGV,oBAATC,MAAwBV,aAAiBU,KAClDZ,EAAGS,OAAOR,EAAKC,GAGbA,aAAiBW,KACnBb,EAAGS,OAAOR,EAAKC,EAAMY,eAGvBd,EAAGS,OACDR,EACiB,iBAAVC,EAAqBa,KAAKC,UAAUd,GAASe,OAAOf,GAE/D,4EAaO,SAA4BgB,GACjC,MAAMlB,EAAK,IAAImB,SACf,IAAA,MAAWlB,KAAOmB,OAAOC,KAAKH,GAC5BnB,EAAoBC,EAAIC,EAAKiB,EAAOjB,IAEtC,OAAOD,CACT,yBAoCAsB,eACEJ,EACAK,GAEA,MAAMC,ECkHR,SACED,GAEA,MAAME,EAAUC,EAAAA,SAOhBC,EAAAA,gBAAgBF,EAAS,GAIzB,IAAIG,EAAcL,GAASM,eAAiB,CAAA,EACtB,mBAAXD,IAAuBA,EAAUA,KAC5C,MAAMJ,EAAgB,CACpBC,aACGF,EACHO,KAAMP,GAASO,MAAQ,WACvBC,eAAgBR,GAASQ,gBAAkB,WAC3CC,SAAUT,GAASS,WAAY,EAC/BC,aAAcV,GAASU,aACnB,IAAIC,IAAIX,EAAQU,aAAaE,IAAIC,GAAOC,EAAAA,OAAWD,GAAKnC,WACxD,EACJ4B,cAAe,CAAA,EACfX,WAAYoB,IACZC,YAAaL,IACbM,WAAYF,IACZG,YAAaP,IACbQ,eAAgBJ,IAChBK,eAAgBT,IAChBU,kBAAc,EACdC,cAAc,EACdC,YAAa,EACbC,wBAAoB,EACpBC,WAAW,GA+Bb,OA7BIC,EAAAA,UAAUrB,IAMZJ,EAAKwB,WAAY,EACjBE,EAAAA,KAAKzB,EAAS,WACd0B,QAAQC,QAAQxB,GAAQyB,KACtBC,IACE9B,EAAKwB,WAAY,EACjBE,EAAAA,KAAKzB,EAAS,WACd8B,mBAAiB/B,EAAM8B,GAAY,KAErCE,IACEhC,EAAKwB,WAAY,EACjBE,EAAAA,KAAKzB,EAAS,cAWlBD,EAAKK,cAAgBD,EAEhBJ,CACT,CDtLeiC,CAAW,IAAIlC,EAASM,cAAeX,IAEpD,MAAO,CAACwC,YADYC,EAAAA,QAAQnC,GACbN,OAAQ0C,EAAAA,UAAUpC,GAAOgB,OAAQqB,YAAUrC,GAC5D"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { FieldErrorEntry, Options } from '../index.js';
|
|
2
|
+
export { l as VALIDATION_OUTCOME, n as ValidationOutcome } from '../form-CvmWHUrd.js';
|
|
3
|
+
import '@for-fun/event-emitter';
|
|
4
|
+
|
|
5
|
+
/** The outcome of {@link validateValues}: the error-free flag, the values
|
|
6
|
+
* once the round has landed (schema-coerced where the validator produced
|
|
7
|
+
* parsed values), and every error the round wrote as flat entries. */
|
|
8
|
+
type ValidateValuesResult<T extends Record<string, any> = any> = {
|
|
9
|
+
/** Whether the round landed no errors — `trigger`'s boolean. An invalid
|
|
10
|
+
* payload is a normal outcome, never a rejection: unlike
|
|
11
|
+
* `ensureValidate`, server callers learn validity from data instead of
|
|
12
|
+
* catching, because both branches are interesting on the server
|
|
13
|
+
* (persist vs. bounce back to the client). */
|
|
14
|
+
valid: boolean;
|
|
15
|
+
/** The values after the round. When the validator returned a branded
|
|
16
|
+
* {@link ValidationOutcome} with `values`, those parsed values are the
|
|
17
|
+
* baseline `getValues` layers over the input — schema coerce/transform
|
|
18
|
+
* output included — so this is the tree to persist or feed onward, not
|
|
19
|
+
* necessarily the object passed in. Deep-equals the input otherwise. */
|
|
20
|
+
values: T;
|
|
21
|
+
/** Every error the round landed, flattened to `{path, type, message}`
|
|
22
|
+
* entries — the same list {@link getErrors} hands out on the client.
|
|
23
|
+
* Feed it to `setServerErrors` to land a failed round back on the
|
|
24
|
+
* client form (the Server Actions bridge; see the docs' Server Actions
|
|
25
|
+
* guide). */
|
|
26
|
+
errors: FieldErrorEntry[];
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Convert a values object into FormData — the transport shape React 19
|
|
30
|
+
* Server Actions and multipart handlers expect. Built to pair with the
|
|
31
|
+
* `<Form action>` prop and `validateValues`: the validated (schema-coerced)
|
|
32
|
+
* values tree lands in the server action as FormData, files included.
|
|
33
|
+
*
|
|
34
|
+
* Array values become multiple entries under the same key (FormData's
|
|
35
|
+
* native multi-value convention); File values keep their name; Dates
|
|
36
|
+
* become ISO strings; plain objects JSON.stringify; null/undefined are
|
|
37
|
+
* skipped.
|
|
38
|
+
*/
|
|
39
|
+
declare function formDataFromValues(values: Record<string, any>): FormData;
|
|
40
|
+
/**
|
|
41
|
+
* Validate a payload of values on the server — no form instance, no
|
|
42
|
+
* React.
|
|
43
|
+
*
|
|
44
|
+
* Spins up a throwaway form from `options` (with `initialValues` forced
|
|
45
|
+
* to `values`), runs one whole-form `trigger`, and reads the outcome
|
|
46
|
+
* back. `trigger` never rejects and waits out async validators and any
|
|
47
|
+
* `validateDebounce` window, so a single `await` drains the whole round —
|
|
48
|
+
* the returned `valid`/`errors` are final, not a snapshot mid-flight.
|
|
49
|
+
*
|
|
50
|
+
* Validation comes from `options.validate` — the form-level validator.
|
|
51
|
+
* Field validators register through mounted fields (`useField`), and
|
|
52
|
+
* nothing is mounted on the server, so they cannot participate by
|
|
53
|
+
* construction; pass a schema-backed form validator
|
|
54
|
+
* (`standardSchemaFormValidator(schema)` from
|
|
55
|
+
* `react-f0rm/resolvers/standard-schema`) or a hand-written `validate`
|
|
56
|
+
* instead. `mode`/`reValidateMode` are equally inert here — there are no
|
|
57
|
+
* field events to gate — and may be omitted.
|
|
58
|
+
*
|
|
59
|
+
* When the validator returns a branded {@link ValidationOutcome} whose
|
|
60
|
+
* `values` carry the schema's output, those parsed values become the
|
|
61
|
+
* form's parsedValues baseline, so `result.values` flows coercion and
|
|
62
|
+
* transforms forward (`z.coerce.number()` turning `'42'` into `42`, and
|
|
63
|
+
* friends). Persist that tree; on the client the same schema round runs
|
|
64
|
+
* again on submit, keeping one validation source across the boundary.
|
|
65
|
+
*
|
|
66
|
+
* Safe to call from Node, Server Actions and RSC — the module graph is
|
|
67
|
+
* this file plus the pure core, zero React.
|
|
68
|
+
*
|
|
69
|
+
* @param values the payload to validate; becomes the form's initialValues
|
|
70
|
+
* @param options form options; `validate` is where the rules come from
|
|
71
|
+
* @return the settled round: `valid`, the (possibly parsed) values, and
|
|
72
|
+
* the flat error entries
|
|
73
|
+
*/
|
|
74
|
+
declare function validateValues<T extends Record<string, any> = any>(values: T, options?: Options<T>): Promise<ValidateValuesResult<T>>;
|
|
75
|
+
|
|
76
|
+
export { formDataFromValues, validateValues };
|
|
77
|
+
export type { ValidateValuesResult };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{create as e,setMaxListeners as i,emit as t}from"@for-fun/event-emitter";import{c as n,i as a,a as o}from"../errors-CzWtwjO0.mjs";export{V as VALIDATION_OUTCOME}from"../errors-CzWtwjO0.mjs";import{s,g as r}from"../values-B1IV-6V4.mjs";import{t as d}from"../validate-B1Gdjeaq.mjs";function l(e,i,t){if(null!=t)if(Array.isArray(t))for(const n of t)l(e,i,n);else if("undefined"!=typeof FileList&&t instanceof FileList)for(let n=0;n<t.length;n++)e.append(i,t.item(n));else"undefined"!=typeof File&&t instanceof File?e.append(i,t,t.name):"undefined"!=typeof Blob&&t instanceof Blob?e.append(i,t):t instanceof Date?e.append(i,t.toISOString()):e.append(i,"object"==typeof t?JSON.stringify(t):String(t))}function f(e){const i=new FormData;for(const t of Object.keys(e))l(i,t,e[t]);return i}async function p(l,f){const p=function(o){const r=e();i(r,0);let d=o?.initialValues??{};"function"==typeof d&&(d=d());const l={emitter:r,...o,mode:o?.mode??"onSubmit",reValidateMode:o?.reValidateMode??"onChange",disabled:o?.disabled??!1,validateDeps:o?.validateDeps?new Set(o.validateDeps.map(e=>n(e).key)):void 0,initialValues:{},values:new Map,deleted:new Set,errors:new Map,touched:new Set,validators:new Map,validating:new Set,parsedValues:void 0,isSubmitting:!1,submitCount:0,isSubmitSuccessful:void 0,isLoading:!1};return a(d)?(l.isLoading=!0,t(r,"loading"),Promise.resolve(d).then(e=>{l.isLoading=!1,t(r,"loading"),s(l,e??{})},e=>{l.isLoading=!1,t(r,"loading")})):l.initialValues=d,l}({...f,initialValues:l});return{valid:await d(p),values:r(p),errors:o(p)}}export{f as formDataFromValues,p as validateValues};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|