@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.
- package/build/legacy/FieldApi.cjs +33 -10
- package/build/legacy/FieldApi.cjs.map +1 -1
- package/build/legacy/FieldApi.js +33 -10
- package/build/legacy/FieldApi.js.map +1 -1
- package/build/legacy/FormApi.cjs +1 -1
- package/build/legacy/FormApi.cjs.map +1 -1
- package/build/legacy/FormApi.js +8 -2
- package/build/legacy/FormApi.js.map +1 -1
- package/build/legacy/index.d.cts +2 -1
- package/build/legacy/index.d.ts +2 -1
- package/build/legacy/utils.cjs +35 -6
- package/build/legacy/utils.cjs.map +1 -1
- package/build/legacy/utils.d.cts +5 -1
- package/build/legacy/utils.d.ts +5 -1
- package/build/legacy/utils.js +34 -6
- package/build/legacy/utils.js.map +1 -1
- package/build/modern/FieldApi.cjs +33 -10
- package/build/modern/FieldApi.cjs.map +1 -1
- package/build/modern/FieldApi.js +33 -10
- package/build/modern/FieldApi.js.map +1 -1
- package/build/modern/FormApi.cjs +1 -1
- package/build/modern/FormApi.cjs.map +1 -1
- package/build/modern/FormApi.js +8 -2
- package/build/modern/FormApi.js.map +1 -1
- package/build/modern/index.d.cts +2 -1
- package/build/modern/index.d.ts +2 -1
- package/build/modern/utils.cjs +35 -6
- package/build/modern/utils.cjs.map +1 -1
- package/build/modern/utils.d.cts +5 -1
- package/build/modern/utils.d.ts +5 -1
- package/build/modern/utils.js +34 -6
- package/build/modern/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/FieldApi.ts +53 -10
- package/src/FormApi.ts +9 -2
- package/src/tests/FieldApi.spec.ts +19 -0
- package/src/tests/FormApi.spec.ts +149 -1
- package/src/tests/utils.spec.ts +73 -0
- 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
|
|
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/build/modern/utils.d.cts
CHANGED
|
@@ -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 };
|
package/build/modern/utils.d.ts
CHANGED
|
@@ -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 };
|
package/build/modern/utils.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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("
|
|
71
|
+
throw new Error("It seems we have created an infinite loop in deleteBy. ");
|
|
45
72
|
}
|
|
46
|
-
return
|
|
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
|
|
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
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
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
[
|
|
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 (
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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('
|
|
103
|
+
throw new Error('It seems we have created an infinite loop in deleteBy. ')
|
|
68
104
|
}
|
|
69
105
|
|
|
70
|
-
return
|
|
106
|
+
return doDelete(obj)
|
|
71
107
|
}
|
|
72
108
|
|
|
73
109
|
const reFindNumbers0 = /^(\d*)$/gm
|