@tanstack/form-core 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/build/legacy/FieldApi.cjs +33 -10
  2. package/build/legacy/FieldApi.cjs.map +1 -1
  3. package/build/legacy/FieldApi.js +33 -10
  4. package/build/legacy/FieldApi.js.map +1 -1
  5. package/build/legacy/FormApi.cjs +1 -1
  6. package/build/legacy/FormApi.cjs.map +1 -1
  7. package/build/legacy/FormApi.js +8 -2
  8. package/build/legacy/FormApi.js.map +1 -1
  9. package/build/legacy/index.d.cts +2 -1
  10. package/build/legacy/index.d.ts +2 -1
  11. package/build/legacy/utils.cjs +35 -6
  12. package/build/legacy/utils.cjs.map +1 -1
  13. package/build/legacy/utils.d.cts +5 -1
  14. package/build/legacy/utils.d.ts +5 -1
  15. package/build/legacy/utils.js +34 -6
  16. package/build/legacy/utils.js.map +1 -1
  17. package/build/modern/FieldApi.cjs +33 -10
  18. package/build/modern/FieldApi.cjs.map +1 -1
  19. package/build/modern/FieldApi.js +33 -10
  20. package/build/modern/FieldApi.js.map +1 -1
  21. package/build/modern/FormApi.cjs +1 -1
  22. package/build/modern/FormApi.cjs.map +1 -1
  23. package/build/modern/FormApi.js +8 -2
  24. package/build/modern/FormApi.js.map +1 -1
  25. package/build/modern/index.d.cts +2 -1
  26. package/build/modern/index.d.ts +2 -1
  27. package/build/modern/utils.cjs +35 -6
  28. package/build/modern/utils.cjs.map +1 -1
  29. package/build/modern/utils.d.cts +5 -1
  30. package/build/modern/utils.d.ts +5 -1
  31. package/build/modern/utils.js +34 -6
  32. package/build/modern/utils.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/FieldApi.ts +53 -10
  35. package/src/FormApi.ts +9 -2
  36. package/src/tests/FieldApi.spec.ts +19 -0
  37. package/src/tests/FormApi.spec.ts +149 -1
  38. package/src/tests/utils.spec.ts +73 -0
  39. package/src/utils.ts +42 -6
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils.ts"],"sourcesContent":["export type UpdaterFn<TInput, TOutput = TInput> = (input: TInput) => TOutput\n\nexport type Updater<TInput, TOutput = TInput> =\n | TOutput\n | UpdaterFn<TInput, TOutput>\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 */\nexport function getBy(obj: any, path: any) {\n const pathArray = makePathArray(path)\n const pathObj = pathArray\n return pathObj.reduce((current: any, pathPart: any) => {\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 */\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 (typeof key === 'string') {\n if (typeof parent === 'object') {\n return {\n ...parent,\n [key]: doSet(parent[key]),\n }\n }\n return {\n [key]: doSet(),\n }\n }\n\n if (typeof key === 'number') {\n if (Array.isArray(parent)) {\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 throw new Error('Uh oh!')\n }\n\n return doSet(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\nfunction makePathArray(str: string) {\n if (typeof str !== 'string') {\n throw new Error('Path must be a string.')\n }\n\n return str\n .replace('[', '.')\n .replace(']', '')\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\nexport function isNonEmptyArray(obj: any) {\n return !(Array.isArray(obj) && obj.length === 0)\n}\n\nexport type RequiredByKey<T, K extends keyof T> = Omit<T, K> &\n Required<Pick<T, K>>\n\ntype ComputeRange<\n N extends number,\n Result extends Array<unknown> = [],\n> = Result['length'] extends N\n ? Result\n : ComputeRange<N, [...Result, Result['length']]>\ntype Index40 = ComputeRange<40>[number]\n\n// Is this type a tuple?\ntype IsTuple<T> = T extends readonly any[] & { length: infer Length }\n ? Length extends Index40\n ? T\n : never\n : never\n\n// If this type is a tuple, what indices are allowed?\ntype AllowedIndexes<\n Tuple extends ReadonlyArray<any>,\n Keys extends number = never,\n> = Tuple extends readonly []\n ? Keys\n : Tuple extends readonly [infer _, ...infer Tail]\n ? AllowedIndexes<Tail, Keys | Tail['length']>\n : Keys\n\nexport type DeepKeys<T, TDepth extends any[] = []> = TDepth['length'] extends 5\n ? never\n : unknown extends T\n ? string\n : object extends T\n ? string\n : T extends readonly any[] & IsTuple<T>\n ? AllowedIndexes<T> | DeepKeysPrefix<T, AllowedIndexes<T>, TDepth>\n : T extends any[]\n ? DeepKeys<T[number], [...TDepth, any]>\n : T extends Date\n ? never\n : T extends object\n ? (keyof T & string) | DeepKeysPrefix<T, keyof T, TDepth>\n : never\n\ntype DeepKeysPrefix<\n T,\n TPrefix,\n TDepth extends any[],\n> = TPrefix extends keyof T & (number | string)\n ? `${TPrefix}.${DeepKeys<T[TPrefix], [...TDepth, any]> & string}`\n : never\n\nexport type DeepValue<T, TProp> = T extends Record<string | number, any>\n ? TProp extends `${infer TBranch}.${infer TDeepProp}`\n ? DeepValue<T[TBranch], TDeepProp>\n : T[TProp & string]\n : never\n\ntype Narrowable = string | number | bigint | boolean\n\ntype NarrowRaw<A> =\n | (A extends [] ? [] : never)\n | (A extends Narrowable ? A : never)\n | {\n [K in keyof A]: A[K] extends Function ? A[K] : NarrowRaw<A[K]>\n }\n\nexport type Narrow<A> = Try<A, [], NarrowRaw<A>>\n\ntype Try<A1, A2, Catch = never> = A1 extends A2 ? A1 : Catch\n\n// Hack to get TypeScript to show simplified types in error messages\nexport type Pretty<T> = { [K in keyof T]: T[K] } & {}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAMO,SAAS,iBACd,SACA,OACS;AACT,SAAO,OAAO,YAAY,aACrB,QAAuC,KAAK,IAC7C;AACN;AAKO,SAAS,MAAM,KAAU,MAAW;AACzC,QAAM,YAAY,cAAc,IAAI;AACpC,QAAM,UAAU;AAChB,SAAO,QAAQ,OAAO,CAAC,SAAc,aAAkB;AACrD,QAAI,OAAO,YAAY,aAAa;AAClC,aAAO,QAAQ,QAAQ;AAAA,IACzB;AACA,WAAO;AAAA,EACT,GAAG,GAAG;AACR;AAKO,SAAS,MAAM,KAAU,OAAY,SAAuB;AACjE,QAAM,OAAO,cAAc,KAAK;AAEhC,WAAS,MAAM,QAAmB;AAChC,QAAI,CAAC,KAAK,QAAQ;AAChB,aAAO,iBAAiB,SAAS,MAAM;AAAA,IACzC;AAEA,UAAM,MAAM,KAAK,MAAM;AAEvB,QAAI,OAAO,QAAQ,UAAU;AAC3B,UAAI,OAAO,WAAW,UAAU;AAC9B,eAAO;AAAA,UACL,GAAG;AAAA,UACH,CAAC,GAAG,GAAG,MAAM,OAAO,GAAG,CAAC;AAAA,QAC1B;AAAA,MACF;AACA,aAAO;AAAA,QACL,CAAC,GAAG,GAAG,MAAM;AAAA,MACf;AAAA,IACF;AAEA,QAAI,OAAO,QAAQ,UAAU;AAC3B,UAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,cAAM,SAAS,OAAO,MAAM,GAAG,GAAG;AAClC,eAAO;AAAA,UACL,GAAI,OAAO,SAAS,SAAS,IAAI,MAAM,GAAG;AAAA,UAC1C,MAAM,OAAO,GAAG,CAAC;AAAA,UACjB,GAAG,OAAO,MAAM,MAAM,CAAC;AAAA,QACzB;AAAA,MACF;AACA,aAAO,CAAC,GAAG,IAAI,MAAM,GAAG,GAAG,MAAM,CAAC;AAAA,IACpC;AAEA,UAAM,IAAI,MAAM,QAAQ;AAAA,EAC1B;AAEA,SAAO,MAAM,GAAG;AAClB;AAEA,IAAM,iBAAiB;AACvB,IAAM,iBAAiB;AACvB,IAAM,iBAAiB;AACvB,IAAM,iBAAiB;AACvB,IAAM,wBAAwB;AAE9B,IAAM,YAAY;AAClB,IAAM,aAAa,GAAG,SAAS;AAE/B,SAAS,cAAc,KAAa;AAClC,MAAI,OAAO,QAAQ,UAAU;AAC3B,UAAM,IAAI,MAAM,wBAAwB;AAAA,EAC1C;AAEA,SAAO,IACJ,QAAQ,KAAK,GAAG,EAChB,QAAQ,KAAK,EAAE,EACf,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,IACnD;AACA,WAAO;AAAA,EACT,CAAC;AACL;AAEO,SAAS,gBAAgB,KAAU;AACxC,SAAO,EAAE,MAAM,QAAQ,GAAG,KAAK,IAAI,WAAW;AAChD;","names":[]}
1
+ {"version":3,"sources":["../../src/utils.ts"],"sourcesContent":["export type UpdaterFn<TInput, TOutput = TInput> = (input: TInput) => TOutput\n\nexport type Updater<TInput, TOutput = TInput> =\n | TOutput\n | UpdaterFn<TInput, TOutput>\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 */\nexport function getBy(obj: any, path: any) {\n const pathObj = makePathArray(path)\n return pathObj.reduce((current: any, pathPart: any) => {\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 */\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 (typeof key === 'string') {\n if (typeof parent === 'object') {\n return {\n ...parent,\n [key]: doSet(parent[key]),\n }\n }\n return {\n [key]: doSet(),\n }\n }\n\n if (Array.isArray(parent) && key !== undefined) {\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 */\nexport function deleteBy(obj: any, _path: any) {\n const path = makePathArray(_path)\n\n function doDelete(parent: any): any {\n if (path.length === 1) {\n const finalPath = path[0]!\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 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\nfunction makePathArray(str: string) {\n if (typeof str !== 'string') {\n throw new Error('Path must be a string.')\n }\n\n return str\n .replace('[', '.')\n .replace(']', '')\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\nexport function isNonEmptyArray(obj: any) {\n return !(Array.isArray(obj) && obj.length === 0)\n}\n\nexport type RequiredByKey<T, K extends keyof T> = Omit<T, K> &\n Required<Pick<T, K>>\n\ntype ComputeRange<\n N extends number,\n Result extends Array<unknown> = [],\n> = Result['length'] extends N\n ? Result\n : ComputeRange<N, [...Result, Result['length']]>\ntype Index40 = ComputeRange<40>[number]\n\n// Is this type a tuple?\ntype IsTuple<T> = T extends readonly any[] & { length: infer Length }\n ? Length extends Index40\n ? T\n : never\n : never\n\n// If this type is a tuple, what indices are allowed?\ntype AllowedIndexes<\n Tuple extends ReadonlyArray<any>,\n Keys extends number = never,\n> = Tuple extends readonly []\n ? Keys\n : Tuple extends readonly [infer _, ...infer Tail]\n ? AllowedIndexes<Tail, Keys | Tail['length']>\n : Keys\n\nexport type DeepKeys<T, TDepth extends any[] = []> = TDepth['length'] extends 5\n ? never\n : unknown extends T\n ? string\n : object extends T\n ? string\n : T extends readonly any[] & IsTuple<T>\n ? AllowedIndexes<T> | DeepKeysPrefix<T, AllowedIndexes<T>, TDepth>\n : T extends any[]\n ? DeepKeys<T[number], [...TDepth, any]>\n : T extends Date\n ? never\n : T extends object\n ? (keyof T & string) | DeepKeysPrefix<T, keyof T, TDepth>\n : never\n\ntype DeepKeysPrefix<\n T,\n TPrefix,\n TDepth extends any[],\n> = TPrefix extends keyof T & (number | string)\n ? `${TPrefix}.${DeepKeys<T[TPrefix], [...TDepth, any]> & string}`\n : never\n\nexport type DeepValue<T, TProp> = T extends Record<string | number, any>\n ? TProp extends `${infer TBranch}.${infer TDeepProp}`\n ? DeepValue<T[TBranch], TDeepProp>\n : T[TProp & string]\n : never\n\ntype Narrowable = string | number | bigint | boolean\n\ntype NarrowRaw<A> =\n | (A extends [] ? [] : never)\n | (A extends Narrowable ? A : never)\n | {\n [K in keyof A]: A[K] extends Function ? A[K] : NarrowRaw<A[K]>\n }\n\nexport type Narrow<A> = Try<A, [], NarrowRaw<A>>\n\ntype Try<A1, A2, Catch = never> = A1 extends A2 ? A1 : Catch\n\n// Hack to get TypeScript to show simplified types in error messages\nexport type Pretty<T> = { [K in keyof T]: T[K] } & {}\n"],"mappings":"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","names":[]}
@@ -9,6 +9,10 @@ declare function getBy(obj: any, path: any): any;
9
9
  * Set a value on an object using a path, including dot notation.
10
10
  */
11
11
  declare function setBy(obj: any, _path: any, updater: Updater<any>): any;
12
+ /**
13
+ * Delete a field on an object using a path, including dot notation.
14
+ */
15
+ declare function deleteBy(obj: any, _path: any): any;
12
16
  declare function isNonEmptyArray(obj: any): boolean;
13
17
  type RequiredByKey<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
14
18
  type ComputeRange<N extends number, Result extends Array<unknown> = []> = Result['length'] extends N ? Result : ComputeRange<N, [...Result, Result['length']]>;
@@ -30,4 +34,4 @@ type Pretty<T> = {
30
34
  [K in keyof T]: T[K];
31
35
  } & {};
32
36
 
33
- export { DeepKeys, DeepValue, Narrow, Pretty, RequiredByKey, Updater, UpdaterFn, functionalUpdate, getBy, isNonEmptyArray, setBy };
37
+ export { DeepKeys, DeepValue, Narrow, Pretty, RequiredByKey, Updater, UpdaterFn, deleteBy, functionalUpdate, getBy, isNonEmptyArray, setBy };
@@ -9,6 +9,10 @@ declare function getBy(obj: any, path: any): any;
9
9
  * Set a value on an object using a path, including dot notation.
10
10
  */
11
11
  declare function setBy(obj: any, _path: any, updater: Updater<any>): any;
12
+ /**
13
+ * Delete a field on an object using a path, including dot notation.
14
+ */
15
+ declare function deleteBy(obj: any, _path: any): any;
12
16
  declare function isNonEmptyArray(obj: any): boolean;
13
17
  type RequiredByKey<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
14
18
  type ComputeRange<N extends number, Result extends Array<unknown> = []> = Result['length'] extends N ? Result : ComputeRange<N, [...Result, Result['length']]>;
@@ -30,4 +34,4 @@ type Pretty<T> = {
30
34
  [K in keyof T]: T[K];
31
35
  } & {};
32
36
 
33
- export { DeepKeys, DeepValue, Narrow, Pretty, RequiredByKey, Updater, UpdaterFn, functionalUpdate, getBy, isNonEmptyArray, setBy };
37
+ export { DeepKeys, DeepValue, Narrow, Pretty, RequiredByKey, Updater, UpdaterFn, deleteBy, functionalUpdate, getBy, isNonEmptyArray, setBy };
@@ -3,8 +3,7 @@ function functionalUpdate(updater, input) {
3
3
  return typeof updater === "function" ? updater(input) : updater;
4
4
  }
5
5
  function getBy(obj, path) {
6
- const pathArray = makePathArray(path);
7
- const pathObj = pathArray;
6
+ const pathObj = makePathArray(path);
8
7
  return pathObj.reduce((current, pathPart) => {
9
8
  if (typeof current !== "undefined") {
10
9
  return current[pathPart];
@@ -30,20 +29,48 @@ function setBy(obj, _path, updater) {
30
29
  [key]: doSet()
31
30
  };
32
31
  }
32
+ if (Array.isArray(parent) && key !== void 0) {
33
+ const prefix = parent.slice(0, key);
34
+ return [
35
+ ...prefix.length ? prefix : new Array(key),
36
+ doSet(parent[key]),
37
+ ...parent.slice(key + 1)
38
+ ];
39
+ }
40
+ return [...new Array(key), doSet()];
41
+ }
42
+ return doSet(obj);
43
+ }
44
+ function deleteBy(obj, _path) {
45
+ const path = makePathArray(_path);
46
+ function doDelete(parent) {
47
+ if (path.length === 1) {
48
+ const finalPath = path[0];
49
+ const { [finalPath]: remove, ...rest } = parent;
50
+ return rest;
51
+ }
52
+ const key = path.shift();
53
+ if (typeof key === "string") {
54
+ if (typeof parent === "object") {
55
+ return {
56
+ ...parent,
57
+ [key]: doDelete(parent[key])
58
+ };
59
+ }
60
+ }
33
61
  if (typeof key === "number") {
34
62
  if (Array.isArray(parent)) {
35
63
  const prefix = parent.slice(0, key);
36
64
  return [
37
65
  ...prefix.length ? prefix : new Array(key),
38
- doSet(parent[key]),
66
+ doDelete(parent[key]),
39
67
  ...parent.slice(key + 1)
40
68
  ];
41
69
  }
42
- return [...new Array(key), doSet()];
43
70
  }
44
- throw new Error("Uh oh!");
71
+ throw new Error("It seems we have created an infinite loop in deleteBy. ");
45
72
  }
46
- return doSet(obj);
73
+ return doDelete(obj);
47
74
  }
48
75
  var reFindNumbers0 = /^(\d*)$/gm;
49
76
  var reFindNumbers1 = /\.(\d*)\./gm;
@@ -67,6 +94,7 @@ function isNonEmptyArray(obj) {
67
94
  return !(Array.isArray(obj) && obj.length === 0);
68
95
  }
69
96
  export {
97
+ deleteBy,
70
98
  functionalUpdate,
71
99
  getBy,
72
100
  isNonEmptyArray,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils.ts"],"sourcesContent":["export type UpdaterFn<TInput, TOutput = TInput> = (input: TInput) => TOutput\n\nexport type Updater<TInput, TOutput = TInput> =\n | TOutput\n | UpdaterFn<TInput, TOutput>\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 */\nexport function getBy(obj: any, path: any) {\n const pathArray = makePathArray(path)\n const pathObj = pathArray\n return pathObj.reduce((current: any, pathPart: any) => {\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 */\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 (typeof key === 'string') {\n if (typeof parent === 'object') {\n return {\n ...parent,\n [key]: doSet(parent[key]),\n }\n }\n return {\n [key]: doSet(),\n }\n }\n\n if (typeof key === 'number') {\n if (Array.isArray(parent)) {\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 throw new Error('Uh oh!')\n }\n\n return doSet(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\nfunction makePathArray(str: string) {\n if (typeof str !== 'string') {\n throw new Error('Path must be a string.')\n }\n\n return str\n .replace('[', '.')\n .replace(']', '')\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\nexport function isNonEmptyArray(obj: any) {\n return !(Array.isArray(obj) && obj.length === 0)\n}\n\nexport type RequiredByKey<T, K extends keyof T> = Omit<T, K> &\n Required<Pick<T, K>>\n\ntype ComputeRange<\n N extends number,\n Result extends Array<unknown> = [],\n> = Result['length'] extends N\n ? Result\n : ComputeRange<N, [...Result, Result['length']]>\ntype Index40 = ComputeRange<40>[number]\n\n// Is this type a tuple?\ntype IsTuple<T> = T extends readonly any[] & { length: infer Length }\n ? Length extends Index40\n ? T\n : never\n : never\n\n// If this type is a tuple, what indices are allowed?\ntype AllowedIndexes<\n Tuple extends ReadonlyArray<any>,\n Keys extends number = never,\n> = Tuple extends readonly []\n ? Keys\n : Tuple extends readonly [infer _, ...infer Tail]\n ? AllowedIndexes<Tail, Keys | Tail['length']>\n : Keys\n\nexport type DeepKeys<T, TDepth extends any[] = []> = TDepth['length'] extends 5\n ? never\n : unknown extends T\n ? string\n : object extends T\n ? string\n : T extends readonly any[] & IsTuple<T>\n ? AllowedIndexes<T> | DeepKeysPrefix<T, AllowedIndexes<T>, TDepth>\n : T extends any[]\n ? DeepKeys<T[number], [...TDepth, any]>\n : T extends Date\n ? never\n : T extends object\n ? (keyof T & string) | DeepKeysPrefix<T, keyof T, TDepth>\n : never\n\ntype DeepKeysPrefix<\n T,\n TPrefix,\n TDepth extends any[],\n> = TPrefix extends keyof T & (number | string)\n ? `${TPrefix}.${DeepKeys<T[TPrefix], [...TDepth, any]> & string}`\n : never\n\nexport type DeepValue<T, TProp> = T extends Record<string | number, any>\n ? TProp extends `${infer TBranch}.${infer TDeepProp}`\n ? DeepValue<T[TBranch], TDeepProp>\n : T[TProp & string]\n : never\n\ntype Narrowable = string | number | bigint | boolean\n\ntype NarrowRaw<A> =\n | (A extends [] ? [] : never)\n | (A extends Narrowable ? A : never)\n | {\n [K in keyof A]: A[K] extends Function ? A[K] : NarrowRaw<A[K]>\n }\n\nexport type Narrow<A> = Try<A, [], NarrowRaw<A>>\n\ntype Try<A1, A2, Catch = never> = A1 extends A2 ? A1 : Catch\n\n// Hack to get TypeScript to show simplified types in error messages\nexport type Pretty<T> = { [K in keyof T]: T[K] } & {}\n"],"mappings":";AAMO,SAAS,iBACd,SACA,OACS;AACT,SAAO,OAAO,YAAY,aACrB,QAAuC,KAAK,IAC7C;AACN;AAKO,SAAS,MAAM,KAAU,MAAW;AACzC,QAAM,YAAY,cAAc,IAAI;AACpC,QAAM,UAAU;AAChB,SAAO,QAAQ,OAAO,CAAC,SAAc,aAAkB;AACrD,QAAI,OAAO,YAAY,aAAa;AAClC,aAAO,QAAQ,QAAQ;AAAA,IACzB;AACA,WAAO;AAAA,EACT,GAAG,GAAG;AACR;AAKO,SAAS,MAAM,KAAU,OAAY,SAAuB;AACjE,QAAM,OAAO,cAAc,KAAK;AAEhC,WAAS,MAAM,QAAmB;AAChC,QAAI,CAAC,KAAK,QAAQ;AAChB,aAAO,iBAAiB,SAAS,MAAM;AAAA,IACzC;AAEA,UAAM,MAAM,KAAK,MAAM;AAEvB,QAAI,OAAO,QAAQ,UAAU;AAC3B,UAAI,OAAO,WAAW,UAAU;AAC9B,eAAO;AAAA,UACL,GAAG;AAAA,UACH,CAAC,GAAG,GAAG,MAAM,OAAO,GAAG,CAAC;AAAA,QAC1B;AAAA,MACF;AACA,aAAO;AAAA,QACL,CAAC,GAAG,GAAG,MAAM;AAAA,MACf;AAAA,IACF;AAEA,QAAI,OAAO,QAAQ,UAAU;AAC3B,UAAI,MAAM,QAAQ,MAAM,GAAG;AACzB,cAAM,SAAS,OAAO,MAAM,GAAG,GAAG;AAClC,eAAO;AAAA,UACL,GAAI,OAAO,SAAS,SAAS,IAAI,MAAM,GAAG;AAAA,UAC1C,MAAM,OAAO,GAAG,CAAC;AAAA,UACjB,GAAG,OAAO,MAAM,MAAM,CAAC;AAAA,QACzB;AAAA,MACF;AACA,aAAO,CAAC,GAAG,IAAI,MAAM,GAAG,GAAG,MAAM,CAAC;AAAA,IACpC;AAEA,UAAM,IAAI,MAAM,QAAQ;AAAA,EAC1B;AAEA,SAAO,MAAM,GAAG;AAClB;AAEA,IAAM,iBAAiB;AACvB,IAAM,iBAAiB;AACvB,IAAM,iBAAiB;AACvB,IAAM,iBAAiB;AACvB,IAAM,wBAAwB;AAE9B,IAAM,YAAY;AAClB,IAAM,aAAa,GAAG,SAAS;AAE/B,SAAS,cAAc,KAAa;AAClC,MAAI,OAAO,QAAQ,UAAU;AAC3B,UAAM,IAAI,MAAM,wBAAwB;AAAA,EAC1C;AAEA,SAAO,IACJ,QAAQ,KAAK,GAAG,EAChB,QAAQ,KAAK,EAAE,EACf,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,IACnD;AACA,WAAO;AAAA,EACT,CAAC;AACL;AAEO,SAAS,gBAAgB,KAAU;AACxC,SAAO,EAAE,MAAM,QAAQ,GAAG,KAAK,IAAI,WAAW;AAChD;","names":[]}
1
+ {"version":3,"sources":["../../src/utils.ts"],"sourcesContent":["export type UpdaterFn<TInput, TOutput = TInput> = (input: TInput) => TOutput\n\nexport type Updater<TInput, TOutput = TInput> =\n | TOutput\n | UpdaterFn<TInput, TOutput>\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 */\nexport function getBy(obj: any, path: any) {\n const pathObj = makePathArray(path)\n return pathObj.reduce((current: any, pathPart: any) => {\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 */\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 (typeof key === 'string') {\n if (typeof parent === 'object') {\n return {\n ...parent,\n [key]: doSet(parent[key]),\n }\n }\n return {\n [key]: doSet(),\n }\n }\n\n if (Array.isArray(parent) && key !== undefined) {\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 */\nexport function deleteBy(obj: any, _path: any) {\n const path = makePathArray(_path)\n\n function doDelete(parent: any): any {\n if (path.length === 1) {\n const finalPath = path[0]!\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 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\nfunction makePathArray(str: string) {\n if (typeof str !== 'string') {\n throw new Error('Path must be a string.')\n }\n\n return str\n .replace('[', '.')\n .replace(']', '')\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\nexport function isNonEmptyArray(obj: any) {\n return !(Array.isArray(obj) && obj.length === 0)\n}\n\nexport type RequiredByKey<T, K extends keyof T> = Omit<T, K> &\n Required<Pick<T, K>>\n\ntype ComputeRange<\n N extends number,\n Result extends Array<unknown> = [],\n> = Result['length'] extends N\n ? Result\n : ComputeRange<N, [...Result, Result['length']]>\ntype Index40 = ComputeRange<40>[number]\n\n// Is this type a tuple?\ntype IsTuple<T> = T extends readonly any[] & { length: infer Length }\n ? Length extends Index40\n ? T\n : never\n : never\n\n// If this type is a tuple, what indices are allowed?\ntype AllowedIndexes<\n Tuple extends ReadonlyArray<any>,\n Keys extends number = never,\n> = Tuple extends readonly []\n ? Keys\n : Tuple extends readonly [infer _, ...infer Tail]\n ? AllowedIndexes<Tail, Keys | Tail['length']>\n : Keys\n\nexport type DeepKeys<T, TDepth extends any[] = []> = TDepth['length'] extends 5\n ? never\n : unknown extends T\n ? string\n : object extends T\n ? string\n : T extends readonly any[] & IsTuple<T>\n ? AllowedIndexes<T> | DeepKeysPrefix<T, AllowedIndexes<T>, TDepth>\n : T extends any[]\n ? DeepKeys<T[number], [...TDepth, any]>\n : T extends Date\n ? never\n : T extends object\n ? (keyof T & string) | DeepKeysPrefix<T, keyof T, TDepth>\n : never\n\ntype DeepKeysPrefix<\n T,\n TPrefix,\n TDepth extends any[],\n> = TPrefix extends keyof T & (number | string)\n ? `${TPrefix}.${DeepKeys<T[TPrefix], [...TDepth, any]> & string}`\n : never\n\nexport type DeepValue<T, TProp> = T extends Record<string | number, any>\n ? TProp extends `${infer TBranch}.${infer TDeepProp}`\n ? DeepValue<T[TBranch], TDeepProp>\n : T[TProp & string]\n : never\n\ntype Narrowable = string | number | bigint | boolean\n\ntype NarrowRaw<A> =\n | (A extends [] ? [] : never)\n | (A extends Narrowable ? A : never)\n | {\n [K in keyof A]: A[K] extends Function ? A[K] : NarrowRaw<A[K]>\n }\n\nexport type Narrow<A> = Try<A, [], NarrowRaw<A>>\n\ntype Try<A1, A2, Catch = never> = A1 extends A2 ? A1 : Catch\n\n// Hack to get TypeScript to show simplified types in error messages\nexport type Pretty<T> = { [K in keyof T]: T[K] } & {}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/form-core",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "Powerful, type-safe, framework agnostic forms.",
5
5
  "author": "tannerlinsley",
6
6
  "license": "MIT",
package/src/FieldApi.ts CHANGED
@@ -98,6 +98,13 @@ export interface FieldOptions<
98
98
  TData
99
99
  >
100
100
  onBlurAsyncDebounceMs?: number
101
+ onSubmit?: ValidateOrFn<
102
+ TParentData,
103
+ TName,
104
+ ValidatorType,
105
+ FormValidator,
106
+ TData
107
+ >
101
108
  onSubmitAsync?: AsyncValidateOrFn<
102
109
  TParentData,
103
110
  TName,
@@ -330,18 +337,26 @@ export class FieldApi<
330
337
  }) as any
331
338
 
332
339
  validateSync = (value = this.state.value, cause: ValidationCause) => {
333
- const { onChange, onBlur } = this.options
334
- const validate =
335
- cause === 'submit' ? undefined : cause === 'change' ? onChange : onBlur
340
+ const { onChange, onBlur, onSubmit } = this.options
336
341
 
337
- if (!validate) return
342
+ const validates =
343
+ // https://github.com/TanStack/form/issues/490
344
+ cause === 'submit'
345
+ ? ([
346
+ { cause: 'change', validate: onChange },
347
+ { cause: 'blur', validate: onBlur },
348
+ { cause: 'submit', validate: onSubmit },
349
+ ] as const)
350
+ : cause === 'change'
351
+ ? ([{ cause: 'change', validate: onChange }] as const)
352
+ : ([{ cause: 'blur', validate: onBlur }] as const)
338
353
 
339
354
  // Use the validationCount for all field instances to
340
355
  // track freshness of the validation
341
356
  const validationCount = (this.getInfo().validationCount || 0) + 1
342
357
  this.getInfo().validationCount = validationCount
343
358
 
344
- const doValidate = () => {
359
+ const doValidate = (validate: (typeof validates)[number]['validate']) => {
345
360
  if (this.options.validator && typeof validate !== 'function') {
346
361
  return (this.options.validator as Validator<TData>)().validate(
347
362
  value,
@@ -362,20 +377,48 @@ export class FieldApi<
362
377
  )
363
378
  }
364
379
 
365
- const error = normalizeError(doValidate())
366
- const errorMapKey = getErrorMapKey(cause)
367
- if (this.state.meta.errorMap[errorMapKey] !== error) {
380
+ // Needs type cast as eslint errantly believes this is always falsy
381
+ let hasError = false as boolean
382
+
383
+ this.form.store.batch(() => {
384
+ for (const validateObj of validates) {
385
+ if (!validateObj.validate) continue
386
+ const error = normalizeError(doValidate(validateObj.validate))
387
+ const errorMapKey = getErrorMapKey(validateObj.cause)
388
+ if (this.state.meta.errorMap[errorMapKey] !== error) {
389
+ this.setMeta((prev) => ({
390
+ ...prev,
391
+ errorMap: {
392
+ ...prev.errorMap,
393
+ [getErrorMapKey(validateObj.cause)]: error,
394
+ },
395
+ }))
396
+ hasError = true
397
+ }
398
+ }
399
+ })
400
+
401
+ /**
402
+ * when we have an error for onSubmit in the state, we want
403
+ * to clear the error as soon as the user enters a valid value in the field
404
+ */
405
+ const submitErrKey = getErrorMapKey('submit')
406
+ if (
407
+ this.state.meta.errorMap[submitErrKey] &&
408
+ cause !== 'submit' &&
409
+ !hasError
410
+ ) {
368
411
  this.setMeta((prev) => ({
369
412
  ...prev,
370
413
  errorMap: {
371
414
  ...prev.errorMap,
372
- [getErrorMapKey(cause)]: error,
415
+ [submitErrKey]: undefined,
373
416
  },
374
417
  }))
375
418
  }
376
419
 
377
420
  // If a sync error is encountered for the errorMapKey (eg. onChange), cancel any async validation
378
- if (this.state.meta.errorMap[errorMapKey]) {
421
+ if (hasError) {
379
422
  this.cancelValidateAsync()
380
423
  }
381
424
  }
package/src/FormApi.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import { Store } from '@tanstack/store'
2
2
  import type { DeepKeys, DeepValue, Updater } from './utils'
3
- import { functionalUpdate, getBy, isNonEmptyArray, setBy } from './utils'
3
+ import {
4
+ deleteBy,
5
+ functionalUpdate,
6
+ getBy,
7
+ isNonEmptyArray,
8
+ setBy,
9
+ } from './utils'
4
10
  import type { FieldApi, FieldMeta, ValidationCause } from './FieldApi'
5
11
  import type { ValidationError, Validator } from './types'
6
12
 
@@ -553,8 +559,9 @@ export class FormApi<TFormData, ValidatorType> {
553
559
  deleteField = <TField extends DeepKeys<TFormData>>(field: TField) => {
554
560
  this.store.setState((prev) => {
555
561
  const newState = { ...prev }
556
- delete newState.values[field as keyof TFormData]
562
+ newState.values = deleteBy(newState.values, field)
557
563
  delete newState.fieldMeta[field]
564
+
558
565
  return newState
559
566
  })
560
567
  }
@@ -614,4 +614,23 @@ describe('field api', () => {
614
614
  expect(form.store.state.values.name).toBeUndefined()
615
615
  expect(form.store.state.fieldMeta.name).toBeUndefined()
616
616
  })
617
+
618
+ it('should show onSubmit errors', async () => {
619
+ const form = new FormApi({
620
+ defaultValues: {
621
+ firstName: '',
622
+ },
623
+ })
624
+
625
+ const field = new FieldApi({
626
+ form,
627
+ name: 'firstName',
628
+ onSubmit: (v) => (v.length > 0 ? undefined : 'first name is required'),
629
+ })
630
+
631
+ field.mount()
632
+
633
+ await form.handleSubmit()
634
+ expect(field.getMeta().errors).toStrictEqual(['first name is required'])
635
+ })
617
636
  })
@@ -226,6 +226,63 @@ describe('form api', () => {
226
226
  expect(form.getFieldValue('names')).toStrictEqual(['one', 'three', 'two'])
227
227
  })
228
228
 
229
+ it('should handle fields inside an array', async () => {
230
+ interface Employee {
231
+ firstName: string
232
+ }
233
+ interface Form {
234
+ employees: Partial<Employee>[]
235
+ }
236
+
237
+ const form = new FormApi<Form, unknown>()
238
+
239
+ const field = new FieldApi({
240
+ form,
241
+ name: 'employees',
242
+ defaultValue: [],
243
+ })
244
+
245
+ field.mount()
246
+
247
+ const fieldInArray = new FieldApi({
248
+ form,
249
+ name: `employees.${0}.firstName`,
250
+ defaultValue: 'Darcy',
251
+ })
252
+ fieldInArray.mount()
253
+ expect(field.state.value.length).toBe(1)
254
+ expect(fieldInArray.getValue()).toBe('Darcy')
255
+ })
256
+
257
+ it('should handle deleting fields in an array', async () => {
258
+ interface Employee {
259
+ firstName: string
260
+ }
261
+ interface Form {
262
+ employees: Partial<Employee>[]
263
+ }
264
+
265
+ const form = new FormApi<Form, unknown>()
266
+
267
+ const field = new FieldApi({
268
+ form,
269
+ name: 'employees',
270
+ defaultValue: [],
271
+ })
272
+
273
+ field.mount()
274
+
275
+ const fieldInArray = new FieldApi({
276
+ form,
277
+ name: `employees.${0}.firstName`,
278
+ defaultValue: 'Darcy',
279
+ })
280
+ fieldInArray.mount()
281
+ form.deleteField(`employees.${0}.firstName`)
282
+ expect(field.state.value.length).toBe(1)
283
+ expect(Object.keys(field.state.value[0]!).length).toBe(0)
284
+ })
285
+
229
286
  it('should not wipe values when updating', () => {
230
287
  const form = new FormApi({
231
288
  defaultValues: {
@@ -500,7 +557,6 @@ describe('form api', () => {
500
557
 
501
558
  form.mount()
502
559
  field.mount()
503
-
504
560
  expect(form.state.errors.length).toBe(0)
505
561
  field.setValue('other', { touch: true })
506
562
  field.validate('blur')
@@ -668,4 +724,96 @@ describe('form api', () => {
668
724
  onMount: 'Please enter a different value',
669
725
  })
670
726
  })
727
+
728
+ it('should validate fields during submit', async () => {
729
+ const form = new FormApi({
730
+ defaultValues: {
731
+ firstName: '',
732
+ lastName: '',
733
+ },
734
+ })
735
+
736
+ const field = new FieldApi({
737
+ form,
738
+ name: 'firstName',
739
+ onChange: (v) => (v.length > 0 ? undefined : 'first name is required'),
740
+ })
741
+
742
+ const lastNameField = new FieldApi({
743
+ form,
744
+ name: 'lastName',
745
+ onChange: (v) => (v.length > 0 ? undefined : 'last name is required'),
746
+ })
747
+
748
+ field.mount()
749
+ lastNameField.mount()
750
+
751
+ await form.handleSubmit()
752
+ expect(form.state.isFieldsValid).toEqual(false)
753
+ expect(form.state.canSubmit).toEqual(false)
754
+ expect(form.state.fieldMeta['firstName'].errors).toEqual([
755
+ 'first name is required',
756
+ ])
757
+ expect(form.state.fieldMeta['lastName'].errors).toEqual([
758
+ 'last name is required',
759
+ ])
760
+ })
761
+
762
+ it('should run all types of validation on fields during submit', async () => {
763
+ const form = new FormApi({
764
+ defaultValues: {
765
+ firstName: '',
766
+ lastName: '',
767
+ },
768
+ })
769
+
770
+ const field = new FieldApi({
771
+ form,
772
+ name: 'firstName',
773
+ onChange: (v) => (v.length > 0 ? undefined : 'first name is required'),
774
+ onBlur: (v) =>
775
+ v.length > 3
776
+ ? undefined
777
+ : 'first name must be longer than 3 characters',
778
+ })
779
+
780
+ field.mount()
781
+
782
+ await form.handleSubmit()
783
+ expect(form.state.isFieldsValid).toEqual(false)
784
+ expect(form.state.canSubmit).toEqual(false)
785
+ expect(form.state.fieldMeta['firstName'].errors).toEqual([
786
+ 'first name is required',
787
+ 'first name must be longer than 3 characters',
788
+ ])
789
+ })
790
+
791
+ it('should clear onSubmit error when a valid value is entered', async () => {
792
+ const form = new FormApi({
793
+ defaultValues: {
794
+ firstName: '',
795
+ },
796
+ })
797
+
798
+ const field = new FieldApi({
799
+ form,
800
+ name: 'firstName',
801
+ onSubmit: (v) => (v.length > 0 ? undefined : 'first name is required'),
802
+ })
803
+
804
+ field.mount()
805
+
806
+ await form.handleSubmit()
807
+ expect(form.state.isFieldsValid).toEqual(false)
808
+ expect(form.state.canSubmit).toEqual(false)
809
+ expect(form.state.fieldMeta['firstName'].errorMap['onSubmit']).toEqual(
810
+ 'first name is required',
811
+ )
812
+ field.handleChange('test')
813
+ expect(form.state.isFieldsValid).toEqual(true)
814
+ expect(form.state.canSubmit).toEqual(true)
815
+ expect(
816
+ form.state.fieldMeta['firstName'].errorMap['onSubmit'],
817
+ ).toBeUndefined()
818
+ })
671
819
  })
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { deleteBy, getBy, setBy } from '../utils'
3
+
4
+ describe('getBy', () => {
5
+ const structure = {
6
+ name: 'Marc',
7
+ kids: [
8
+ { name: 'Stephen', age: 10 },
9
+ { name: 'Taylor', age: 15 },
10
+ ],
11
+ mother: {
12
+ name: 'Lisa',
13
+ },
14
+ }
15
+
16
+ it('should get subfields by path', () => {
17
+ expect(getBy(structure, 'name')).toBe(structure.name)
18
+ expect(getBy(structure, 'mother.name')).toBe(structure.mother.name)
19
+ })
20
+
21
+ it('should get array subfields by path', () => {
22
+ expect(getBy(structure, 'kids.0.name')).toBe(structure.kids[0]!.name)
23
+ expect(getBy(structure, 'kids.0.age')).toBe(structure.kids[0]!.age)
24
+ })
25
+ })
26
+
27
+ describe('setBy', () => {
28
+ const structure = {
29
+ name: 'Marc',
30
+ kids: [
31
+ { name: 'Stephen', age: 10 },
32
+ { name: 'Taylor', age: 15 },
33
+ ],
34
+ mother: {
35
+ name: 'Lisa',
36
+ },
37
+ }
38
+
39
+ it('should set subfields by path', () => {
40
+ expect(setBy(structure, 'name', 'Lisa').name).toBe('Lisa')
41
+ expect(setBy(structure, 'mother.name', 'Tina').mother.name).toBe('Tina')
42
+ })
43
+
44
+ it('should set array subfields by path', () => {
45
+ expect(setBy(structure, 'kids.0.name', 'Taylor').kids[0].name).toBe(
46
+ 'Taylor',
47
+ )
48
+ expect(setBy(structure, 'kids.0.age', 20).kids[0].age).toBe(20)
49
+ })
50
+ })
51
+
52
+ describe('deleteBy', () => {
53
+ const structure = {
54
+ name: 'Marc',
55
+ kids: [
56
+ { name: 'Stephen', age: 10 },
57
+ { name: 'Taylor', age: 15 },
58
+ ],
59
+ mother: {
60
+ name: 'Lisa',
61
+ },
62
+ }
63
+
64
+ it('should delete subfields by path', () => {
65
+ expect(deleteBy(structure, 'name').name).not.toBeDefined()
66
+ expect(deleteBy(structure, 'mother.name').mother.name).not.toBeDefined()
67
+ })
68
+
69
+ it('should delete array subfields by path', () => {
70
+ expect(deleteBy(structure, 'kids.0.name').kids[0].name).not.toBeDefined()
71
+ expect(deleteBy(structure, 'kids.0.age').kids[0].age).not.toBeDefined()
72
+ })
73
+ })
package/src/utils.ts CHANGED
@@ -17,8 +17,7 @@ export function functionalUpdate<TInput, TOutput = TInput>(
17
17
  * Get a value from an object using a path, including dot notation.
18
18
  */
19
19
  export function getBy(obj: any, path: any) {
20
- const pathArray = makePathArray(path)
21
- const pathObj = pathArray
20
+ const pathObj = makePathArray(path)
22
21
  return pathObj.reduce((current: any, pathPart: any) => {
23
22
  if (typeof current !== 'undefined') {
24
23
  return current[pathPart]
@@ -52,22 +51,59 @@ export function setBy(obj: any, _path: any, updater: Updater<any>) {
52
51
  }
53
52
  }
54
53
 
54
+ if (Array.isArray(parent) && key !== undefined) {
55
+ const prefix = parent.slice(0, key)
56
+ return [
57
+ ...(prefix.length ? prefix : new Array(key)),
58
+ doSet(parent[key]),
59
+ ...parent.slice(key + 1),
60
+ ]
61
+ }
62
+ return [...new Array(key), doSet()]
63
+ }
64
+
65
+ return doSet(obj)
66
+ }
67
+
68
+ /**
69
+ * Delete a field on an object using a path, including dot notation.
70
+ */
71
+ export function deleteBy(obj: any, _path: any) {
72
+ const path = makePathArray(_path)
73
+
74
+ function doDelete(parent: any): any {
75
+ if (path.length === 1) {
76
+ const finalPath = path[0]!
77
+ const { [finalPath]: remove, ...rest } = parent
78
+ return rest
79
+ }
80
+
81
+ const key = path.shift()
82
+
83
+ if (typeof key === 'string') {
84
+ if (typeof parent === 'object') {
85
+ return {
86
+ ...parent,
87
+ [key]: doDelete(parent[key]),
88
+ }
89
+ }
90
+ }
91
+
55
92
  if (typeof key === 'number') {
56
93
  if (Array.isArray(parent)) {
57
94
  const prefix = parent.slice(0, key)
58
95
  return [
59
96
  ...(prefix.length ? prefix : new Array(key)),
60
- doSet(parent[key]),
97
+ doDelete(parent[key]),
61
98
  ...parent.slice(key + 1),
62
99
  ]
63
100
  }
64
- return [...new Array(key), doSet()]
65
101
  }
66
102
 
67
- throw new Error('Uh oh!')
103
+ throw new Error('It seems we have created an infinite loop in deleteBy. ')
68
104
  }
69
105
 
70
- return doSet(obj)
106
+ return doDelete(obj)
71
107
  }
72
108
 
73
109
  const reFindNumbers0 = /^(\d*)$/gm