@tanstack/form-core 0.0.9 → 0.0.12

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 (79) hide show
  1. package/build/lib/FieldApi.cjs +307 -0
  2. package/build/lib/FieldApi.cjs.map +1 -0
  3. package/build/lib/FieldApi.d.ts +112 -0
  4. package/build/lib/FieldApi.d.ts.map +1 -0
  5. package/build/lib/FieldApi.js +305 -0
  6. package/build/lib/FieldApi.js.map +1 -0
  7. package/build/lib/FieldApi.legacy.cjs +307 -0
  8. package/build/lib/FieldApi.legacy.cjs.map +1 -0
  9. package/build/lib/FieldApi.legacy.js +305 -0
  10. package/build/lib/FieldApi.legacy.js.map +1 -0
  11. package/build/lib/FormApi.cjs +248 -0
  12. package/build/lib/FormApi.cjs.map +1 -0
  13. package/build/{types → lib}/FormApi.d.ts +16 -11
  14. package/build/lib/FormApi.d.ts.map +1 -0
  15. package/build/lib/FormApi.js +246 -0
  16. package/build/lib/FormApi.js.map +1 -0
  17. package/build/lib/FormApi.legacy.cjs +248 -0
  18. package/build/lib/FormApi.legacy.cjs.map +1 -0
  19. package/build/lib/FormApi.legacy.js +246 -0
  20. package/build/lib/FormApi.legacy.js.map +1 -0
  21. package/build/lib/_virtual/_rollupPluginBabelHelpers.cjs +65 -0
  22. package/build/lib/_virtual/_rollupPluginBabelHelpers.cjs.map +1 -0
  23. package/build/lib/_virtual/_rollupPluginBabelHelpers.js +56 -0
  24. package/build/{cjs → lib}/_virtual/_rollupPluginBabelHelpers.js.map +1 -1
  25. package/build/lib/_virtual/_rollupPluginBabelHelpers.legacy.cjs +65 -0
  26. package/build/lib/_virtual/_rollupPluginBabelHelpers.legacy.cjs.map +1 -0
  27. package/build/lib/_virtual/_rollupPluginBabelHelpers.legacy.js +56 -0
  28. package/build/lib/_virtual/_rollupPluginBabelHelpers.legacy.js.map +1 -0
  29. package/build/lib/index.cjs +14 -0
  30. package/build/lib/index.cjs.map +1 -0
  31. package/build/{types → lib}/index.d.ts +1 -0
  32. package/build/lib/index.d.ts.map +1 -0
  33. package/build/lib/index.js +4 -0
  34. package/build/{cjs → lib}/index.js.map +1 -1
  35. package/build/lib/index.legacy.cjs +14 -0
  36. package/build/lib/index.legacy.cjs.map +1 -0
  37. package/build/lib/index.legacy.js +4 -0
  38. package/build/lib/index.legacy.js.map +1 -0
  39. package/build/lib/tests/FieldApi.spec.d.ts +2 -0
  40. package/build/lib/tests/FieldApi.spec.d.ts.map +1 -0
  41. package/build/lib/tests/FieldApi.test-d.d.ts +2 -0
  42. package/build/lib/tests/FieldApi.test-d.d.ts.map +1 -0
  43. package/build/lib/tests/FormApi.spec.d.ts +2 -0
  44. package/build/lib/tests/FormApi.spec.d.ts.map +1 -0
  45. package/build/{cjs/utils.js → lib/utils.cjs} +18 -27
  46. package/build/lib/utils.cjs.map +1 -0
  47. package/build/{types → lib}/utils.d.ts +10 -0
  48. package/build/lib/utils.d.ts.map +1 -0
  49. package/build/lib/utils.js +77 -0
  50. package/build/lib/utils.js.map +1 -0
  51. package/build/lib/utils.legacy.cjs +81 -0
  52. package/build/lib/utils.legacy.cjs.map +1 -0
  53. package/build/lib/utils.legacy.js +77 -0
  54. package/build/lib/utils.legacy.js.map +1 -0
  55. package/package.json +23 -10
  56. package/src/FieldApi.ts +190 -138
  57. package/src/FormApi.ts +84 -118
  58. package/src/tests/FieldApi.spec.ts +143 -0
  59. package/src/tests/FieldApi.test-d.ts +41 -0
  60. package/src/tests/FormApi.spec.ts +216 -0
  61. package/src/utils.ts +10 -1
  62. package/build/cjs/FieldApi.js +0 -345
  63. package/build/cjs/FieldApi.js.map +0 -1
  64. package/build/cjs/FormApi.js +0 -317
  65. package/build/cjs/FormApi.js.map +0 -1
  66. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js +0 -31
  67. package/build/cjs/index.js +0 -26
  68. package/build/cjs/utils.js.map +0 -1
  69. package/build/esm/index.js +0 -724
  70. package/build/esm/index.js.map +0 -1
  71. package/build/stats-html.html +0 -2689
  72. package/build/stats-react.json +0 -190
  73. package/build/types/FieldApi.d.ts +0 -79
  74. package/build/types/tests/test.test.d.ts +0 -0
  75. package/build/umd/index.development.js +0 -785
  76. package/build/umd/index.development.js.map +0 -1
  77. package/build/umd/index.production.js +0 -22
  78. package/build/umd/index.production.js.map +0 -1
  79. package/src/tests/test.test.tsx +0 -5
@@ -0,0 +1,77 @@
1
+ function functionalUpdate(updater, input) {
2
+ return typeof updater === 'function' ? updater(input) : updater;
3
+ }
4
+
5
+ /**
6
+ * Get a value from an object using a path, including dot notation.
7
+ */
8
+ function getBy(obj, path) {
9
+ const pathArray = makePathArray(path);
10
+ const pathObj = pathArray;
11
+ return pathObj.reduce((current, pathPart) => {
12
+ if (typeof current !== 'undefined') {
13
+ return current[pathPart];
14
+ }
15
+ return undefined;
16
+ }, obj);
17
+ }
18
+
19
+ /**
20
+ * Set a value on an object using a path, including dot notation.
21
+ */
22
+ function setBy(obj, _path, updater) {
23
+ const path = makePathArray(_path);
24
+ function doSet(parent) {
25
+ if (!path.length) {
26
+ return functionalUpdate(updater, parent);
27
+ }
28
+ const key = path.shift();
29
+ if (typeof key === 'string') {
30
+ if (typeof parent === 'object') {
31
+ return {
32
+ ...parent,
33
+ [key]: doSet(parent[key])
34
+ };
35
+ }
36
+ return {
37
+ [key]: doSet()
38
+ };
39
+ }
40
+ if (typeof key === 'number') {
41
+ if (Array.isArray(parent)) {
42
+ const prefix = parent.slice(0, key);
43
+ return [...(prefix.length ? prefix : new Array(key)), doSet(parent[key]), ...parent.slice(key + 1)];
44
+ }
45
+ return [...new Array(key), doSet()];
46
+ }
47
+ throw new Error('Uh oh!');
48
+ }
49
+ return doSet(obj);
50
+ }
51
+ const reFindNumbers0 = /^(\d*)$/gm;
52
+ const reFindNumbers1 = /\.(\d*)\./gm;
53
+ const reFindNumbers2 = /^(\d*)\./gm;
54
+ const reFindNumbers3 = /\.(\d*$)/gm;
55
+ const reFindMultiplePeriods = /\.{2,}/gm;
56
+ const intPrefix = '__int__';
57
+ const intReplace = intPrefix + "$1";
58
+ function makePathArray(str) {
59
+ if (typeof str !== 'string') {
60
+ throw new Error('Path must be a string.');
61
+ }
62
+ return str.replace('[', '.').replace(']', '').replace(reFindNumbers0, intReplace).replace(reFindNumbers1, "." + intReplace + ".").replace(reFindNumbers2, intReplace + ".").replace(reFindNumbers3, "." + intReplace).replace(reFindMultiplePeriods, '.').split('.').map(d => {
63
+ if (d.indexOf(intPrefix) === 0) {
64
+ return parseInt(d.substring(intPrefix.length), 10);
65
+ }
66
+ return d;
67
+ });
68
+ }
69
+
70
+ // Is this type a tuple?
71
+
72
+ // If this type is a tuple, what indices are allowed?
73
+
74
+ // Hack to get TypeScript to show simplified types in error messages
75
+
76
+ export { functionalUpdate, getBy, setBy };
77
+ //# sourceMappingURL=utils.legacy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.legacy.js","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 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> = unknown extends T\n ? keyof T\n : object extends T\n ? string\n : T extends readonly any[] & IsTuple<T>\n ? AllowedIndexes<T> | DeepKeysPrefix<T, AllowedIndexes<T>>\n : T extends any[]\n ? DeepKeys<T[number]>\n : T extends Date\n ? never\n : T extends object\n ? (keyof T & string) | DeepKeysPrefix<T, keyof T>\n : never\n\ntype DeepKeysPrefix<T, TPrefix> = TPrefix extends keyof T & (number | string)\n ? `${TPrefix}.${DeepKeys<T[TPrefix]> & 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"],"names":["functionalUpdate","updater","input","getBy","obj","path","pathArray","makePathArray","pathObj","reduce","current","pathPart","undefined","setBy","_path","doSet","parent","length","key","shift","Array","isArray","prefix","slice","Error","reFindNumbers0","reFindNumbers1","reFindNumbers2","reFindNumbers3","reFindMultiplePeriods","intPrefix","intReplace","str","replace","split","map","d","indexOf","parseInt","substring"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/form-core",
3
- "version": "0.0.9",
3
+ "version": "0.0.12",
4
4
  "description": "Powerful, type-safe, framework agnostic forms.",
5
5
  "author": "tannerlinsley",
6
6
  "license": "MIT",
@@ -10,23 +10,36 @@
10
10
  "type": "github",
11
11
  "url": "https://github.com/sponsors/tannerlinsley"
12
12
  },
13
- "types": "build/types/index.d.ts",
14
- "main": "build/cjs/index.js",
15
- "module": "build/esm/index.js",
13
+ "type": "module",
14
+ "types": "build/lib/index.d.ts",
15
+ "main": "build/lib/index.legacy.cjs",
16
+ "module": "build/lib/index.legacy.js",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./build/lib/index.d.ts",
20
+ "import": "./build/lib/index.js",
21
+ "require": "./build/lib/index.cjs",
22
+ "default": "./build/lib/index.cjs"
23
+ },
24
+ "./package.json": "./package.json"
25
+ },
16
26
  "sideEffects": false,
17
27
  "files": [
18
- "build/**",
28
+ "build/lib/*",
19
29
  "src"
20
30
  ],
21
31
  "dependencies": {
22
- "@tanstack/store": "0.0.1-beta.84"
32
+ "@tanstack/store": "0.0.1-beta.89"
23
33
  },
24
34
  "scripts": {
25
- "clean": "rimraf ./build",
35
+ "clean": "rimraf ./build && rimraf ./coverage",
26
36
  "test:eslint": "eslint --ext .ts,.tsx ./src",
27
- "test:types": "tsc",
28
- "test:lib": "jest --config ./jest.config.ts",
37
+ "test:types": "tsc --noEmit && vitest typecheck",
38
+ "test:lib": "vitest run --coverage",
29
39
  "test:lib:dev": "pnpm run test:lib --watch",
30
- "build:types": "tsc --build"
40
+ "test:build": "publint --strict",
41
+ "build": "pnpm build:rollup && pnpm build:types",
42
+ "build:rollup": "rollup --config rollup.config.js",
43
+ "build:types": "tsc --emitDeclarationOnly"
31
44
  }
32
45
  }
package/src/FieldApi.ts CHANGED
@@ -1,26 +1,45 @@
1
- //
2
- import type { DeepKeys, DeepValue, RequiredByKey, Updater } from './utils'
1
+ import type { DeepKeys, DeepValue, Updater } from './utils'
3
2
  import type { FormApi, ValidationError } from './FormApi'
4
3
  import { Store } from '@tanstack/store'
5
4
 
6
5
  export type ValidationCause = 'change' | 'blur' | 'submit'
7
6
 
8
- export interface FieldOptions<TData, TFormData> {
9
- name: unknown extends TFormData ? string : DeepKeys<TFormData>
7
+ type ValidateFn<TData, TFormData> = (
8
+ value: TData,
9
+ fieldApi: FieldApi<TData, TFormData>,
10
+ ) => ValidationError
11
+
12
+ type ValidateAsyncFn<TData, TFormData> = (
13
+ value: TData,
14
+ fieldApi: FieldApi<TData, TFormData>,
15
+ ) => ValidationError | Promise<ValidationError>
16
+
17
+ export interface FieldOptions<
18
+ _TData,
19
+ TFormData,
20
+ /**
21
+ * This allows us to restrict the name to only be a valid field name while
22
+ * also assigning it to a generic
23
+ */
24
+ TName = unknown extends TFormData ? string : DeepKeys<TFormData>,
25
+ /**
26
+ * If TData is unknown, we can use the TName generic to determine the type
27
+ */
28
+ TData = unknown extends _TData ? DeepValue<TFormData, TName> : _TData,
29
+ > {
30
+ name: TName
10
31
  index?: TData extends any[] ? number : never
11
32
  defaultValue?: TData
12
- validate?: (
13
- value: TData,
14
- fieldApi: FieldApi<TData, TFormData>,
15
- ) => ValidationError
16
- validateAsync?: (
17
- value: TData,
18
- fieldApi: FieldApi<TData, TFormData>,
19
- ) => ValidationError | Promise<ValidationError>
20
- validatePristine?: boolean // Default: false
21
- validateOn?: ValidationCause // Default: 'change'
22
- validateAsyncOn?: ValidationCause // Default: 'blur'
23
- validateAsyncDebounceMs?: number
33
+ asyncDebounceMs?: number
34
+ asyncAlways?: boolean
35
+ onMount?: (formApi: FieldApi<TData, TFormData>) => void
36
+ onChange?: ValidateFn<TData, TFormData>
37
+ onChangeAsync?: ValidateAsyncFn<TData, TFormData>
38
+ onChangeAsyncDebounceMs?: number
39
+ onBlur?: ValidateFn<TData, TFormData>
40
+ onBlurAsync?: ValidateAsyncFn<TData, TFormData>
41
+ onBlurAsyncDebounceMs?: number
42
+ onSubmitAsync?: ValidateAsyncFn<TData, TFormData>
24
43
  defaultMeta?: Partial<FieldMeta>
25
44
  }
26
45
 
@@ -50,12 +69,12 @@ export type UserInputProps = {
50
69
 
51
70
  export type ChangeProps<TData> = {
52
71
  value: TData
53
- onChange: (updater: Updater<TData>) => void
72
+ onChange: (value: TData) => void
54
73
  onBlur: (event: any) => void
55
74
  }
56
75
 
57
- export type InputProps = {
58
- value: string
76
+ export type InputProps<T> = {
77
+ value: T
59
78
  onChange: (event: any) => void
60
79
  onBlur: (event: any) => void
61
80
  }
@@ -67,19 +86,33 @@ export type FieldState<TData> = {
67
86
  meta: FieldMeta
68
87
  }
69
88
 
89
+ /**
90
+ * TData may not be known at the time of FieldApi construction, so we need to
91
+ * use a conditional type to determine if TData is known or not.
92
+ *
93
+ * If TData is not known, we use the TFormData type to determine the type of
94
+ * the field value based on the field name.
95
+ */
96
+ type GetTData<Name, TData, TFormData> = unknown extends TData
97
+ ? DeepValue<TFormData, Name>
98
+ : TData
99
+
70
100
  export class FieldApi<TData, TFormData> {
71
101
  uid: number
72
102
  form: FormApi<TFormData>
73
103
  name!: DeepKeys<TFormData>
74
- store!: Store<FieldState<TData>>
75
- state!: FieldState<TData>
76
- options: RequiredByKey<
77
- FieldOptions<TData, TFormData>,
78
- | 'validatePristine'
79
- | 'validateOn'
80
- | 'validateAsyncOn'
81
- | 'validateAsyncDebounceMs'
82
- > = {} as any
104
+ /**
105
+ * This is a hack that allows us to use `GetTData` without calling it everywhere
106
+ *
107
+ * Unfortunately this hack appears to be needed alongside the `TName` hack
108
+ * further up in this file. This properly types all of the internal methods,
109
+ * while the `TName` hack types the options properly
110
+ */
111
+ _tdata!: GetTData<typeof this.name, TData, TFormData>
112
+ store!: Store<FieldState<typeof this._tdata>>
113
+ state!: FieldState<typeof this._tdata>
114
+ prevState!: FieldState<typeof this._tdata>
115
+ options: FieldOptions<typeof this._tdata, TFormData> = {} as any
83
116
 
84
117
  constructor(opts: FieldApiOptions<TData, TFormData>) {
85
118
  this.form = opts.form
@@ -92,7 +125,7 @@ export class FieldApi<TData, TFormData> {
92
125
 
93
126
  this.name = opts.name as any
94
127
 
95
- this.store = new Store<FieldState<TData>>(
128
+ this.store = new Store<FieldState<typeof this._tdata>>(
96
129
  {
97
130
  value: this.getValue(),
98
131
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@@ -103,23 +136,22 @@ export class FieldApi<TData, TFormData> {
103
136
  },
104
137
  },
105
138
  {
106
- onUpdate: (next) => {
107
- next.meta.touchedError = next.meta.isTouched
108
- ? next.meta.error
139
+ onUpdate: () => {
140
+ const state = this.store.state
141
+
142
+ state.meta.touchedError = state.meta.isTouched
143
+ ? state.meta.error
109
144
  : undefined
110
145
 
111
- // Do not validate pristine fields
112
- const prevState = this.state
113
- this.state = next
114
- if (next.value !== prevState.value) {
115
- this.validate('change', next.value)
116
- }
146
+ this.prevState = state
147
+ this.state = state
117
148
  },
118
149
  },
119
150
  )
120
151
 
121
152
  this.state = this.store.state
122
- this.update(opts)
153
+ this.prevState = this.state
154
+ this.update(opts as never)
123
155
  }
124
156
 
125
157
  mount = () => {
@@ -127,9 +159,22 @@ export class FieldApi<TData, TFormData> {
127
159
  info.instances[this.uid] = this
128
160
 
129
161
  const unsubscribe = this.form.store.subscribe(() => {
130
- this.#updateStore()
162
+ this.store.batch(() => {
163
+ const nextValue = this.getValue()
164
+ const nextMeta = this.getMeta()
165
+
166
+ if (nextValue !== this.state.value) {
167
+ this.store.setState((prev) => ({ ...prev, value: nextValue }))
168
+ }
169
+
170
+ if (nextMeta !== this.state.meta) {
171
+ this.store.setState((prev) => ({ ...prev, meta: nextMeta }))
172
+ }
173
+ })
131
174
  })
132
175
 
176
+ this.options.onMount?.(this as never)
177
+
133
178
  return () => {
134
179
  unsubscribe()
135
180
  delete info.instances[this.uid]
@@ -139,37 +184,30 @@ export class FieldApi<TData, TFormData> {
139
184
  }
140
185
  }
141
186
 
142
- #updateStore = () => {
143
- this.store.batch(() => {
144
- const nextValue = this.getValue()
145
- const nextMeta = this.getMeta()
146
-
147
- if (nextValue !== this.state.value) {
148
- this.store.setState((prev) => ({ ...prev, value: nextValue }))
149
- }
150
-
151
- if (nextMeta !== this.state.meta) {
152
- this.store.setState((prev) => ({ ...prev, meta: nextMeta }))
153
- }
154
- })
155
- }
156
-
157
- update = (opts: FieldApiOptions<TData, TFormData>) => {
187
+ update = (opts: FieldApiOptions<typeof this._tdata, TFormData>) => {
158
188
  this.options = {
159
- validatePristine: this.form.options.defaultValidatePristine ?? false,
160
- validateOn: this.form.options.defaultValidateOn ?? 'change',
161
- validateAsyncOn: this.form.options.defaultValidateAsyncOn ?? 'blur',
162
- validateAsyncDebounceMs:
163
- this.form.options.defaultValidateAsyncDebounceMs ?? 0,
189
+ asyncDebounceMs: this.form.options.asyncDebounceMs ?? 0,
190
+ onChangeAsyncDebounceMs: this.form.options.onChangeAsyncDebounceMs ?? 0,
191
+ onBlurAsyncDebounceMs: this.form.options.onBlurAsyncDebounceMs ?? 0,
164
192
  ...opts,
165
- }
193
+ } as never
166
194
 
167
195
  // Default Value
168
- if (
169
- this.state.value === undefined &&
170
- this.options.defaultValue !== undefined
171
- ) {
172
- this.setValue(this.options.defaultValue)
196
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
197
+ if (this.state.value === undefined) {
198
+ if (this.options.defaultValue !== undefined) {
199
+ this.setValue(this.options.defaultValue as never)
200
+ } else if (
201
+ opts.form.options.defaultValues?.[
202
+ this.options.name as keyof TFormData
203
+ ] !== undefined
204
+ ) {
205
+ this.setValue(
206
+ opts.form.options.defaultValues[
207
+ this.options.name as keyof TFormData
208
+ ] as never,
209
+ )
210
+ }
173
211
  }
174
212
 
175
213
  // Default Meta
@@ -179,46 +217,61 @@ export class FieldApi<TData, TFormData> {
179
217
  }
180
218
  }
181
219
 
182
- getValue = (): TData => {
220
+ getValue = (): typeof this._tdata => {
183
221
  return this.form.getFieldValue(this.name)
184
222
  }
223
+
185
224
  setValue = (
186
- updater: Updater<TData>,
225
+ updater: Updater<typeof this._tdata>,
187
226
  options?: { touch?: boolean; notify?: boolean },
188
- ) => this.form.setFieldValue(this.name, updater as any, options)
227
+ ) => {
228
+ this.form.setFieldValue(this.name, updater as never, options)
229
+ this.validate('change', this.state.value)
230
+ }
189
231
 
190
232
  getMeta = (): FieldMeta => this.form.getFieldMeta(this.name)
233
+
191
234
  setMeta = (updater: Updater<FieldMeta>) =>
192
235
  this.form.setFieldMeta(this.name, updater)
193
236
 
194
237
  getInfo = () => this.form.getFieldInfo(this.name)
195
238
 
196
- pushValue = (value: TData extends any[] ? TData[number] : never) =>
197
- this.form.pushFieldValue(this.name, value as any)
198
- insertValue = (index: number, value: TData) =>
199
- this.form.insertFieldValue(this.name, index, value as any)
239
+ pushValue = (
240
+ value: typeof this._tdata extends any[]
241
+ ? (typeof this._tdata)[number]
242
+ : never,
243
+ ) => this.form.pushFieldValue(this.name, value as any)
244
+
245
+ insertValue = (
246
+ index: number,
247
+ value: typeof this._tdata extends any[]
248
+ ? (typeof this._tdata)[number]
249
+ : never,
250
+ ) => this.form.insertFieldValue(this.name, index, value as any)
251
+
200
252
  removeValue = (index: number) => this.form.removeFieldValue(this.name, index)
253
+
201
254
  swapValues = (aIndex: number, bIndex: number) =>
202
255
  this.form.swapFieldValues(this.name, aIndex, bIndex)
203
256
 
204
- getSubField = <TName extends DeepKeys<TData>>(name: TName) =>
205
- new FieldApi<DeepValue<TData, TName>, TFormData>({
206
- name: `${this.name}.${name}` as any,
257
+ getSubField = <TName extends DeepKeys<typeof this._tdata>>(name: TName) =>
258
+ new FieldApi<DeepValue<typeof this._tdata, TName>, TFormData>({
259
+ name: `${this.name}.${name}` as never,
207
260
  form: this.form,
208
261
  })
209
262
 
210
- validateSync = async (value = this.state.value) => {
211
- const { validate } = this.options
263
+ validateSync = (value = this.state.value, cause: ValidationCause) => {
264
+ const { onChange, onBlur } = this.options
265
+ const validate =
266
+ cause === 'submit' ? undefined : cause === 'change' ? onChange : onBlur
212
267
 
213
- if (!validate) {
214
- return
215
- }
268
+ if (!validate) return
216
269
 
217
270
  // Use the validationCount for all field instances to
218
271
  // track freshness of the validation
219
272
  const validationCount = (this.getInfo().validationCount || 0) + 1
220
273
  this.getInfo().validationCount = validationCount
221
- const error = normalizeError(validate(value, this))
274
+ const error = normalizeError(validate(value as never, this as never))
222
275
 
223
276
  if (this.state.meta.error !== error) {
224
277
  this.setMeta((prev) => ({
@@ -249,12 +302,33 @@ export class FieldApi<TData, TFormData> {
249
302
  }))
250
303
  }
251
304
 
252
- validateAsync = async (value = this.state.value) => {
253
- const { validateAsync, validateAsyncDebounceMs } = this.options
254
-
255
- if (!validateAsync) {
256
- return
257
- }
305
+ validateAsync = async (value = this.state.value, cause: ValidationCause) => {
306
+ const {
307
+ onChangeAsync,
308
+ onBlurAsync,
309
+ onSubmitAsync,
310
+ asyncDebounceMs,
311
+ onBlurAsyncDebounceMs,
312
+ onChangeAsyncDebounceMs,
313
+ } = this.options
314
+
315
+ const validate =
316
+ cause === 'change'
317
+ ? onChangeAsync
318
+ : cause === 'submit'
319
+ ? onSubmitAsync
320
+ : onBlurAsync
321
+
322
+ if (!validate) return
323
+
324
+ const debounceMs =
325
+ cause === 'submit'
326
+ ? 0
327
+ : (cause === 'change'
328
+ ? onChangeAsyncDebounceMs
329
+ : onBlurAsyncDebounceMs) ??
330
+ asyncDebounceMs ??
331
+ 500
258
332
 
259
333
  if (this.state.meta.isValidating !== true)
260
334
  this.setMeta((prev) => ({ ...prev, isValidating: true }))
@@ -273,14 +347,14 @@ export class FieldApi<TData, TFormData> {
273
347
  })
274
348
  }
275
349
 
276
- if (validateAsyncDebounceMs > 0) {
277
- await new Promise((r) => setTimeout(r, validateAsyncDebounceMs))
350
+ if (debounceMs > 0) {
351
+ await new Promise((r) => setTimeout(r, debounceMs))
278
352
  }
279
353
 
280
354
  // Only kick off validation if this validation is the latest attempt
281
355
  if (checkLatest()) {
282
356
  try {
283
- const rawError = await validateAsync(value, this)
357
+ const rawError = await validate(value as never, this as never)
284
358
 
285
359
  if (checkLatest()) {
286
360
  const error = normalizeError(rawError)
@@ -308,89 +382,67 @@ export class FieldApi<TData, TFormData> {
308
382
  return this.getInfo().validationPromise
309
383
  }
310
384
 
311
- shouldValidate = (isAsync: boolean, cause?: ValidationCause) => {
312
- const { validateOn, validateAsyncOn } = this.options
313
- const level = getValidationCauseLevel(cause)
314
-
315
- // Must meet *at least* the validation level to validate,
316
- // e.g. if validateOn is 'change' and validateCause is 'blur',
317
- // the field will still validate
318
- return Object.keys(validateCauseLevels).some((d) =>
319
- isAsync
320
- ? validateAsyncOn
321
- : validateOn === d && level >= validateCauseLevels[d],
322
- )
323
- }
324
-
325
- validate = async (
326
- cause?: ValidationCause,
327
- value?: TData,
328
- ): Promise<ValidationError> => {
385
+ validate = (
386
+ cause: ValidationCause,
387
+ value?: typeof this._tdata,
388
+ ): ValidationError | Promise<ValidationError> => {
329
389
  // If the field is pristine and validatePristine is false, do not validate
330
- if (!this.options.validatePristine && !this.state.meta.isTouched) return
390
+ if (!this.state.meta.isTouched) return
331
391
 
332
392
  // Attempt to sync validate first
333
- if (this.shouldValidate(false, cause)) {
334
- this.validateSync(value)
335
- }
393
+ this.validateSync(value, cause)
336
394
 
337
395
  // If there is an error, return it, do not attempt async validation
338
396
  if (this.state.meta.error) {
339
- return this.state.meta.error
397
+ if (!this.options.asyncAlways) {
398
+ return this.state.meta.error
399
+ }
340
400
  }
341
401
 
342
402
  // No error? Attempt async validation
343
- if (this.shouldValidate(true, cause)) {
344
- return this.validateAsync(value)
345
- }
346
-
347
- // If there is no sync error or async validation attempt, there is no error
348
- return undefined
403
+ return this.validateAsync(value, cause)
349
404
  }
350
405
 
351
406
  getChangeProps = <T extends UserChangeProps<any>>(
352
407
  props: T = {} as T,
353
- ): ChangeProps<TData> & Omit<T, keyof ChangeProps<TData>> => {
408
+ ): ChangeProps<typeof this._tdata> &
409
+ Omit<T, keyof ChangeProps<typeof this._tdata>> => {
354
410
  return {
355
411
  ...props,
356
412
  value: this.state.value,
357
413
  onChange: (value) => {
358
- this.setValue(value)
414
+ this.setValue(value as never)
359
415
  props.onChange?.(value)
360
416
  },
361
417
  onBlur: (e) => {
418
+ const prevTouched = this.state.meta.isTouched
362
419
  this.setMeta((prev) => ({ ...prev, isTouched: true }))
420
+ if (!prevTouched) {
421
+ this.validate('change')
422
+ }
363
423
  this.validate('blur')
364
- props.onBlur?.(e)
365
424
  },
366
- } as ChangeProps<TData> & Omit<T, keyof ChangeProps<TData>>
425
+ } as ChangeProps<typeof this._tdata> &
426
+ Omit<T, keyof ChangeProps<typeof this._tdata>>
367
427
  }
368
428
 
369
429
  getInputProps = <T extends UserInputProps>(
370
430
  props: T = {} as T,
371
- ): InputProps & Omit<T, keyof InputProps> => {
431
+ ): InputProps<typeof this._tdata> &
432
+ Omit<T, keyof InputProps<typeof this._tdata>> => {
372
433
  return {
373
434
  ...props,
374
- value: String(this.state.value),
435
+ value: this.state.value,
375
436
  onChange: (e) => {
376
437
  this.setValue(e.target.value)
377
438
  props.onChange?.(e.target.value)
378
439
  },
379
440
  onBlur: this.getChangeProps(props).onBlur,
380
- }
441
+ } as InputProps<typeof this._tdata> &
442
+ Omit<T, keyof InputProps<typeof this._tdata>>
381
443
  }
382
444
  }
383
445
 
384
- const validateCauseLevels = {
385
- change: 0,
386
- blur: 1,
387
- submit: 2,
388
- }
389
-
390
- function getValidationCauseLevel(cause?: ValidationCause) {
391
- return !cause ? 3 : validateCauseLevels[cause]
392
- }
393
-
394
446
  function normalizeError(rawError?: ValidationError) {
395
447
  if (rawError) {
396
448
  if (typeof rawError !== 'string') {