react-f0rm 1.2.0 → 1.4.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.
Files changed (98) hide show
  1. package/README.md +90 -1062
  2. package/devtools.d.ts +1 -0
  3. package/devtools.js +1 -0
  4. package/dist/array-3hAjl6Bz.d.ts +160 -0
  5. package/dist/devtools/index.cjs.js +1 -1
  6. package/dist/devtools/index.cjs.js.map +1 -1
  7. package/dist/devtools/index.d.cts +34 -0
  8. package/dist/devtools/index.d.mts +34 -0
  9. package/dist/devtools/index.d.ts +3 -2
  10. package/dist/devtools/index.mjs +1 -1
  11. package/dist/devtools/index.mjs.map +1 -1
  12. package/dist/errors-8oPux7GR.cjs.js +2 -0
  13. package/dist/errors-8oPux7GR.cjs.js.map +1 -0
  14. package/dist/errors-ClH3eV1R.mjs +2 -0
  15. package/dist/errors-ClH3eV1R.mjs.map +1 -0
  16. package/dist/index.cjs.js +1 -1
  17. package/dist/index.cjs.js.map +1 -1
  18. package/dist/index.d.cts +1833 -0
  19. package/dist/index.d.mts +1833 -0
  20. package/dist/index.d.ts +678 -128
  21. package/dist/index.mjs +1 -1
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/index.umd.js +1210 -480
  24. package/dist/index.umd.js.map +1 -1
  25. package/dist/index.umd.min.js +2 -2
  26. package/dist/index.umd.min.js.map +1 -1
  27. package/dist/persist.cjs.js +1 -1
  28. package/dist/persist.cjs.js.map +1 -1
  29. package/dist/persist.d.cts +49 -0
  30. package/dist/persist.d.mts +49 -0
  31. package/dist/persist.mjs +1 -1
  32. package/dist/persist.mjs.map +1 -1
  33. package/dist/resolvers/standard-schema.cjs.js +1 -1
  34. package/dist/resolvers/standard-schema.cjs.js.map +1 -1
  35. package/dist/resolvers/standard-schema.d.cts +2 -0
  36. package/dist/resolvers/standard-schema.d.mts +2 -0
  37. package/dist/resolvers/standard-schema.d.ts +1 -67
  38. package/dist/resolvers/standard-schema.mjs +1 -1
  39. package/dist/resolvers/standard-schema.mjs.map +1 -1
  40. package/dist/resolvers/yup.cjs.js +1 -1
  41. package/dist/resolvers/yup.cjs.js.map +1 -1
  42. package/dist/resolvers/yup.d.cts +6 -0
  43. package/dist/resolvers/yup.d.mts +6 -0
  44. package/dist/resolvers/yup.d.ts +0 -1
  45. package/dist/resolvers/yup.mjs +1 -1
  46. package/dist/resolvers/yup.mjs.map +1 -1
  47. package/dist/resolvers/zod.cjs.js +1 -1
  48. package/dist/resolvers/zod.cjs.js.map +1 -1
  49. package/dist/resolvers/zod.d.cts +6 -0
  50. package/dist/resolvers/zod.d.mts +6 -0
  51. package/dist/resolvers/zod.d.ts +0 -1
  52. package/dist/resolvers/zod.mjs +1 -1
  53. package/dist/resolvers/zod.mjs.map +1 -1
  54. package/dist/server/index.cjs.js +1 -1
  55. package/dist/server/index.cjs.js.map +1 -1
  56. package/dist/server/index.d.cts +106 -0
  57. package/dist/server/index.d.mts +106 -0
  58. package/dist/server/index.d.ts +31 -2
  59. package/dist/server/index.mjs +1 -1
  60. package/dist/server/index.mjs.map +1 -1
  61. package/dist/standard-schema-CYbnugL7.d.ts +1184 -0
  62. package/dist/standardSchema-BpZvg0tZ.mjs +2 -0
  63. package/dist/standardSchema-BpZvg0tZ.mjs.map +1 -0
  64. package/dist/standardSchema-CxV_DhBO.cjs.js +2 -0
  65. package/dist/standardSchema-CxV_DhBO.cjs.js.map +1 -0
  66. package/dist/validate-BEjNsNvd.mjs +2 -0
  67. package/dist/validate-BEjNsNvd.mjs.map +1 -0
  68. package/dist/validate-BvN6hiEG.cjs.js +2 -0
  69. package/dist/validate-BvN6hiEG.cjs.js.map +1 -0
  70. package/dist/values-Bod74Kzu.mjs +2 -0
  71. package/dist/values-Bod74Kzu.mjs.map +1 -0
  72. package/dist/values-DKpOCD5J.cjs.js +2 -0
  73. package/dist/values-DKpOCD5J.cjs.js.map +1 -0
  74. package/package.json +90 -34
  75. package/persist.d.ts +1 -0
  76. package/persist.js +1 -0
  77. package/resolvers/standard-schema.d.ts +1 -0
  78. package/resolvers/standard-schema.js +1 -0
  79. package/resolvers/yup.d.ts +1 -0
  80. package/resolvers/yup.js +1 -0
  81. package/resolvers/zod.d.ts +1 -0
  82. package/resolvers/zod.js +1 -0
  83. package/server.d.ts +1 -0
  84. package/server.js +1 -0
  85. package/dist/errors-CxSjrWJO.cjs.js +0 -2
  86. package/dist/errors-CxSjrWJO.cjs.js.map +0 -1
  87. package/dist/errors-CzWtwjO0.mjs +0 -2
  88. package/dist/errors-CzWtwjO0.mjs.map +0 -1
  89. package/dist/form-CvmWHUrd.d.ts +0 -423
  90. package/dist/validate-B1Gdjeaq.mjs +0 -2
  91. package/dist/validate-B1Gdjeaq.mjs.map +0 -1
  92. package/dist/validate-CUmNZqg6.d.ts +0 -238
  93. package/dist/validate-DAfz8Nbb.cjs.js +0 -2
  94. package/dist/validate-DAfz8Nbb.cjs.js.map +0 -1
  95. package/dist/values-B1IV-6V4.mjs +0 -2
  96. package/dist/values-B1IV-6V4.mjs.map +0 -1
  97. package/dist/values-CDNAYEOB.cjs.js +0 -2
  98. package/dist/values-CDNAYEOB.cjs.js.map +0 -1
@@ -1,2 +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{}}})};
1
+ "use strict";var e=require("@for-fun/event-emitter"),t=require("./values-DKpOCD5J.cjs.js"),r=require("./errors-8oPux7GR.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
2
  //# sourceMappingURL=persist.cjs.js.map
@@ -1 +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"}
1
+ {"version":3,"file":"persist.cjs.js","sources":["../src/subscribe.ts","../src/persist.ts"],"sourcesContent":["import {on} from './emitter';\nimport type {EventEmitter} from './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'`, `'status'` 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'`, `'status'` 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 | 'status'\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'`/`'status'`/\n * `'submitSuccessful'` are payload-less. Defaults to `'branch'` — the\n * intuitive linkage semantics, where subscribing to `'tags'` means the\n * 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/** The handle {@link watch} returns: a subscribe/getSnapshot pair any\n * reactive runtime can bind to — React's `useSyncExternalStore(subscribe,\n * getSnapshot)`, a Solid signal, a Vue ref, a Svelte store. The handle\n * keeps one internal listener alive from creation, so `getSnapshot()` is\n * always fresh (a read never returns a pre-write value even with no\n * consumer subscribed); `subscribe` adds a consumer callback and returns\n * its own unsubscribe. Call {@link WatchHandle.dispose} when the handle's\n * lifetime ends (adapter teardown, effect cleanup). */\nexport type WatchHandle<T> = {\n /** Read the current snapshot. Recomputed on every heard event (and on\n * first read), cached in between — repeated reads share one reference\n * until the watched state actually changes. */\n getSnapshot: () => T;\n /** Register a change listener; returns the unsubscribe function. The\n * listener fires only when the projection observably changed: with an\n * `isEqual` comparator the getter is re-run per event and an equal\n * verdict skips the callback entirely (TanStack's `useSelector`\n * contract); without one, every heard event wakes the listener. */\n subscribe: (invalidate: () => void) => () => void;\n /** Remove the internal listener and every consumer callback. The handle\n * is dead afterwards — reads return the last cached value and no\n * callback ever fires again. */\n dispose: () => void;\n};\n\n/**\n * Watch a projection of form state without React — the framework-free\n * counterpart of {@link useWatch} (same signature, same `isEqual`\n * bailout), exported as a named top-level function so it is tree-shaken\n * when unused. Headless adapters (Solid/Vue/Svelte bridges, imperative\n * autosave/analytics code) consume the returned\n * {@link WatchHandle}: read the current snapshot through `getSnapshot()`\n * and re-read (or re-render) whenever `subscribe`'s listener fires.\n *\n * The handle subscribes eagerly at creation, so `getSnapshot()` never\n * returns a stale value — including reads with no consumer subscribed.\n * `getter` and `isEqual` are captured when `watch` is called: call it at\n * setup time, like a subscription, and {@link WatchHandle.dispose} it at\n * teardown. Every emission of `event` wakes the listener (payload-less\n * broadcasts included) — the wide surface `useWatch` uses; path-scoped\n * variants are the `useValue`-family hooks on the React side and\n * {@link subscribe} with a `name` on this side.\n *\n * ```js\n * const handle = watch(form, 'change', () => getValue(form, 'email'));\n * // imperative consumer:\n * const off = handle.subscribe(() => save(handle.getSnapshot()));\n * // React adapter (useWatch is this composition):\n * useSyncExternalStore(handle.subscribe, handle.getSnapshot, handle.getSnapshot);\n * handle.dispose(); // teardown\n * ```\n *\n * @param form the form to watch\n * @param event the event whose emissions invalidate the snapshot\n * @param getter the projection — read fresh state through the `get*`\n * readers inside it\n * @param isEqual optional equality check; an equal verdict after an event\n * skips the listeners entirely (wide getters returning fresh\n * references per call stop churning subscribers)\n */\nexport function watch<T>(\n form: Form,\n event: SubscribeEvent,\n getter: () => T,\n isEqual?: (prev: T, next: T) => boolean\n): WatchHandle<T> {\n const cache: {hasValue: boolean; value?: T} = {hasValue: false};\n const consumers = new Set<() => void>();\n const wake = () => {\n if (isEqual && cache.hasValue) {\n // Custom comparator: decide before waking consumers. Equal means\n // observably unchanged — keep the cached reference and skip. Unequal\n // stores the fresh snapshot so the next read needs no recompute.\n const next = getter();\n if (isEqual(cache.value as T, next)) return;\n cache.value = next;\n } else {\n cache.value = getter();\n cache.hasValue = true;\n }\n consumers.forEach(invalidate => invalidate());\n };\n // Eager: the cache tracks the form from creation, so reads are fresh\n // even before any consumer subscribes (and events emitted between\n // watch() and subscribe() are never missed).\n const off = on(form.emitter, event, wake);\n return {\n getSnapshot: () => {\n if (!cache.hasValue) {\n cache.value = getter();\n cache.hasValue = true;\n }\n return cache.value as T;\n },\n subscribe: (invalidate: () => void) => {\n consumers.add(invalidate);\n return () => {\n consumers.delete(invalidate);\n };\n },\n dispose: () => {\n off();\n consumers.clear();\n }\n };\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,CAkJO,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,EA9F1B,SACLI,EACAJ,EACAR,EACAwB,GAEA,OAAOb,KAAGC,EAASJ,EAAQiB,SACT,IAAZA,GAAyBA,EAAQzB,MAAQA,GAAKwB,KAEtD,CAsFQE,CAAWrB,EAAKO,QAASJ,EAAOc,EAAKtB,IAAKU,GAjI3C,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,CAgHQG,CAAYtB,EAAKO,QAASJ,EAAOc,EAAMb,EAAOC,KAEpD,OAA+B,IAAxBS,EAAaS,OAChBT,EAAa,GACb,IAAMA,EAAaU,QAAQC,GAAeA,IAChD,qBC/HO,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 };
@@ -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 CHANGED
@@ -1,2 +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};
1
+ import{on as e}from"@for-fun/event-emitter";import{s as t,g as r}from"./values-Bod74Kzu.mjs";import{c as n}from"./errors-ClH3eV1R.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
2
  //# sourceMappingURL=persist.mjs.map
@@ -1 +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
+ {"version":3,"file":"persist.mjs","sources":["../src/subscribe.ts","../src/persist.ts"],"sourcesContent":["import {on} from './emitter';\nimport type {EventEmitter} from './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'`, `'status'` 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'`, `'status'` 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 | 'status'\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'`/`'status'`/\n * `'submitSuccessful'` are payload-less. Defaults to `'branch'` — the\n * intuitive linkage semantics, where subscribing to `'tags'` means the\n * 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/** The handle {@link watch} returns: a subscribe/getSnapshot pair any\n * reactive runtime can bind to — React's `useSyncExternalStore(subscribe,\n * getSnapshot)`, a Solid signal, a Vue ref, a Svelte store. The handle\n * keeps one internal listener alive from creation, so `getSnapshot()` is\n * always fresh (a read never returns a pre-write value even with no\n * consumer subscribed); `subscribe` adds a consumer callback and returns\n * its own unsubscribe. Call {@link WatchHandle.dispose} when the handle's\n * lifetime ends (adapter teardown, effect cleanup). */\nexport type WatchHandle<T> = {\n /** Read the current snapshot. Recomputed on every heard event (and on\n * first read), cached in between — repeated reads share one reference\n * until the watched state actually changes. */\n getSnapshot: () => T;\n /** Register a change listener; returns the unsubscribe function. The\n * listener fires only when the projection observably changed: with an\n * `isEqual` comparator the getter is re-run per event and an equal\n * verdict skips the callback entirely (TanStack's `useSelector`\n * contract); without one, every heard event wakes the listener. */\n subscribe: (invalidate: () => void) => () => void;\n /** Remove the internal listener and every consumer callback. The handle\n * is dead afterwards — reads return the last cached value and no\n * callback ever fires again. */\n dispose: () => void;\n};\n\n/**\n * Watch a projection of form state without React — the framework-free\n * counterpart of {@link useWatch} (same signature, same `isEqual`\n * bailout), exported as a named top-level function so it is tree-shaken\n * when unused. Headless adapters (Solid/Vue/Svelte bridges, imperative\n * autosave/analytics code) consume the returned\n * {@link WatchHandle}: read the current snapshot through `getSnapshot()`\n * and re-read (or re-render) whenever `subscribe`'s listener fires.\n *\n * The handle subscribes eagerly at creation, so `getSnapshot()` never\n * returns a stale value — including reads with no consumer subscribed.\n * `getter` and `isEqual` are captured when `watch` is called: call it at\n * setup time, like a subscription, and {@link WatchHandle.dispose} it at\n * teardown. Every emission of `event` wakes the listener (payload-less\n * broadcasts included) — the wide surface `useWatch` uses; path-scoped\n * variants are the `useValue`-family hooks on the React side and\n * {@link subscribe} with a `name` on this side.\n *\n * ```js\n * const handle = watch(form, 'change', () => getValue(form, 'email'));\n * // imperative consumer:\n * const off = handle.subscribe(() => save(handle.getSnapshot()));\n * // React adapter (useWatch is this composition):\n * useSyncExternalStore(handle.subscribe, handle.getSnapshot, handle.getSnapshot);\n * handle.dispose(); // teardown\n * ```\n *\n * @param form the form to watch\n * @param event the event whose emissions invalidate the snapshot\n * @param getter the projection — read fresh state through the `get*`\n * readers inside it\n * @param isEqual optional equality check; an equal verdict after an event\n * skips the listeners entirely (wide getters returning fresh\n * references per call stop churning subscribers)\n */\nexport function watch<T>(\n form: Form,\n event: SubscribeEvent,\n getter: () => T,\n isEqual?: (prev: T, next: T) => boolean\n): WatchHandle<T> {\n const cache: {hasValue: boolean; value?: T} = {hasValue: false};\n const consumers = new Set<() => void>();\n const wake = () => {\n if (isEqual && cache.hasValue) {\n // Custom comparator: decide before waking consumers. Equal means\n // observably unchanged — keep the cached reference and skip. Unequal\n // stores the fresh snapshot so the next read needs no recompute.\n const next = getter();\n if (isEqual(cache.value as T, next)) return;\n cache.value = next;\n } else {\n cache.value = getter();\n cache.hasValue = true;\n }\n consumers.forEach(invalidate => invalidate());\n };\n // Eager: the cache tracks the form from creation, so reads are fresh\n // even before any consumer subscribes (and events emitted between\n // watch() and subscribe() are never missed).\n const off = on(form.emitter, event, wake);\n return {\n getSnapshot: () => {\n if (!cache.hasValue) {\n cache.value = getter();\n cache.hasValue = true;\n }\n return cache.value as T;\n },\n subscribe: (invalidate: () => void) => {\n consumers.add(invalidate);\n return () => {\n consumers.delete(invalidate);\n };\n },\n dispose: () => {\n off();\n consumers.clear();\n }\n };\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,CAkJO,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,EA9F1B,SACLI,EACAJ,EACAR,EACAwB,GAEA,OAAOb,EAAGC,EAASJ,EAAQiB,SACT,IAAZA,GAAyBA,EAAQzB,MAAQA,GAAKwB,KAEtD,CAsFQE,CAAWrB,EAAKO,QAASJ,EAAOc,EAAKtB,IAAKU,GAjI3C,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,CAgHQG,CAAYtB,EAAKO,QAASJ,EAAOc,EAAMb,EAAOC,KAEpD,OAA+B,IAAxBS,EAAaS,OAChBT,EAAa,GACb,IAAMA,EAAaU,QAAQC,GAAeA,IAChD,CC/HO,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";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)}};
1
+ "use strict";var r=require("../standardSchema-CxV_DhBO.cjs.js");require("../errors-8oPux7GR.cjs.js"),require("@for-fun/event-emitter"),exports.hasStandardProps=r.hasStandardProps,exports.standardSchemaFormValidator=r.schemaToFormValidator,exports.standardSchemaResolver=r.schemaToFieldValidator;
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 {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"}
1
+ {"version":3,"file":"standard-schema.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export { I as InferSchemaValues, ad as StandardSchemaIssue, S as StandardSchemaV1, ae as hasStandardProps, af as standardSchemaFormValidator, ag as standardSchemaResolver } from '../standard-schema-CYbnugL7.js';
2
+ import '@for-fun/event-emitter';
@@ -0,0 +1,2 @@
1
+ export { I as InferSchemaValues, ad as StandardSchemaIssue, S as StandardSchemaV1, ae as hasStandardProps, af as standardSchemaFormValidator, ag as standardSchemaResolver } from '../standard-schema-CYbnugL7.js';
2
+ import '@for-fun/event-emitter';
@@ -1,68 +1,2 @@
1
- import { ValidationOutcome } from '../index.js';
2
- import { Validator } from '../index.js';
1
+ export { I as InferSchemaValues, ad as StandardSchemaIssue, S as StandardSchemaV1, ae as hasStandardProps, af as standardSchemaFormValidator, ag as standardSchemaResolver } from '../standard-schema-CYbnugL7.js';
3
2
  import '@for-fun/event-emitter';
4
-
5
- /**
6
- * Minimal copy of the Standard Schema v1 interfaces
7
- * (https://standardschema.dev) so this module has zero runtime and type
8
- * dependencies on any schema library. Implemented by zod v3.24+/v4,
9
- * valibot v1, arktype and others.
10
- */
11
- type StandardSchemaIssue = {
12
- readonly message: string;
13
- readonly path?: ReadonlyArray<PropertyKey | {
14
- readonly key: PropertyKey;
15
- }> | undefined;
16
- };
17
- type StandardSchemaV1<Input = unknown, Output = Input> = {
18
- readonly '~standard': {
19
- readonly version: 1;
20
- readonly vendor: string;
21
- readonly validate: (value: Input) => {
22
- readonly value: Output;
23
- readonly issues?: undefined;
24
- } | {
25
- readonly issues: ReadonlyArray<StandardSchemaIssue>;
26
- } | Promise<{
27
- readonly value: Output;
28
- readonly issues?: undefined;
29
- } | {
30
- readonly issues: ReadonlyArray<StandardSchemaIssue>;
31
- }>;
32
- };
33
- };
34
- /**
35
- * Does the schema implement the Standard Schema v1 props?
36
- */
37
- declare function hasStandardProps(schema: any): schema is StandardSchemaV1;
38
- /**
39
- * Field-level Standard Schema adapter: validate a single value with any
40
- * schema implementing '~standard' and map every issue to a FieldError,
41
- * so a value breaking several rules surfaces all of them (setErrorByPath
42
- * stores the array; error/errorObject readers still see the first).
43
- *
44
- * @param schema a Standard Schema v1 (zod v3.24+/v4, valibot v1, arktype...)
45
- * @return field validator compatible with useField's validate option
46
- */
47
- declare function standardSchemaResolver(schema: StandardSchemaV1): Validator;
48
- /**
49
- * Form-level Standard Schema adapter: validate the whole values object with
50
- * any schema implementing '~standard' and return a ValidationOutcome. On
51
- * failure `errors` carries the nested shape Options.validate expects
52
- * ({a: {b: FieldError[]}}; ensureValidate flattens it back to per-field
53
- * errors, keeping every issue of a path). Issues without a path are
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`
57
- * carries the schema's parsed output (coerce/transform results included),
58
- * which the form stores as its parsedValues baseline — the layer getValues
59
- * reads above initialValues, mirroring how react-hook-form's zodResolver
60
- * and TanStack's standardSchemaValidators use the parsed value.
61
- *
62
- * @param schema a Standard Schema v1 (zod v3.24+/v4, valibot v1, arktype...)
63
- * @return form-level validator for createForm({validate: ...})
64
- */
65
- declare function standardSchemaFormValidator<T extends Record<string, any>>(schema: StandardSchemaV1<T, any>): (values: T) => Promise<ValidationOutcome<T>>;
66
-
67
- export { hasStandardProps, standardSchemaFormValidator, standardSchemaResolver };
68
- export type { StandardSchemaIssue, StandardSchemaV1 };
@@ -1,2 +1,2 @@
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};
1
+ export{h as hasStandardProps,s as standardSchemaFormValidator,a as standardSchemaResolver}from"../standardSchema-BpZvg0tZ.mjs";import"../errors-ClH3eV1R.mjs";import"@for-fun/event-emitter";
2
2
  //# sourceMappingURL=standard-schema.mjs.map
@@ -1 +1 @@
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
+ {"version":3,"file":"standard-schema.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":""}
@@ -1,2 +1,2 @@
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"}))}}};
1
+ "use strict";var r=require("../standardSchema-CxV_DhBO.cjs.js");require("../errors-8oPux7GR.cjs.js"),require("@for-fun/event-emitter"),exports.yupResolver=function(e){return r.hasStandardProps(e)?r.schemaToFieldValidator(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":"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"}
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":"2JAIO,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"}
@@ -0,0 +1,6 @@
1
+ import { Validator } from '../index.js';
2
+ import '@for-fun/event-emitter';
3
+
4
+ declare function yupResolver(schema: any): Validator;
5
+
6
+ export { yupResolver };
@@ -0,0 +1,6 @@
1
+ import { Validator } from '../index.js';
2
+ import '@for-fun/event-emitter';
3
+
4
+ declare function yupResolver(schema: any): Validator;
5
+
6
+ export { yupResolver };
@@ -1,5 +1,4 @@
1
1
  import { Validator } from '../index.js';
2
- import '../form-CvmWHUrd.js';
3
2
  import '@for-fun/event-emitter';
4
3
 
5
4
  declare function yupResolver(schema: any): Validator;
@@ -1,2 +1,2 @@
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};
1
+ import{h as r,a}from"../standardSchema-BpZvg0tZ.mjs";import"../errors-ClH3eV1R.mjs";import"@for-fun/event-emitter";function e(e){return r(e)?a(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"}))}}}export{e 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":"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
+ {"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":"mHAIO,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 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"}]}};
1
+ "use strict";var e=require("../standardSchema-CxV_DhBO.cjs.js");require("../errors-8oPux7GR.cjs.js"),require("@for-fun/event-emitter"),exports.zodResolver=function(s){return e.hasStandardProps(s)?e.schemaToFieldValidator(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":"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"}
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":"2JAIO,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"}
@@ -0,0 +1,6 @@
1
+ import { Validator } from '../index.js';
2
+ import '@for-fun/event-emitter';
3
+
4
+ declare function zodResolver(schema: any): Validator;
5
+
6
+ export { zodResolver };
@@ -0,0 +1,6 @@
1
+ import { Validator } from '../index.js';
2
+ import '@for-fun/event-emitter';
3
+
4
+ declare function zodResolver(schema: any): Validator;
5
+
6
+ export { zodResolver };
@@ -1,5 +1,4 @@
1
1
  import { Validator } from '../index.js';
2
- import '../form-CvmWHUrd.js';
3
2
  import '@for-fun/event-emitter';
4
3
 
5
4
  declare function zodResolver(schema: any): Validator;
@@ -1,2 +1,2 @@
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};
1
+ import{h as e,a as s}from"../standardSchema-BpZvg0tZ.mjs";import"../errors-ClH3eV1R.mjs";import"@for-fun/event-emitter";function t(t){return e(t)?s(t):async e=>{const s=await t.safeParseAsync(e);if(s.success)return;const{issues:a}=s.error;return a?.length?a.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":"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"}
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":"wHAIO,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"}
@@ -1,2 +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)}};
1
+ "use strict";var e=require("@for-fun/event-emitter"),t=require("../standardSchema-CxV_DhBO.cjs.js"),n=require("../errors-8oPux7GR.cjs.js"),a=require("../values-DKpOCD5J.cjs.js"),r=require("../validate-BvN6hiEG.cjs.js");const i=new WeakMap;function s(e,t,s,o){i.get(e)?.get(t.key)?function(e,t,s){a.setValueByPath(e,t,s);const o=i.get(e)?.get(t.key);o&&function(e,t,i){("onChange"===i||"all"===i||"onTouched"===i&&a.hasTouchedByPath(e,t)||n.getFieldErrorsByPath(e,t).length>0&&"onChange"===e.reValidateMode)&&e.validators.get(t.key)?.();r.revalidateFormOnChange(e,t,i),"onChange"===e.validateMode&&e.validate&&r.runFormValidate(e).catch(()=>{});r.revalidateDependentsOnChange(e,t,i)}(e,t,o.mode??e.mode)}(e,t,s):a.setValueByPath(e,t,s,o)}function o(e){return void 0!==e.required||void 0!==e.min||void 0!==e.max||void 0!==e.minLength||void 0!==e.maxLength||void 0!==e.pattern||void 0!==e.validate}function u(e,t){switch(e){case"required":return"This field is required";case"min":return`Must be at least ${t}`;case"max":return`Must be at most ${t}`;case"minLength":return`Must be at least ${t} characters`;case"maxLength":return`Must be at most ${t} characters`;case"pattern":return"Invalid format";default:return"Invalid value"}}function l(e){return(t,n)=>{if(e.required&&(""===t||null==t||Array.isArray(t)&&0===t.length))return[{type:"required",message:"string"==typeof e.required?e.required:u("required")}];const a=[],r=(t,n)=>e.messages?.[t]??u(t,n);if(void 0!==e.min){const n=Number(t);!Number.isNaN(n)&&n<e.min&&a.push({type:"min",message:r("min",e.min)})}if(void 0!==e.max){const n=Number(t);!Number.isNaN(n)&&n>e.max&&a.push({type:"max",message:r("max",e.max)})}const i="string"==typeof t||Array.isArray(t);if(void 0!==e.minLength&&i&&t.length<e.minLength&&a.push({type:"minLength",message:r("minLength",e.minLength)}),void 0!==e.maxLength&&i&&t.length>e.maxLength&&a.push({type:"maxLength",message:r("maxLength",e.maxLength)}),e.pattern&&!e.pattern.value.test(t)&&a.push({type:"pattern",message:e.messages?.pattern??e.pattern.message??u("pattern")}),void 0!==e.validate){const r="function"==typeof e.validate?{validate:e.validate}:e.validate;for(const[e,i]of Object.entries(r)){const r=i(t,n);if(void 0!==r)for(const t of Array.isArray(r)?r:[r])a.push("string"==typeof t?{type:e,message:t}:{...t,type:e})}}return a.length?a:void 0}}function c(t,u,c){const d=n.create(u),{mode:f,shouldUnregister:h,eventToValue:g,valueAsNumber:m,valueAsDate:p,rules:y}=c??{},v=g??(e=>{const t=e?.target;return t?"file"===t.type?t.files:"checkbox"===t.type?t.checked:m?t.valueAsNumber:p?t.valueAsDate:t.value:e});let V=null,B=null,x=null,b=null,P=null,k=null;const A=s=>{if(b&&(!function(t,r,i){const{key:s}=r;i.present?a.setValueByPath(t,r,i.value,{shouldDirty:!1}):(t.deleted.delete(s),n.bumpValuesVersion(t),e.emit(t.emitter,"change",r)),i.touched&&a.setTouchedByPath(t,r),i.errors&&n.setErrorByPath(t,r,i.errors)}(t,d,b),b=null),V=s,"file"!==s.type&&void 0===a.getValueByPath(t,d)&&(a.seedValueByPath(t,d,"checkbox"===s.type?s.checked:s.value),a.emitChangeByPath(t,d)),B=function(e,t,n){let a=i.get(e);a||(a=new Map,i.set(e,a));const r=a.has(t.key),s={};return a.set(t.key,{mode:n,token:s}),{token:s,displaced:r}}(t,d,f).token,y&&o(y)){const e=void 0!==y.required?l({required:y.required}):void 0,n=void 0!==y.required?{...y,required:void 0}:y;x=r.registerValidatorByPath(t,d,{validate:()=>o(n)?l(n):void 0,debounce:()=>0,sync:()=>e})}P=e.on(t.emitter,"focusError",(e,t)=>{e===d.key&&V&&"function"==typeof V.focus&&(V.focus(),t?.shouldSelect&&"function"==typeof V.select&&V.select())}),k=e.on(t.emitter,"change",e=>{e||(()=>{if(!V||"file"===V.type)return;const e=a.getValueByPath(t,d),n=null==e?"":String(e);V.value!==n&&(V.value=n)})()})},L=()=>{B&&(!function(e,t,n){const a=i.get(e),r=a?.get(t.key);a&&r&&r.token===n&&a.delete(t.key)}(t,d,B),B=null),x&&(x(),x=null),P&&(P(),P=null),k&&(k(),k=null),V=null,!1!==(h??t.shouldUnregister)&&(b=function(e,t){const{key:n}=t,r={present:e.values.has(n),value:e.values.get(n),touched:e.touched.has(n),errors:e.errors.get(n)};return a.removeFieldByPath(e,t),r}(t,d))};return{name:d.key,onChange:e=>s(t,d,v(e)),onBlur:()=>function(e,t){a.setTouchedByPath(e,t);const s=i.get(e)?.get(t.key);if(!s)return;const o=s.mode??e.mode;("onBlur"===o||"onTouched"===o||"all"===o||n.getFieldErrorsByPath(e,t).length>0&&"onBlur"===e.reValidateMode)&&e.validators.get(t.key)?.(),"onBlur"===e.validateMode&&e.validate&&r.runFormValidate(e).catch(()=>{})}(t,d),ref:e=>{e?A(e):L()}}}function d(e,t){const n=a.getValueByPath(e,t);return Array.isArray(n)?n:[]}function f(e,t,n){a.setValueByPath(e,t,[...n])}function h(e,t,n){if(null!=n)if(Array.isArray(n))for(const a of n)h(e,t,a);else if("undefined"!=typeof FileList&&n instanceof FileList)for(let a=0;a<n.length;a++)e.append(t,n.item(a));else"undefined"!=typeof File&&n instanceof File?e.append(t,n,n.name):"undefined"!=typeof Blob&&n instanceof Blob?e.append(t,n):n instanceof Date?e.append(t,n.toISOString()):e.append(t,"object"==typeof n?JSON.stringify(n):String(n))}function g(e){const t=e.trim();if(t.length>0&&(t.startsWith("{")||t.startsWith("[")))try{return JSON.parse(t)}catch{return e}return e}exports.VALIDATION_OUTCOME=n.VALIDATION_OUTCOME,exports.appendValue=function(e,t,r){!function(e,t,n){const r=d(e,t);a.setValueByPath(e,t,[...r,n])}(e,n.create(t),r)},exports.formDataFromValues=function(e){const t=new FormData;for(const n of Object.keys(e))h(t,n,e[n]);return t},exports.insertValue=function(e,t,r,i){return function(e,t,n,r){const i=d(e,t);return!(n<0||n>i.length||(a.setValueByPath(e,t,[...i.slice(0,n),r,...i.slice(n)]),0))}(e,n.create(t),r,i)},exports.moveValue=function(e,t,r,i){return function(e,t,n,r){const i=d(e,t);if(n<0||n>=i.length||r<0||r>=i.length)return!1;if(n===r)return!1;const s=[...i],[o]=s.splice(n,1);return s.splice(r,0,o),a.setValueByPath(e,t,s),!0}(e,n.create(t),r,i)},exports.prependValue=function(e,t,r){!function(e,t,n){const r=d(e,t);a.setValueByPath(e,t,[n,...r])}(e,n.create(t),r)},exports.removeValue=function(e,t,r){return function(e,t,n){const r=d(e,t),i=new Set;for(const e of Array.isArray(n)?n:[n])e>=0&&e<r.length&&i.add(e);if(0===i.size)return[];const s=[...i].sort((e,t)=>t-e);return a.setValueByPath(e,t,r.filter((e,t)=>!i.has(t))),s}(e,n.create(t),r)},exports.replaceValues=function(e,t,a){f(e,n.create(t),a)},exports.swapValues=function(e,t,r,i){return function(e,t,n,r){const i=d(e,t);if(n<0||n>=i.length||r<0||r>=i.length)return!1;if(n===r)return!1;const s=[...i];return[s[n],s[r]]=[s[r],s[n]],a.setValueByPath(e,t,s),!0}(e,n.create(t),r,i)},exports.updateValue=function(e,t,r,i){return function(e,t,n,r){const i=d(e,t);if(n<0||n>=i.length)return!1;const s=[...i];return s[n]=r,a.setValueByPath(e,t,s),!0}(e,n.create(t),r,i)},exports.validateValues=async function(i,s){const o=function(r){const i=e.create();e.setMaxListeners(i,0);let s=r?.initialValues??{};"function"==typeof s&&(s=s());const o=r?.validate,u=o&&t.hasStandardProps(o)?t.schemaToFormValidator(o):o,l={emitter:i,...r,validate:u,mode:r?.mode??"onSubmit",reValidateMode:r?.reValidateMode??"onChange",validateMode:r?.validateMode??"onSubmit",disabled:r?.disabled??!1,validateOnMount:r?.validateOnMount??!1,asyncAlways:r?.asyncAlways??!1,shouldUseNativeValidation:r?.shouldUseNativeValidation??!0,validateDeps:r?.validateDeps?new Set(r.validateDeps.map(e=>n.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,isSubmitted:!1,submitCount:0,isSubmitSuccessful:void 0,isLoading:!1,status:void 0,register:(e,t)=>c(l,e,t)};return n.isPromise(s)?(l.isLoading=!0,e.emit(i,"loading"),Promise.resolve(s).then(t=>{a.setInitialValues(l,t??{}),l.isLoading=!1,e.emit(i,"loading")},t=>{l.isLoading=!1,e.emit(i,"loading")})):l.initialValues=s,l}({...s,initialValues:i});return{valid:await r.trigger(o),values:a.getValues(o),errors:n.getErrors(o)}},exports.valuesFromFormData=function(e){const t={};for(const n of new Set(e.keys())){const a=e.getAll(n),r=e=>"undefined"!=typeof File&&e instanceof File?e:g(e);t[n]=1===a.length?r(a[0]):a.map(r)}return t};
2
2
  //# sourceMappingURL=index.cjs.js.map