@tanstack/form-core 1.6.2 → 1.7.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.
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["import type { GlobalFormValidationError, ValidationCause } from './types'\nimport type { FormValidators } from './FormApi'\nimport type { AnyFieldMeta, FieldValidators } from './FieldApi'\n\nexport type UpdaterFn<TInput, TOutput = TInput> = (input: TInput) => TOutput\n\nexport type Updater<TInput, TOutput = TInput> =\n | TOutput\n | UpdaterFn<TInput, TOutput>\n\n/**\n * @private\n */\nexport function functionalUpdate<TInput, TOutput = TInput>(\n updater: Updater<TInput, TOutput>,\n input: TInput,\n): TOutput {\n return typeof updater === 'function'\n ? (updater as UpdaterFn<TInput, TOutput>)(input)\n : updater\n}\n\n/**\n * Get a value from an object using a path, including dot notation.\n * @private\n */\nexport function getBy(obj: any, path: any) {\n const pathObj = makePathArray(path)\n return pathObj.reduce((current: any, pathPart: any) => {\n if (current === null) return null\n if (typeof current !== 'undefined') {\n return current[pathPart]\n }\n return undefined\n }, obj)\n}\n\n/**\n * Set a value on an object using a path, including dot notation.\n * @private\n */\nexport function setBy(obj: any, _path: any, updater: Updater<any>) {\n const path = makePathArray(_path)\n\n function doSet(parent?: any): any {\n if (!path.length) {\n return functionalUpdate(updater, parent)\n }\n\n const key = path.shift()\n\n if (\n typeof key === 'string' ||\n (typeof key === 'number' && !Array.isArray(parent))\n ) {\n if (typeof parent === 'object') {\n if (parent === null) {\n parent = {}\n }\n return {\n ...parent,\n [key]: doSet(parent[key]),\n }\n }\n return {\n [key]: doSet(),\n }\n }\n\n if (Array.isArray(parent) && typeof key === 'number') {\n const prefix = parent.slice(0, key)\n return [\n ...(prefix.length ? prefix : new Array(key)),\n doSet(parent[key]),\n ...parent.slice(key + 1),\n ]\n }\n return [...new Array(key), doSet()]\n }\n\n return doSet(obj)\n}\n\n/**\n * Delete a field on an object using a path, including dot notation.\n * @private\n */\nexport function deleteBy(obj: any, _path: any) {\n const path = makePathArray(_path)\n\n function doDelete(parent: any): any {\n if (!parent) return\n if (path.length === 1) {\n const finalPath = path[0]!\n if (Array.isArray(parent) && typeof finalPath === 'number') {\n return parent.filter((_, i) => i !== finalPath)\n }\n const { [finalPath]: remove, ...rest } = parent\n return rest\n }\n\n const key = path.shift()\n\n if (typeof key === 'string') {\n if (typeof parent === 'object') {\n return {\n ...parent,\n [key]: doDelete(parent[key]),\n }\n }\n }\n\n if (typeof key === 'number') {\n if (Array.isArray(parent)) {\n if (key >= parent.length) {\n return parent\n }\n const prefix = parent.slice(0, key)\n return [\n ...(prefix.length ? prefix : new Array(key)),\n doDelete(parent[key]),\n ...parent.slice(key + 1),\n ]\n }\n }\n\n throw new Error('It seems we have created an infinite loop in deleteBy. ')\n }\n\n return doDelete(obj)\n}\n\nconst reFindNumbers0 = /^(\\d*)$/gm\nconst reFindNumbers1 = /\\.(\\d*)\\./gm\nconst reFindNumbers2 = /^(\\d*)\\./gm\nconst reFindNumbers3 = /\\.(\\d*$)/gm\nconst reFindMultiplePeriods = /\\.{2,}/gm\n\nconst intPrefix = '__int__'\nconst intReplace = `${intPrefix}$1`\n\n/**\n * @private\n */\nexport function makePathArray(str: string | Array<string | number>) {\n if (Array.isArray(str)) {\n return [...str]\n }\n\n if (typeof str !== 'string') {\n throw new Error('Path must be a string.')\n }\n\n return str\n .replace(/\\[/g, '.')\n .replace(/\\]/g, '')\n .replace(reFindNumbers0, intReplace)\n .replace(reFindNumbers1, `.${intReplace}.`)\n .replace(reFindNumbers2, `${intReplace}.`)\n .replace(reFindNumbers3, `.${intReplace}`)\n .replace(reFindMultiplePeriods, '.')\n .split('.')\n .map((d) => {\n if (d.indexOf(intPrefix) === 0) {\n return parseInt(d.substring(intPrefix.length), 10)\n }\n return d\n })\n}\n\n/**\n * @private\n */\nexport function isNonEmptyArray(obj: any) {\n return !(Array.isArray(obj) && obj.length === 0)\n}\n\ninterface AsyncValidatorArrayPartialOptions<T> {\n validators?: T\n asyncDebounceMs?: number\n}\n\n/**\n * @private\n */\nexport interface AsyncValidator<T> {\n cause: ValidationCause\n validate: T\n debounceMs: number\n}\n\n/**\n * @private\n */\nexport function getAsyncValidatorArray<T>(\n cause: ValidationCause,\n options: AsyncValidatorArrayPartialOptions<T>,\n): T extends FieldValidators<any, any, any, any, any, any, any, any, any, any>\n ? Array<\n AsyncValidator<T['onChangeAsync'] | T['onBlurAsync'] | T['onSubmitAsync']>\n >\n : T extends FormValidators<any, any, any, any, any, any, any, any>\n ? Array<\n AsyncValidator<\n T['onChangeAsync'] | T['onBlurAsync'] | T['onSubmitAsync']\n >\n >\n : never {\n const { asyncDebounceMs } = options\n const {\n onChangeAsync,\n onBlurAsync,\n onSubmitAsync,\n onBlurAsyncDebounceMs,\n onChangeAsyncDebounceMs,\n } = (options.validators || {}) as\n | FieldValidators<any, any, any, any, any, any, any, any, any, any>\n | FormValidators<any, any, any, any, any, any, any, any>\n\n const defaultDebounceMs = asyncDebounceMs ?? 0\n\n const changeValidator = {\n cause: 'change',\n validate: onChangeAsync,\n debounceMs: onChangeAsyncDebounceMs ?? defaultDebounceMs,\n } as const\n\n const blurValidator = {\n cause: 'blur',\n validate: onBlurAsync,\n debounceMs: onBlurAsyncDebounceMs ?? defaultDebounceMs,\n } as const\n\n const submitValidator = {\n cause: 'submit',\n validate: onSubmitAsync,\n debounceMs: 0,\n } as const\n\n const noopValidator = (\n validator:\n | typeof changeValidator\n | typeof blurValidator\n | typeof submitValidator,\n ) => ({ ...validator, debounceMs: 0 }) as const\n\n switch (cause) {\n case 'submit':\n return [\n noopValidator(changeValidator),\n noopValidator(blurValidator),\n submitValidator,\n ] as never\n case 'blur':\n return [blurValidator] as never\n case 'change':\n return [changeValidator] as never\n case 'server':\n default:\n return [] as never\n }\n}\n\ninterface SyncValidatorArrayPartialOptions<T> {\n validators?: T\n}\n\n/**\n * @private\n */\nexport interface SyncValidator<T> {\n cause: ValidationCause\n validate: T\n}\n\n/**\n * @private\n */\nexport function getSyncValidatorArray<T>(\n cause: ValidationCause,\n options: SyncValidatorArrayPartialOptions<T>,\n): T extends FieldValidators<any, any, any, any, any, any, any, any, any, any>\n ? Array<\n SyncValidator<T['onChange'] | T['onBlur'] | T['onSubmit'] | T['onMount']>\n >\n : T extends FormValidators<any, any, any, any, any, any, any, any>\n ? Array<\n SyncValidator<\n T['onChange'] | T['onBlur'] | T['onSubmit'] | T['onMount']\n >\n >\n : never {\n const { onChange, onBlur, onSubmit, onMount } = (options.validators || {}) as\n | FieldValidators<any, any, any, any, any, any, any, any, any, any>\n | FormValidators<any, any, any, any, any, any, any, any>\n\n const changeValidator = { cause: 'change', validate: onChange } as const\n const blurValidator = { cause: 'blur', validate: onBlur } as const\n const submitValidator = { cause: 'submit', validate: onSubmit } as const\n const mountValidator = { cause: 'mount', validate: onMount } as const\n\n // Allows us to clear onServer errors\n const serverValidator = {\n cause: 'server',\n validate: () => undefined,\n } as const\n\n switch (cause) {\n case 'mount':\n return [mountValidator] as never\n case 'submit':\n return [\n changeValidator,\n blurValidator,\n submitValidator,\n serverValidator,\n ] as never\n case 'server':\n return [serverValidator] as never\n case 'blur':\n return [blurValidator, serverValidator] as never\n case 'change':\n default:\n return [changeValidator, serverValidator] as never\n }\n}\n\nexport const isGlobalFormValidationError = (\n error: unknown,\n): error is GlobalFormValidationError<unknown> => {\n return !!error && typeof error === 'object' && 'fields' in error\n}\n\nexport function shallow<T>(objA: T, objB: T) {\n if (Object.is(objA, objB)) {\n return true\n }\n\n if (\n typeof objA !== 'object' ||\n objA === null ||\n typeof objB !== 'object' ||\n objB === null\n ) {\n return false\n }\n\n if (objA instanceof Map && objB instanceof Map) {\n if (objA.size !== objB.size) return false\n for (const [k, v] of objA) {\n if (!objB.has(k) || !Object.is(v, objB.get(k))) return false\n }\n return true\n }\n\n if (objA instanceof Set && objB instanceof Set) {\n if (objA.size !== objB.size) return false\n for (const v of objA) {\n if (!objB.has(v)) return false\n }\n return true\n }\n\n const keysA = Object.keys(objA)\n if (keysA.length !== Object.keys(objB).length) {\n return false\n }\n\n for (let i = 0; i < keysA.length; i++) {\n if (\n !Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) ||\n !Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T])\n ) {\n return false\n }\n }\n return true\n}\n"],"names":[],"mappings":"AAagB,SAAA,iBACd,SACA,OACS;AACT,SAAO,OAAO,YAAY,aACrB,QAAuC,KAAK,IAC7C;AACN;AAMgB,SAAA,MAAM,KAAU,MAAW;AACnC,QAAA,UAAU,cAAc,IAAI;AAClC,SAAO,QAAQ,OAAO,CAAC,SAAc,aAAkB;AACjD,QAAA,YAAY,KAAa,QAAA;AACzB,QAAA,OAAO,YAAY,aAAa;AAClC,aAAO,QAAQ,QAAQ;AAAA,IAAA;AAElB,WAAA;AAAA,KACN,GAAG;AACR;AAMgB,SAAA,MAAM,KAAU,OAAY,SAAuB;AAC3D,QAAA,OAAO,cAAc,KAAK;AAEhC,WAAS,MAAM,QAAmB;AAC5B,QAAA,CAAC,KAAK,QAAQ;AACT,aAAA,iBAAiB,SAAS,MAAM;AAAA,IAAA;AAGnC,UAAA,MAAM,KAAK,MAAM;AAGrB,QAAA,OAAO,QAAQ,YACd,OAAO,QAAQ,YAAY,CAAC,MAAM,QAAQ,MAAM,GACjD;AACI,UAAA,OAAO,WAAW,UAAU;AAC9B,YAAI,WAAW,MAAM;AACnB,mBAAS,CAAC;AAAA,QAAA;AAEL,eAAA;AAAA,UACL,GAAG;AAAA,UACH,CAAC,GAAG,GAAG,MAAM,OAAO,GAAG,CAAC;AAAA,QAC1B;AAAA,MAAA;AAEK,aAAA;AAAA,QACL,CAAC,GAAG,GAAG,MAAM;AAAA,MACf;AAAA,IAAA;AAGF,QAAI,MAAM,QAAQ,MAAM,KAAK,OAAO,QAAQ,UAAU;AACpD,YAAM,SAAS,OAAO,MAAM,GAAG,GAAG;AAC3B,aAAA;AAAA,QACL,GAAI,OAAO,SAAS,SAAS,IAAI,MAAM,GAAG;AAAA,QAC1C,MAAM,OAAO,GAAG,CAAC;AAAA,QACjB,GAAG,OAAO,MAAM,MAAM,CAAC;AAAA,MACzB;AAAA,IAAA;AAEF,WAAO,CAAC,GAAG,IAAI,MAAM,GAAG,GAAG,OAAO;AAAA,EAAA;AAGpC,SAAO,MAAM,GAAG;AAClB;AAMgB,SAAA,SAAS,KAAU,OAAY;AACvC,QAAA,OAAO,cAAc,KAAK;AAEhC,WAAS,SAAS,QAAkB;AAClC,QAAI,CAAC,OAAQ;AACT,QAAA,KAAK,WAAW,GAAG;AACf,YAAA,YAAY,KAAK,CAAC;AACxB,UAAI,MAAM,QAAQ,MAAM,KAAK,OAAO,cAAc,UAAU;AAC1D,eAAO,OAAO,OAAO,CAAC,GAAG,MAAM,MAAM,SAAS;AAAA,MAAA;AAEhD,YAAM,EAAE,CAAC,SAAS,GAAG,QAAQ,GAAG,KAAS,IAAA;AAClC,aAAA;AAAA,IAAA;AAGH,UAAA,MAAM,KAAK,MAAM;AAEnB,QAAA,OAAO,QAAQ,UAAU;AACvB,UAAA,OAAO,WAAW,UAAU;AACvB,eAAA;AAAA,UACL,GAAG;AAAA,UACH,CAAC,GAAG,GAAG,SAAS,OAAO,GAAG,CAAC;AAAA,QAC7B;AAAA,MAAA;AAAA,IACF;AAGE,QAAA,OAAO,QAAQ,UAAU;AACvB,UAAA,MAAM,QAAQ,MAAM,GAAG;AACrB,YAAA,OAAO,OAAO,QAAQ;AACjB,iBAAA;AAAA,QAAA;AAET,cAAM,SAAS,OAAO,MAAM,GAAG,GAAG;AAC3B,eAAA;AAAA,UACL,GAAI,OAAO,SAAS,SAAS,IAAI,MAAM,GAAG;AAAA,UAC1C,SAAS,OAAO,GAAG,CAAC;AAAA,UACpB,GAAG,OAAO,MAAM,MAAM,CAAC;AAAA,QACzB;AAAA,MAAA;AAAA,IACF;AAGI,UAAA,IAAI,MAAM,yDAAyD;AAAA,EAAA;AAG3E,SAAO,SAAS,GAAG;AACrB;AAEA,MAAM,iBAAiB;AACvB,MAAM,iBAAiB;AACvB,MAAM,iBAAiB;AACvB,MAAM,iBAAiB;AACvB,MAAM,wBAAwB;AAE9B,MAAM,YAAY;AAClB,MAAM,aAAa,GAAG,SAAS;AAKxB,SAAS,cAAc,KAAsC;AAC9D,MAAA,MAAM,QAAQ,GAAG,GAAG;AACf,WAAA,CAAC,GAAG,GAAG;AAAA,EAAA;AAGZ,MAAA,OAAO,QAAQ,UAAU;AACrB,UAAA,IAAI,MAAM,wBAAwB;AAAA,EAAA;AAG1C,SAAO,IACJ,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,EAAE,EACjB,QAAQ,gBAAgB,UAAU,EAClC,QAAQ,gBAAgB,IAAI,UAAU,GAAG,EACzC,QAAQ,gBAAgB,GAAG,UAAU,GAAG,EACxC,QAAQ,gBAAgB,IAAI,UAAU,EAAE,EACxC,QAAQ,uBAAuB,GAAG,EAClC,MAAM,GAAG,EACT,IAAI,CAAC,MAAM;AACV,QAAI,EAAE,QAAQ,SAAS,MAAM,GAAG;AAC9B,aAAO,SAAS,EAAE,UAAU,UAAU,MAAM,GAAG,EAAE;AAAA,IAAA;AAE5C,WAAA;AAAA,EAAA,CACR;AACL;AAKO,SAAS,gBAAgB,KAAU;AACxC,SAAO,EAAE,MAAM,QAAQ,GAAG,KAAK,IAAI,WAAW;AAChD;AAmBgB,SAAA,uBACd,OACA,SAWU;AACJ,QAAA,EAAE,oBAAoB;AACtB,QAAA;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA,IACG,QAAQ,cAAc,CAAC;AAI5B,QAAM,oBAAoB,mBAAmB;AAE7C,QAAM,kBAAkB;AAAA,IACtB,OAAO;AAAA,IACP,UAAU;AAAA,IACV,YAAY,2BAA2B;AAAA,EACzC;AAEA,QAAM,gBAAgB;AAAA,IACpB,OAAO;AAAA,IACP,UAAU;AAAA,IACV,YAAY,yBAAyB;AAAA,EACvC;AAEA,QAAM,kBAAkB;AAAA,IACtB,OAAO;AAAA,IACP,UAAU;AAAA,IACV,YAAY;AAAA,EACd;AAEA,QAAM,gBAAgB,CACpB,eAII,EAAE,GAAG,WAAW,YAAY;AAElC,UAAQ,OAAO;AAAA,IACb,KAAK;AACI,aAAA;AAAA,QACL,cAAc,eAAe;AAAA,QAC7B,cAAc,aAAa;AAAA,QAC3B;AAAA,MACF;AAAA,IACF,KAAK;AACH,aAAO,CAAC,aAAa;AAAA,IACvB,KAAK;AACH,aAAO,CAAC,eAAe;AAAA,IACzB,KAAK;AAAA,IACL;AACE,aAAO,CAAC;AAAA,EAAA;AAEd;AAiBgB,SAAA,sBACd,OACA,SAWU;AACJ,QAAA,EAAE,UAAU,QAAQ,UAAU,YAAa,QAAQ,cAAc,CAAC;AAIxE,QAAM,kBAAkB,EAAE,OAAO,UAAU,UAAU,SAAS;AAC9D,QAAM,gBAAgB,EAAE,OAAO,QAAQ,UAAU,OAAO;AACxD,QAAM,kBAAkB,EAAE,OAAO,UAAU,UAAU,SAAS;AAC9D,QAAM,iBAAiB,EAAE,OAAO,SAAS,UAAU,QAAQ;AAG3D,QAAM,kBAAkB;AAAA,IACtB,OAAO;AAAA,IACP,UAAU,MAAM;AAAA,EAClB;AAEA,UAAQ,OAAO;AAAA,IACb,KAAK;AACH,aAAO,CAAC,cAAc;AAAA,IACxB,KAAK;AACI,aAAA;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF,KAAK;AACH,aAAO,CAAC,eAAe;AAAA,IACzB,KAAK;AACI,aAAA,CAAC,eAAe,eAAe;AAAA,IACxC,KAAK;AAAA,IACL;AACS,aAAA,CAAC,iBAAiB,eAAe;AAAA,EAAA;AAE9C;AAEa,MAAA,8BAA8B,CACzC,UACgD;AAChD,SAAO,CAAC,CAAC,SAAS,OAAO,UAAU,YAAY,YAAY;AAC7D;AAEgB,SAAA,QAAW,MAAS,MAAS;AAC3C,MAAI,OAAO,GAAG,MAAM,IAAI,GAAG;AAClB,WAAA;AAAA,EAAA;AAIP,MAAA,OAAO,SAAS,YAChB,SAAS,QACT,OAAO,SAAS,YAChB,SAAS,MACT;AACO,WAAA;AAAA,EAAA;AAGL,MAAA,gBAAgB,OAAO,gBAAgB,KAAK;AAC9C,QAAI,KAAK,SAAS,KAAK,KAAa,QAAA;AACpC,eAAW,CAAC,GAAG,CAAC,KAAK,MAAM;AACzB,UAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,GAAG,KAAK,IAAI,CAAC,CAAC,EAAU,QAAA;AAAA,IAAA;AAElD,WAAA;AAAA,EAAA;AAGL,MAAA,gBAAgB,OAAO,gBAAgB,KAAK;AAC9C,QAAI,KAAK,SAAS,KAAK,KAAa,QAAA;AACpC,eAAW,KAAK,MAAM;AACpB,UAAI,CAAC,KAAK,IAAI,CAAC,EAAU,QAAA;AAAA,IAAA;AAEpB,WAAA;AAAA,EAAA;AAGH,QAAA,QAAQ,OAAO,KAAK,IAAI;AAC9B,MAAI,MAAM,WAAW,OAAO,KAAK,IAAI,EAAE,QAAQ;AACtC,WAAA;AAAA,EAAA;AAGT,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AAEnC,QAAA,CAAC,OAAO,UAAU,eAAe,KAAK,MAAM,MAAM,CAAC,CAAW,KAC9D,CAAC,OAAO,GAAG,KAAK,MAAM,CAAC,CAAY,GAAG,KAAK,MAAM,CAAC,CAAY,CAAC,GAC/D;AACO,aAAA;AAAA,IAAA;AAAA,EACT;AAEK,SAAA;AACT;"}
1
+ {"version":3,"file":"utils.js","sources":["../../src/utils.ts"],"sourcesContent":["import type {\n GlobalFormValidationError,\n ValidationCause,\n ValidationError,\n ValidationSource,\n} from './types'\nimport type { FormValidators } from './FormApi'\nimport type { AnyFieldMeta, FieldValidators } from './FieldApi'\n\nexport type UpdaterFn<TInput, TOutput = TInput> = (input: TInput) => TOutput\n\nexport type Updater<TInput, TOutput = TInput> =\n | TOutput\n | UpdaterFn<TInput, TOutput>\n\n/**\n * @private\n */\nexport function functionalUpdate<TInput, TOutput = TInput>(\n updater: Updater<TInput, TOutput>,\n input: TInput,\n): TOutput {\n return typeof updater === 'function'\n ? (updater as UpdaterFn<TInput, TOutput>)(input)\n : updater\n}\n\n/**\n * Get a value from an object using a path, including dot notation.\n * @private\n */\nexport function getBy(obj: any, path: any) {\n const pathObj = makePathArray(path)\n return pathObj.reduce((current: any, pathPart: any) => {\n if (current === null) return null\n if (typeof current !== 'undefined') {\n return current[pathPart]\n }\n return undefined\n }, obj)\n}\n\n/**\n * Set a value on an object using a path, including dot notation.\n * @private\n */\nexport function setBy(obj: any, _path: any, updater: Updater<any>) {\n const path = makePathArray(_path)\n\n function doSet(parent?: any): any {\n if (!path.length) {\n return functionalUpdate(updater, parent)\n }\n\n const key = path.shift()\n\n if (\n typeof key === 'string' ||\n (typeof key === 'number' && !Array.isArray(parent))\n ) {\n if (typeof parent === 'object') {\n if (parent === null) {\n parent = {}\n }\n return {\n ...parent,\n [key]: doSet(parent[key]),\n }\n }\n return {\n [key]: doSet(),\n }\n }\n\n if (Array.isArray(parent) && typeof key === 'number') {\n const prefix = parent.slice(0, key)\n return [\n ...(prefix.length ? prefix : new Array(key)),\n doSet(parent[key]),\n ...parent.slice(key + 1),\n ]\n }\n return [...new Array(key), doSet()]\n }\n\n return doSet(obj)\n}\n\n/**\n * Delete a field on an object using a path, including dot notation.\n * @private\n */\nexport function deleteBy(obj: any, _path: any) {\n const path = makePathArray(_path)\n\n function doDelete(parent: any): any {\n if (!parent) return\n if (path.length === 1) {\n const finalPath = path[0]!\n if (Array.isArray(parent) && typeof finalPath === 'number') {\n return parent.filter((_, i) => i !== finalPath)\n }\n const { [finalPath]: remove, ...rest } = parent\n return rest\n }\n\n const key = path.shift()\n\n if (typeof key === 'string') {\n if (typeof parent === 'object') {\n return {\n ...parent,\n [key]: doDelete(parent[key]),\n }\n }\n }\n\n if (typeof key === 'number') {\n if (Array.isArray(parent)) {\n if (key >= parent.length) {\n return parent\n }\n const prefix = parent.slice(0, key)\n return [\n ...(prefix.length ? prefix : new Array(key)),\n doDelete(parent[key]),\n ...parent.slice(key + 1),\n ]\n }\n }\n\n throw new Error('It seems we have created an infinite loop in deleteBy. ')\n }\n\n return doDelete(obj)\n}\n\nconst reFindNumbers0 = /^(\\d*)$/gm\nconst reFindNumbers1 = /\\.(\\d*)\\./gm\nconst reFindNumbers2 = /^(\\d*)\\./gm\nconst reFindNumbers3 = /\\.(\\d*$)/gm\nconst reFindMultiplePeriods = /\\.{2,}/gm\n\nconst intPrefix = '__int__'\nconst intReplace = `${intPrefix}$1`\n\n/**\n * @private\n */\nexport function makePathArray(str: string | Array<string | number>) {\n if (Array.isArray(str)) {\n return [...str]\n }\n\n if (typeof str !== 'string') {\n throw new Error('Path must be a string.')\n }\n\n return str\n .replace(/\\[/g, '.')\n .replace(/\\]/g, '')\n .replace(reFindNumbers0, intReplace)\n .replace(reFindNumbers1, `.${intReplace}.`)\n .replace(reFindNumbers2, `${intReplace}.`)\n .replace(reFindNumbers3, `.${intReplace}`)\n .replace(reFindMultiplePeriods, '.')\n .split('.')\n .map((d) => {\n if (d.indexOf(intPrefix) === 0) {\n return parseInt(d.substring(intPrefix.length), 10)\n }\n return d\n })\n}\n\n/**\n * @private\n */\nexport function isNonEmptyArray(obj: any) {\n return !(Array.isArray(obj) && obj.length === 0)\n}\n\ninterface AsyncValidatorArrayPartialOptions<T> {\n validators?: T\n asyncDebounceMs?: number\n}\n\n/**\n * @private\n */\nexport interface AsyncValidator<T> {\n cause: ValidationCause\n validate: T\n debounceMs: number\n}\n\n/**\n * @private\n */\nexport function getAsyncValidatorArray<T>(\n cause: ValidationCause,\n options: AsyncValidatorArrayPartialOptions<T>,\n): T extends FieldValidators<any, any, any, any, any, any, any, any, any, any>\n ? Array<\n AsyncValidator<T['onChangeAsync'] | T['onBlurAsync'] | T['onSubmitAsync']>\n >\n : T extends FormValidators<any, any, any, any, any, any, any, any>\n ? Array<\n AsyncValidator<\n T['onChangeAsync'] | T['onBlurAsync'] | T['onSubmitAsync']\n >\n >\n : never {\n const { asyncDebounceMs } = options\n const {\n onChangeAsync,\n onBlurAsync,\n onSubmitAsync,\n onBlurAsyncDebounceMs,\n onChangeAsyncDebounceMs,\n } = (options.validators || {}) as\n | FieldValidators<any, any, any, any, any, any, any, any, any, any>\n | FormValidators<any, any, any, any, any, any, any, any>\n\n const defaultDebounceMs = asyncDebounceMs ?? 0\n\n const changeValidator = {\n cause: 'change',\n validate: onChangeAsync,\n debounceMs: onChangeAsyncDebounceMs ?? defaultDebounceMs,\n } as const\n\n const blurValidator = {\n cause: 'blur',\n validate: onBlurAsync,\n debounceMs: onBlurAsyncDebounceMs ?? defaultDebounceMs,\n } as const\n\n const submitValidator = {\n cause: 'submit',\n validate: onSubmitAsync,\n debounceMs: 0,\n } as const\n\n const noopValidator = (\n validator:\n | typeof changeValidator\n | typeof blurValidator\n | typeof submitValidator,\n ) => ({ ...validator, debounceMs: 0 }) as const\n\n switch (cause) {\n case 'submit':\n return [\n noopValidator(changeValidator),\n noopValidator(blurValidator),\n submitValidator,\n ] as never\n case 'blur':\n return [blurValidator] as never\n case 'change':\n return [changeValidator] as never\n case 'server':\n default:\n return [] as never\n }\n}\n\ninterface SyncValidatorArrayPartialOptions<T> {\n validators?: T\n}\n\n/**\n * @private\n */\nexport interface SyncValidator<T> {\n cause: ValidationCause\n validate: T\n}\n\n/**\n * @private\n */\nexport function getSyncValidatorArray<T>(\n cause: ValidationCause,\n options: SyncValidatorArrayPartialOptions<T>,\n): T extends FieldValidators<any, any, any, any, any, any, any, any, any, any>\n ? Array<\n SyncValidator<T['onChange'] | T['onBlur'] | T['onSubmit'] | T['onMount']>\n >\n : T extends FormValidators<any, any, any, any, any, any, any, any>\n ? Array<\n SyncValidator<\n T['onChange'] | T['onBlur'] | T['onSubmit'] | T['onMount']\n >\n >\n : never {\n const { onChange, onBlur, onSubmit, onMount } = (options.validators || {}) as\n | FieldValidators<any, any, any, any, any, any, any, any, any, any>\n | FormValidators<any, any, any, any, any, any, any, any>\n\n const changeValidator = { cause: 'change', validate: onChange } as const\n const blurValidator = { cause: 'blur', validate: onBlur } as const\n const submitValidator = { cause: 'submit', validate: onSubmit } as const\n const mountValidator = { cause: 'mount', validate: onMount } as const\n\n // Allows us to clear onServer errors\n const serverValidator = {\n cause: 'server',\n validate: () => undefined,\n } as const\n\n switch (cause) {\n case 'mount':\n return [mountValidator] as never\n case 'submit':\n return [\n changeValidator,\n blurValidator,\n submitValidator,\n serverValidator,\n ] as never\n case 'server':\n return [serverValidator] as never\n case 'blur':\n return [blurValidator, serverValidator] as never\n case 'change':\n default:\n return [changeValidator, serverValidator] as never\n }\n}\n\nexport const isGlobalFormValidationError = (\n error: unknown,\n): error is GlobalFormValidationError<unknown> => {\n return !!error && typeof error === 'object' && 'fields' in error\n}\n\nexport function shallow<T>(objA: T, objB: T) {\n if (Object.is(objA, objB)) {\n return true\n }\n\n if (\n typeof objA !== 'object' ||\n objA === null ||\n typeof objB !== 'object' ||\n objB === null\n ) {\n return false\n }\n\n if (objA instanceof Map && objB instanceof Map) {\n if (objA.size !== objB.size) return false\n for (const [k, v] of objA) {\n if (!objB.has(k) || !Object.is(v, objB.get(k))) return false\n }\n return true\n }\n\n if (objA instanceof Set && objB instanceof Set) {\n if (objA.size !== objB.size) return false\n for (const v of objA) {\n if (!objB.has(v)) return false\n }\n return true\n }\n\n const keysA = Object.keys(objA)\n if (keysA.length !== Object.keys(objB).length) {\n return false\n }\n\n for (let i = 0; i < keysA.length; i++) {\n if (\n !Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) ||\n !Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T])\n ) {\n return false\n }\n }\n return true\n}\n\n/**\n * Determines the logic for determining the error source and value to set on the field meta within the form level sync/async validation.\n * @private\n */\nexport const determineFormLevelErrorSourceAndValue = ({\n newFormValidatorError,\n isPreviousErrorFromFormValidator,\n previousErrorValue,\n}: {\n newFormValidatorError: ValidationError\n isPreviousErrorFromFormValidator: boolean\n previousErrorValue: ValidationError\n}): {\n newErrorValue: ValidationError\n newSource: ValidationSource | undefined\n} => {\n // All falsy values are not considered errors\n if (newFormValidatorError) {\n return { newErrorValue: newFormValidatorError, newSource: 'form' }\n }\n\n // Clears form level error since it's now stale\n if (isPreviousErrorFromFormValidator) {\n return { newErrorValue: undefined, newSource: undefined }\n }\n\n // At this point, we have a preivous error which must have been set by the field validator, keep as is\n if (previousErrorValue) {\n return { newErrorValue: previousErrorValue, newSource: 'field' }\n }\n\n // No new or previous error, clear the error\n return { newErrorValue: undefined, newSource: undefined }\n}\n\n/**\n * Determines the logic for determining the error source and value to set on the field meta within the field level sync/async validation.\n * @private\n */\nexport const determineFieldLevelErrorSourceAndValue = ({\n formLevelError,\n fieldLevelError,\n}: {\n formLevelError: ValidationError\n fieldLevelError: ValidationError\n}): {\n newErrorValue: ValidationError\n newSource: ValidationSource | undefined\n} => {\n // At field level, we prioritize the field level error\n if (fieldLevelError) {\n return { newErrorValue: fieldLevelError, newSource: 'field' }\n }\n\n // If there is no field level error, and there is a form level error, we set the form level error\n if (formLevelError) {\n return { newErrorValue: formLevelError, newSource: 'form' }\n }\n\n return { newErrorValue: undefined, newSource: undefined }\n}\n"],"names":[],"mappings":"AAkBgB,SAAA,iBACd,SACA,OACS;AACT,SAAO,OAAO,YAAY,aACrB,QAAuC,KAAK,IAC7C;AACN;AAMgB,SAAA,MAAM,KAAU,MAAW;AACnC,QAAA,UAAU,cAAc,IAAI;AAClC,SAAO,QAAQ,OAAO,CAAC,SAAc,aAAkB;AACjD,QAAA,YAAY,KAAa,QAAA;AACzB,QAAA,OAAO,YAAY,aAAa;AAClC,aAAO,QAAQ,QAAQ;AAAA,IAAA;AAElB,WAAA;AAAA,KACN,GAAG;AACR;AAMgB,SAAA,MAAM,KAAU,OAAY,SAAuB;AAC3D,QAAA,OAAO,cAAc,KAAK;AAEhC,WAAS,MAAM,QAAmB;AAC5B,QAAA,CAAC,KAAK,QAAQ;AACT,aAAA,iBAAiB,SAAS,MAAM;AAAA,IAAA;AAGnC,UAAA,MAAM,KAAK,MAAM;AAGrB,QAAA,OAAO,QAAQ,YACd,OAAO,QAAQ,YAAY,CAAC,MAAM,QAAQ,MAAM,GACjD;AACI,UAAA,OAAO,WAAW,UAAU;AAC9B,YAAI,WAAW,MAAM;AACnB,mBAAS,CAAC;AAAA,QAAA;AAEL,eAAA;AAAA,UACL,GAAG;AAAA,UACH,CAAC,GAAG,GAAG,MAAM,OAAO,GAAG,CAAC;AAAA,QAC1B;AAAA,MAAA;AAEK,aAAA;AAAA,QACL,CAAC,GAAG,GAAG,MAAM;AAAA,MACf;AAAA,IAAA;AAGF,QAAI,MAAM,QAAQ,MAAM,KAAK,OAAO,QAAQ,UAAU;AACpD,YAAM,SAAS,OAAO,MAAM,GAAG,GAAG;AAC3B,aAAA;AAAA,QACL,GAAI,OAAO,SAAS,SAAS,IAAI,MAAM,GAAG;AAAA,QAC1C,MAAM,OAAO,GAAG,CAAC;AAAA,QACjB,GAAG,OAAO,MAAM,MAAM,CAAC;AAAA,MACzB;AAAA,IAAA;AAEF,WAAO,CAAC,GAAG,IAAI,MAAM,GAAG,GAAG,OAAO;AAAA,EAAA;AAGpC,SAAO,MAAM,GAAG;AAClB;AAMgB,SAAA,SAAS,KAAU,OAAY;AACvC,QAAA,OAAO,cAAc,KAAK;AAEhC,WAAS,SAAS,QAAkB;AAClC,QAAI,CAAC,OAAQ;AACT,QAAA,KAAK,WAAW,GAAG;AACf,YAAA,YAAY,KAAK,CAAC;AACxB,UAAI,MAAM,QAAQ,MAAM,KAAK,OAAO,cAAc,UAAU;AAC1D,eAAO,OAAO,OAAO,CAAC,GAAG,MAAM,MAAM,SAAS;AAAA,MAAA;AAEhD,YAAM,EAAE,CAAC,SAAS,GAAG,QAAQ,GAAG,KAAS,IAAA;AAClC,aAAA;AAAA,IAAA;AAGH,UAAA,MAAM,KAAK,MAAM;AAEnB,QAAA,OAAO,QAAQ,UAAU;AACvB,UAAA,OAAO,WAAW,UAAU;AACvB,eAAA;AAAA,UACL,GAAG;AAAA,UACH,CAAC,GAAG,GAAG,SAAS,OAAO,GAAG,CAAC;AAAA,QAC7B;AAAA,MAAA;AAAA,IACF;AAGE,QAAA,OAAO,QAAQ,UAAU;AACvB,UAAA,MAAM,QAAQ,MAAM,GAAG;AACrB,YAAA,OAAO,OAAO,QAAQ;AACjB,iBAAA;AAAA,QAAA;AAET,cAAM,SAAS,OAAO,MAAM,GAAG,GAAG;AAC3B,eAAA;AAAA,UACL,GAAI,OAAO,SAAS,SAAS,IAAI,MAAM,GAAG;AAAA,UAC1C,SAAS,OAAO,GAAG,CAAC;AAAA,UACpB,GAAG,OAAO,MAAM,MAAM,CAAC;AAAA,QACzB;AAAA,MAAA;AAAA,IACF;AAGI,UAAA,IAAI,MAAM,yDAAyD;AAAA,EAAA;AAG3E,SAAO,SAAS,GAAG;AACrB;AAEA,MAAM,iBAAiB;AACvB,MAAM,iBAAiB;AACvB,MAAM,iBAAiB;AACvB,MAAM,iBAAiB;AACvB,MAAM,wBAAwB;AAE9B,MAAM,YAAY;AAClB,MAAM,aAAa,GAAG,SAAS;AAKxB,SAAS,cAAc,KAAsC;AAC9D,MAAA,MAAM,QAAQ,GAAG,GAAG;AACf,WAAA,CAAC,GAAG,GAAG;AAAA,EAAA;AAGZ,MAAA,OAAO,QAAQ,UAAU;AACrB,UAAA,IAAI,MAAM,wBAAwB;AAAA,EAAA;AAG1C,SAAO,IACJ,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,EAAE,EACjB,QAAQ,gBAAgB,UAAU,EAClC,QAAQ,gBAAgB,IAAI,UAAU,GAAG,EACzC,QAAQ,gBAAgB,GAAG,UAAU,GAAG,EACxC,QAAQ,gBAAgB,IAAI,UAAU,EAAE,EACxC,QAAQ,uBAAuB,GAAG,EAClC,MAAM,GAAG,EACT,IAAI,CAAC,MAAM;AACV,QAAI,EAAE,QAAQ,SAAS,MAAM,GAAG;AAC9B,aAAO,SAAS,EAAE,UAAU,UAAU,MAAM,GAAG,EAAE;AAAA,IAAA;AAE5C,WAAA;AAAA,EAAA,CACR;AACL;AAKO,SAAS,gBAAgB,KAAU;AACxC,SAAO,EAAE,MAAM,QAAQ,GAAG,KAAK,IAAI,WAAW;AAChD;AAmBgB,SAAA,uBACd,OACA,SAWU;AACJ,QAAA,EAAE,oBAAoB;AACtB,QAAA;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA,IACG,QAAQ,cAAc,CAAC;AAI5B,QAAM,oBAAoB,mBAAmB;AAE7C,QAAM,kBAAkB;AAAA,IACtB,OAAO;AAAA,IACP,UAAU;AAAA,IACV,YAAY,2BAA2B;AAAA,EACzC;AAEA,QAAM,gBAAgB;AAAA,IACpB,OAAO;AAAA,IACP,UAAU;AAAA,IACV,YAAY,yBAAyB;AAAA,EACvC;AAEA,QAAM,kBAAkB;AAAA,IACtB,OAAO;AAAA,IACP,UAAU;AAAA,IACV,YAAY;AAAA,EACd;AAEA,QAAM,gBAAgB,CACpB,eAII,EAAE,GAAG,WAAW,YAAY;AAElC,UAAQ,OAAO;AAAA,IACb,KAAK;AACI,aAAA;AAAA,QACL,cAAc,eAAe;AAAA,QAC7B,cAAc,aAAa;AAAA,QAC3B;AAAA,MACF;AAAA,IACF,KAAK;AACH,aAAO,CAAC,aAAa;AAAA,IACvB,KAAK;AACH,aAAO,CAAC,eAAe;AAAA,IACzB,KAAK;AAAA,IACL;AACE,aAAO,CAAC;AAAA,EAAA;AAEd;AAiBgB,SAAA,sBACd,OACA,SAWU;AACJ,QAAA,EAAE,UAAU,QAAQ,UAAU,YAAa,QAAQ,cAAc,CAAC;AAIxE,QAAM,kBAAkB,EAAE,OAAO,UAAU,UAAU,SAAS;AAC9D,QAAM,gBAAgB,EAAE,OAAO,QAAQ,UAAU,OAAO;AACxD,QAAM,kBAAkB,EAAE,OAAO,UAAU,UAAU,SAAS;AAC9D,QAAM,iBAAiB,EAAE,OAAO,SAAS,UAAU,QAAQ;AAG3D,QAAM,kBAAkB;AAAA,IACtB,OAAO;AAAA,IACP,UAAU,MAAM;AAAA,EAClB;AAEA,UAAQ,OAAO;AAAA,IACb,KAAK;AACH,aAAO,CAAC,cAAc;AAAA,IACxB,KAAK;AACI,aAAA;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF,KAAK;AACH,aAAO,CAAC,eAAe;AAAA,IACzB,KAAK;AACI,aAAA,CAAC,eAAe,eAAe;AAAA,IACxC,KAAK;AAAA,IACL;AACS,aAAA,CAAC,iBAAiB,eAAe;AAAA,EAAA;AAE9C;AAEa,MAAA,8BAA8B,CACzC,UACgD;AAChD,SAAO,CAAC,CAAC,SAAS,OAAO,UAAU,YAAY,YAAY;AAC7D;AAEgB,SAAA,QAAW,MAAS,MAAS;AAC3C,MAAI,OAAO,GAAG,MAAM,IAAI,GAAG;AAClB,WAAA;AAAA,EAAA;AAIP,MAAA,OAAO,SAAS,YAChB,SAAS,QACT,OAAO,SAAS,YAChB,SAAS,MACT;AACO,WAAA;AAAA,EAAA;AAGL,MAAA,gBAAgB,OAAO,gBAAgB,KAAK;AAC9C,QAAI,KAAK,SAAS,KAAK,KAAa,QAAA;AACpC,eAAW,CAAC,GAAG,CAAC,KAAK,MAAM;AACzB,UAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,GAAG,KAAK,IAAI,CAAC,CAAC,EAAU,QAAA;AAAA,IAAA;AAElD,WAAA;AAAA,EAAA;AAGL,MAAA,gBAAgB,OAAO,gBAAgB,KAAK;AAC9C,QAAI,KAAK,SAAS,KAAK,KAAa,QAAA;AACpC,eAAW,KAAK,MAAM;AACpB,UAAI,CAAC,KAAK,IAAI,CAAC,EAAU,QAAA;AAAA,IAAA;AAEpB,WAAA;AAAA,EAAA;AAGH,QAAA,QAAQ,OAAO,KAAK,IAAI;AAC9B,MAAI,MAAM,WAAW,OAAO,KAAK,IAAI,EAAE,QAAQ;AACtC,WAAA;AAAA,EAAA;AAGT,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AAEnC,QAAA,CAAC,OAAO,UAAU,eAAe,KAAK,MAAM,MAAM,CAAC,CAAW,KAC9D,CAAC,OAAO,GAAG,KAAK,MAAM,CAAC,CAAY,GAAG,KAAK,MAAM,CAAC,CAAY,CAAC,GAC/D;AACO,aAAA;AAAA,IAAA;AAAA,EACT;AAEK,SAAA;AACT;AAMO,MAAM,wCAAwC,CAAC;AAAA,EACpD;AAAA,EACA;AAAA,EACA;AACF,MAOK;AAEH,MAAI,uBAAuB;AACzB,WAAO,EAAE,eAAe,uBAAuB,WAAW,OAAO;AAAA,EAAA;AAInE,MAAI,kCAAkC;AACpC,WAAO,EAAE,eAAe,QAAW,WAAW,OAAU;AAAA,EAAA;AAI1D,MAAI,oBAAoB;AACtB,WAAO,EAAE,eAAe,oBAAoB,WAAW,QAAQ;AAAA,EAAA;AAIjE,SAAO,EAAE,eAAe,QAAW,WAAW,OAAU;AAC1D;AAMO,MAAM,yCAAyC,CAAC;AAAA,EACrD;AAAA,EACA;AACF,MAMK;AAEH,MAAI,iBAAiB;AACnB,WAAO,EAAE,eAAe,iBAAiB,WAAW,QAAQ;AAAA,EAAA;AAI9D,MAAI,gBAAgB;AAClB,WAAO,EAAE,eAAe,gBAAgB,WAAW,OAAO;AAAA,EAAA;AAG5D,SAAO,EAAE,eAAe,QAAW,WAAW,OAAU;AAC1D;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/form-core",
3
- "version": "1.6.2",
3
+ "version": "1.7.0",
4
4
  "description": "Powerful, type-safe, framework agnostic forms.",
5
5
  "author": "tannerlinsley",
6
6
  "license": "MIT",
@@ -40,9 +40,9 @@
40
40
  "@tanstack/store": "^0.7.0"
41
41
  },
42
42
  "devDependencies": {
43
- "arktype": "^2.1.19",
43
+ "arktype": "^2.1.20",
44
44
  "valibot": "^1.0.0",
45
- "zod": "^3.24.2"
45
+ "zod": "^3.24.3"
46
46
  },
47
47
  "scripts": {}
48
48
  }
package/src/FieldApi.ts CHANGED
@@ -4,7 +4,12 @@ import {
4
4
  standardSchemaValidators,
5
5
  } from './standardSchemaValidator'
6
6
  import { defaultFieldMeta } from './metaHelper'
7
- import { getAsyncValidatorArray, getBy, getSyncValidatorArray } from './utils'
7
+ import {
8
+ determineFieldLevelErrorSourceAndValue,
9
+ getAsyncValidatorArray,
10
+ getBy,
11
+ getSyncValidatorArray,
12
+ } from './utils'
8
13
  import type { DeepKeys, DeepValue, UnwrapOneLevelOfArray } from './util-types'
9
14
  import type {
10
15
  StandardSchemaV1,
@@ -25,6 +30,7 @@ import type {
25
30
  ValidationCause,
26
31
  ValidationError,
27
32
  ValidationErrorMap,
33
+ ValidationErrorMapSource,
28
34
  } from './types'
29
35
  import type { AsyncValidator, SyncValidator, Updater } from './utils'
30
36
 
@@ -561,6 +567,10 @@ export type FieldMetaBase<
561
567
  UnwrapFieldValidateOrFn<TName, TOnSubmit, TFormOnSubmit>,
562
568
  UnwrapFieldAsyncValidateOrFn<TName, TOnSubmitAsync, TFormOnSubmitAsync>
563
569
  >
570
+ /**
571
+ * @private allows tracking the source of the errors in the error map
572
+ */
573
+ errorSourceMap: ValidationErrorMapSource
564
574
  /**
565
575
  * A flag indicating whether the field is currently being validated.
566
576
  */
@@ -1101,6 +1111,11 @@ export class FieldApi<
1101
1111
  ...prev,
1102
1112
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1103
1113
  errorMap: { ...prev?.errorMap, onMount: error },
1114
+ errorSourceMap: {
1115
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1116
+ ...prev?.errorSourceMap,
1117
+ onMount: 'field',
1118
+ },
1104
1119
  }) as never,
1105
1120
  )
1106
1121
  }
@@ -1345,39 +1360,43 @@ export class FieldApi<
1345
1360
  ) => {
1346
1361
  const errorMapKey = getErrorMapKey(validateObj.cause)
1347
1362
 
1348
- const error =
1349
- /*
1350
- If `validateObj.validate` is `undefined`, then the field doesn't have
1351
- a validator for this event, but there still could be an error that
1352
- needs to be cleaned up related to the current event left by the
1353
- form's validator.
1354
- */
1355
- validateObj.validate
1356
- ? normalizeError(
1357
- field.runValidator({
1358
- validate: validateObj.validate,
1359
- value: {
1360
- value: field.store.state.value,
1361
- validationSource: 'field',
1362
- fieldApi: field,
1363
- },
1364
- type: 'validate',
1365
- }),
1366
- )
1367
- : errorFromForm[errorMapKey]
1363
+ const fieldLevelError = validateObj.validate
1364
+ ? normalizeError(
1365
+ field.runValidator({
1366
+ validate: validateObj.validate,
1367
+ value: {
1368
+ value: field.store.state.value,
1369
+ validationSource: 'field',
1370
+ fieldApi: field,
1371
+ },
1372
+ type: 'validate',
1373
+ }),
1374
+ )
1375
+ : undefined
1376
+
1377
+ const formLevelError = errorFromForm[errorMapKey]
1368
1378
 
1369
- if (field.state.meta.errorMap[errorMapKey] !== error) {
1379
+ const { newErrorValue, newSource } =
1380
+ determineFieldLevelErrorSourceAndValue({
1381
+ formLevelError,
1382
+ fieldLevelError,
1383
+ })
1384
+
1385
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1386
+ if (field.state.meta.errorMap?.[errorMapKey] !== newErrorValue) {
1370
1387
  field.setMeta((prev) => ({
1371
1388
  ...prev,
1372
1389
  errorMap: {
1373
1390
  ...prev.errorMap,
1374
- [getErrorMapKey(validateObj.cause)]:
1375
- // Prefer the error message from the field validators if they exist
1376
- error ? error : errorFromForm[errorMapKey],
1391
+ [errorMapKey]: newErrorValue,
1392
+ },
1393
+ errorSourceMap: {
1394
+ ...prev.errorSourceMap,
1395
+ [errorMapKey]: newSource,
1377
1396
  },
1378
1397
  }))
1379
1398
  }
1380
- if (error || errorFromForm[errorMapKey]) {
1399
+ if (newErrorValue) {
1381
1400
  hasErrored = true
1382
1401
  }
1383
1402
  }
@@ -1398,7 +1417,8 @@ export class FieldApi<
1398
1417
  const submitErrKey = getErrorMapKey('submit')
1399
1418
 
1400
1419
  if (
1401
- this.state.meta.errorMap[submitErrKey] &&
1420
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1421
+ this.state.meta.errorMap?.[submitErrKey] &&
1402
1422
  cause !== 'submit' &&
1403
1423
  !hasErrored
1404
1424
  ) {
@@ -1408,6 +1428,10 @@ export class FieldApi<
1408
1428
  ...prev.errorMap,
1409
1429
  [submitErrKey]: undefined,
1410
1430
  },
1431
+ errorSourceMap: {
1432
+ ...prev.errorSourceMap,
1433
+ [submitErrKey]: undefined,
1434
+ },
1411
1435
  }))
1412
1436
  }
1413
1437
 
@@ -1521,22 +1545,33 @@ export class FieldApi<
1521
1545
  rawError = e as ValidationError
1522
1546
  }
1523
1547
  if (controller.signal.aborted) return resolve(undefined)
1524
- const error = normalizeError(rawError)
1525
- const fieldErrorFromForm =
1548
+
1549
+ const fieldLevelError = normalizeError(rawError)
1550
+ const formLevelError =
1526
1551
  asyncFormValidationResults[this.name]?.[errorMapKey]
1527
- const fieldError = error || fieldErrorFromForm
1552
+
1553
+ const { newErrorValue, newSource } =
1554
+ determineFieldLevelErrorSourceAndValue({
1555
+ formLevelError,
1556
+ fieldLevelError,
1557
+ })
1558
+
1528
1559
  field.setMeta((prev) => {
1529
1560
  return {
1530
1561
  ...prev,
1531
1562
  errorMap: {
1532
1563
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1533
1564
  ...prev?.errorMap,
1534
- [errorMapKey]: fieldError,
1565
+ [errorMapKey]: newErrorValue,
1566
+ },
1567
+ errorSourceMap: {
1568
+ ...prev.errorSourceMap,
1569
+ [errorMapKey]: newSource,
1535
1570
  },
1536
1571
  }
1537
1572
  })
1538
1573
 
1539
- resolve(fieldError)
1574
+ resolve(newErrorValue)
1540
1575
  }),
1541
1576
  )
1542
1577
  }
@@ -1669,6 +1704,10 @@ export class FieldApi<
1669
1704
 
1670
1705
  private triggerOnBlurListener() {
1671
1706
  const debounceMs = this.options.listeners?.onBlurDebounceMs
1707
+ this.form.options.listeners?.onBlur?.({
1708
+ formApi: this.form,
1709
+ fieldApi: this,
1710
+ })
1672
1711
 
1673
1712
  if (debounceMs && debounceMs > 0) {
1674
1713
  if (this.timeoutIds.listeners.blur) {
@@ -1692,6 +1731,11 @@ export class FieldApi<
1692
1731
  private triggerOnChangeListener() {
1693
1732
  const debounceMs = this.options.listeners?.onChangeDebounceMs
1694
1733
 
1734
+ this.form.options.listeners?.onChange?.({
1735
+ formApi: this.form,
1736
+ fieldApi: this,
1737
+ })
1738
+
1695
1739
  if (debounceMs && debounceMs > 0) {
1696
1740
  if (this.timeoutIds.listeners.change) {
1697
1741
  clearTimeout(this.timeoutIds.listeners.change)
package/src/FormApi.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Derived, Store, batch } from '@tanstack/store'
2
2
  import {
3
3
  deleteBy,
4
+ determineFormLevelErrorSourceAndValue,
4
5
  functionalUpdate,
5
6
  getAsyncValidatorArray,
6
7
  getBy,
@@ -21,7 +22,12 @@ import type {
21
22
  StandardSchemaV1Issue,
22
23
  TStandardSchemaValidatorValue,
23
24
  } from './standardSchemaValidator'
24
- import type { AnyFieldMeta, AnyFieldMetaBase, FieldApi } from './FieldApi'
25
+ import type {
26
+ AnyFieldApi,
27
+ AnyFieldMeta,
28
+ AnyFieldMetaBase,
29
+ FieldApi,
30
+ } from './FieldApi'
25
31
  import type {
26
32
  FormValidationError,
27
33
  FormValidationErrorMap,
@@ -232,6 +238,81 @@ export interface FormTransform<
232
238
  deps: unknown[]
233
239
  }
234
240
 
241
+ export interface FormListeners<
242
+ TFormData,
243
+ TOnMount extends undefined | FormValidateOrFn<TFormData>,
244
+ TOnChange extends undefined | FormValidateOrFn<TFormData>,
245
+ TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
246
+ TOnBlur extends undefined | FormValidateOrFn<TFormData>,
247
+ TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
248
+ TOnSubmit extends undefined | FormValidateOrFn<TFormData>,
249
+ TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>,
250
+ TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>,
251
+ TSubmitMeta = never,
252
+ > {
253
+ onChange?: (props: {
254
+ formApi: FormApi<
255
+ TFormData,
256
+ TOnMount,
257
+ TOnChange,
258
+ TOnChangeAsync,
259
+ TOnBlur,
260
+ TOnBlurAsync,
261
+ TOnSubmit,
262
+ TOnSubmitAsync,
263
+ TOnServer,
264
+ TSubmitMeta
265
+ >
266
+ fieldApi: AnyFieldApi
267
+ }) => void
268
+
269
+ onBlur?: (props: {
270
+ formApi: FormApi<
271
+ TFormData,
272
+ TOnMount,
273
+ TOnChange,
274
+ TOnChangeAsync,
275
+ TOnBlur,
276
+ TOnBlurAsync,
277
+ TOnSubmit,
278
+ TOnSubmitAsync,
279
+ TOnServer,
280
+ TSubmitMeta
281
+ >
282
+ fieldApi: AnyFieldApi
283
+ }) => void
284
+
285
+ onMount?: (props: {
286
+ formApi: FormApi<
287
+ TFormData,
288
+ TOnMount,
289
+ TOnChange,
290
+ TOnChangeAsync,
291
+ TOnBlur,
292
+ TOnBlurAsync,
293
+ TOnSubmit,
294
+ TOnSubmitAsync,
295
+ TOnServer,
296
+ TSubmitMeta
297
+ >
298
+ }) => void
299
+
300
+ onSubmit?: (props: {
301
+ formApi: FormApi<
302
+ TFormData,
303
+ TOnMount,
304
+ TOnChange,
305
+ TOnChangeAsync,
306
+ TOnBlur,
307
+ TOnBlurAsync,
308
+ TOnSubmit,
309
+ TOnSubmitAsync,
310
+ TOnServer,
311
+ TSubmitMeta
312
+ >
313
+ }) => void
314
+ }
315
+
235
316
  /**
236
317
  * An object representing the options for a form.
237
318
  */
@@ -298,6 +379,22 @@ export interface FormOptions<
298
379
  */
299
380
  onSubmitMeta?: TSubmitMeta
300
381
 
382
+ /**
383
+ * form level listeners
384
+ */
385
+ listeners?: FormListeners<
386
+ TFormData,
387
+ TOnMount,
388
+ TOnChange,
389
+ TOnChangeAsync,
390
+ TOnBlur,
391
+ TOnBlurAsync,
392
+ TOnSubmit,
393
+ TOnSubmitAsync,
394
+ TOnServer,
395
+ TSubmitMeta
396
+ >
397
+
301
398
  /**
302
399
  * A function to be called when the form is submitted, what should happen once the user submits a valid form returns `any` or a promise `Promise<any>`
303
400
  */
@@ -733,22 +830,6 @@ export class FormApi<
733
830
  */
734
831
  prevTransformArray: unknown[] = []
735
832
 
736
- /**
737
- * @private Persistent store of all field validation errors originating from form-level validators.
738
- * Maintains the cumulative state across validation cycles, including cleared errors (undefined values).
739
- * This map preserves the complete validation state for all fields.
740
- */
741
- cumulativeFieldsErrorMap: FormErrorMapFromValidator<
742
- TFormData,
743
- TOnMount,
744
- TOnChange,
745
- TOnChangeAsync,
746
- TOnBlur,
747
- TOnBlurAsync,
748
- TOnSubmit,
749
- TOnSubmitAsync
750
- > = {}
751
-
752
833
  /**
753
834
  * Constructs a new `FormApi` instance with the given form options.
754
835
  */
@@ -1064,6 +1145,9 @@ export class FormApi<
1064
1145
  cleanupFieldMetaDerived()
1065
1146
  cleanupStoreDerived()
1066
1147
  }
1148
+
1149
+ this.options.listeners?.onMount?.({ formApi: this })
1150
+
1067
1151
  const { onMount } = this.options.validators || {}
1068
1152
  if (!onMount) return cleanup
1069
1153
  this.validateSync('mount')
@@ -1306,56 +1390,56 @@ export class FormApi<
1306
1390
 
1307
1391
  const errorMapKey = getErrorMapKey(validateObj.cause)
1308
1392
 
1309
- if (fieldErrors) {
1310
- for (const [field, fieldError] of Object.entries(fieldErrors) as [
1311
- DeepKeys<TFormData>,
1312
- ValidationError,
1313
- ][]) {
1314
- const oldErrorMap = this.cumulativeFieldsErrorMap[field] || {}
1315
- const newErrorMap = {
1316
- ...oldErrorMap,
1317
- [errorMapKey]: fieldError,
1318
- }
1319
- currentValidationErrorMap[field] = newErrorMap
1320
- this.cumulativeFieldsErrorMap[field] = newErrorMap
1393
+ for (const field of Object.keys(
1394
+ this.state.fieldMeta,
1395
+ ) as DeepKeys<TFormData>[]) {
1396
+ const fieldMeta = this.getFieldMeta(field)
1397
+ if (!fieldMeta) continue
1398
+
1399
+ const {
1400
+ errorMap: currentErrorMap,
1401
+ errorSourceMap: currentErrorMapSource,
1402
+ } = fieldMeta
1403
+
1404
+ const newFormValidatorError = fieldErrors?.[field]
1405
+
1406
+ const { newErrorValue, newSource } =
1407
+ determineFormLevelErrorSourceAndValue({
1408
+ newFormValidatorError,
1409
+ isPreviousErrorFromFormValidator:
1410
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1411
+ currentErrorMapSource?.[errorMapKey] === 'form',
1412
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1413
+ previousErrorValue: currentErrorMap?.[errorMapKey],
1414
+ })
1321
1415
 
1322
- const fieldMeta = this.getFieldMeta(field)
1323
- if (fieldMeta && fieldMeta.errorMap[errorMapKey] !== fieldError) {
1324
- this.setFieldMeta(field, (prev) => ({
1325
- ...prev,
1326
- errorMap: {
1327
- ...prev.errorMap,
1328
- [errorMapKey]: fieldError,
1329
- },
1330
- }))
1416
+ if (newSource === 'form') {
1417
+ currentValidationErrorMap[field] = {
1418
+ ...currentValidationErrorMap[field],
1419
+ [errorMapKey]: newFormValidatorError,
1331
1420
  }
1332
1421
  }
1333
- }
1334
1422
 
1335
- for (const field of Object.keys(this.cumulativeFieldsErrorMap) as Array<
1336
- DeepKeys<TFormData>
1337
- >) {
1338
- const fieldMeta = this.getFieldMeta(field)
1339
1423
  if (
1340
- fieldMeta?.errorMap[errorMapKey] &&
1341
- !currentValidationErrorMap[field]?.[errorMapKey]
1424
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1425
+ currentErrorMap?.[errorMapKey] !== newErrorValue
1342
1426
  ) {
1343
- this.cumulativeFieldsErrorMap[field] = {
1344
- ...this.cumulativeFieldsErrorMap[field],
1345
- [errorMapKey]: undefined,
1346
- }
1347
-
1348
1427
  this.setFieldMeta(field, (prev) => ({
1349
1428
  ...prev,
1350
1429
  errorMap: {
1351
1430
  ...prev.errorMap,
1352
- [errorMapKey]: undefined,
1431
+ [errorMapKey]: newErrorValue,
1432
+ },
1433
+ errorSourceMap: {
1434
+ ...prev.errorSourceMap,
1435
+ [errorMapKey]: newSource,
1353
1436
  },
1354
1437
  }))
1355
1438
  }
1356
1439
  }
1357
1440
 
1358
- if (this.state.errorMap[errorMapKey] !== formError) {
1441
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1442
+ if (this.state.errorMap?.[errorMapKey] !== formError) {
1359
1443
  this.baseStore.setState((prev) => ({
1360
1444
  ...prev,
1361
1445
  errorMap: {
@@ -1376,7 +1460,8 @@ export class FormApi<
1376
1460
  */
1377
1461
  const submitErrKey = getErrorMapKey('submit')
1378
1462
  if (
1379
- this.state.errorMap[submitErrKey] &&
1463
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1464
+ this.state.errorMap?.[submitErrKey] &&
1380
1465
  cause !== 'submit' &&
1381
1466
  !hasErrored
1382
1467
  ) {
@@ -1422,7 +1507,7 @@ export class FormApi<
1422
1507
  */
1423
1508
  const promises: Promise<ValidationPromiseResult<TFormData>>[] = []
1424
1509
 
1425
- let fieldErrors:
1510
+ let fieldErrorsFromFormValidators:
1426
1511
  | Partial<Record<DeepKeys<TFormData>, ValidationError>>
1427
1512
  | undefined
1428
1513
 
@@ -1473,26 +1558,56 @@ export class FormApi<
1473
1558
  normalizeError<TFormData>(rawError)
1474
1559
 
1475
1560
  if (fieldErrorsFromNormalizeError) {
1476
- fieldErrors = fieldErrors
1477
- ? { ...fieldErrors, ...fieldErrorsFromNormalizeError }
1561
+ fieldErrorsFromFormValidators = fieldErrorsFromFormValidators
1562
+ ? {
1563
+ ...fieldErrorsFromFormValidators,
1564
+ ...fieldErrorsFromNormalizeError,
1565
+ }
1478
1566
  : fieldErrorsFromNormalizeError
1479
1567
  }
1480
1568
  const errorMapKey = getErrorMapKey(validateObj.cause)
1481
1569
 
1482
- if (fieldErrors) {
1483
- for (const [field, fieldError] of Object.entries(fieldErrors)) {
1484
- const fieldMeta = this.getFieldMeta(field as DeepKeys<TFormData>)
1485
- if (fieldMeta && fieldMeta.errorMap[errorMapKey] !== fieldError) {
1486
- this.setFieldMeta(field as DeepKeys<TFormData>, (prev) => ({
1487
- ...prev,
1488
- errorMap: {
1489
- ...prev.errorMap,
1490
- [errorMapKey]: fieldError,
1491
- },
1492
- }))
1493
- }
1570
+ for (const field of Object.keys(
1571
+ this.state.fieldMeta,
1572
+ ) as DeepKeys<TFormData>[]) {
1573
+ const fieldMeta = this.getFieldMeta(field)
1574
+ if (!fieldMeta) continue
1575
+
1576
+ const {
1577
+ errorMap: currentErrorMap,
1578
+ errorSourceMap: currentErrorMapSource,
1579
+ } = fieldMeta
1580
+
1581
+ const newFormValidatorError = fieldErrorsFromFormValidators?.[field]
1582
+
1583
+ const { newErrorValue, newSource } =
1584
+ determineFormLevelErrorSourceAndValue({
1585
+ newFormValidatorError,
1586
+ isPreviousErrorFromFormValidator:
1587
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1588
+ currentErrorMapSource?.[errorMapKey] === 'form',
1589
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1590
+ previousErrorValue: currentErrorMap?.[errorMapKey],
1591
+ })
1592
+
1593
+ if (
1594
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1595
+ currentErrorMap?.[errorMapKey] !== newErrorValue
1596
+ ) {
1597
+ this.setFieldMeta(field, (prev) => ({
1598
+ ...prev,
1599
+ errorMap: {
1600
+ ...prev.errorMap,
1601
+ [errorMapKey]: newErrorValue,
1602
+ },
1603
+ errorSourceMap: {
1604
+ ...prev.errorSourceMap,
1605
+ [errorMapKey]: newSource,
1606
+ },
1607
+ }))
1494
1608
  }
1495
1609
  }
1610
+
1496
1611
  this.baseStore.setState((prev) => ({
1497
1612
  ...prev,
1498
1613
  errorMap: {
@@ -1501,7 +1616,11 @@ export class FormApi<
1501
1616
  },
1502
1617
  }))
1503
1618
 
1504
- resolve(fieldErrors ? { fieldErrors, errorMapKey } : undefined)
1619
+ resolve(
1620
+ fieldErrorsFromFormValidators
1621
+ ? { fieldErrors: fieldErrorsFromFormValidators, errorMapKey }
1622
+ : undefined,
1623
+ )
1505
1624
  }),
1506
1625
  )
1507
1626
  }
@@ -1587,7 +1706,7 @@ export class FormApi<
1587
1706
  }
1588
1707
 
1589
1708
  /**
1590
- * Handles the form submission, performs validation, and calls the appropriate onSubmit or onInvalidSubmit callbacks.
1709
+ * Handles the form submission, performs validation, and calls the appropriate onSubmit or onSubmitInvalid callbacks.
1591
1710
  */
1592
1711
  handleSubmit(): Promise<void>
1593
1712
  handleSubmit(submitMeta: TSubmitMeta): Promise<void>
@@ -1656,6 +1775,8 @@ export class FormApi<
1656
1775
  )
1657
1776
  })
1658
1777
 
1778
+ this.options.listeners?.onSubmit?.({ formApi: this })
1779
+
1659
1780
  try {
1660
1781
  // Run the submit code
1661
1782
  await this.options.onSubmit?.({
package/src/metaHelper.ts CHANGED
@@ -16,6 +16,7 @@ export const defaultFieldMeta: AnyFieldMeta = {
16
16
  isPristine: true,
17
17
  errors: [],
18
18
  errorMap: {},
19
+ errorSourceMap: {},
19
20
  }
20
21
 
21
22
  export function metaHelper<