@tanstack/form-core 0.3.3 → 0.3.5

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/src/FieldApi.ts CHANGED
@@ -4,59 +4,67 @@ import { Store } from '@tanstack/store'
4
4
 
5
5
  export type ValidationCause = 'change' | 'blur' | 'submit' | 'mount'
6
6
 
7
- type ValidateFn<TData, TFormData> = (
7
+ type ValidateFn<TData, TParentData, TName extends DeepKeys<TParentData>> = (
8
8
  value: TData,
9
- fieldApi: FieldApi<TData, TFormData>,
9
+ fieldApi: FieldApi<TData, TParentData, TName>,
10
10
  ) => ValidationError
11
11
 
12
- type ValidateAsyncFn<TData, TFormData> = (
12
+ type ValidateAsyncFn<
13
+ TData,
14
+ TParentData,
15
+ TName extends DeepKeys<TParentData>,
16
+ > = (
13
17
  value: TData,
14
- fieldApi: FieldApi<TData, TFormData>,
18
+ fieldApi: FieldApi<TData, TParentData, TName>,
15
19
  ) => ValidationError | Promise<ValidationError>
16
20
 
17
21
  export interface FieldOptions<
18
- _TData,
19
- TFormData,
22
+ TData,
23
+ TParentData,
20
24
  /**
21
25
  * This allows us to restrict the name to only be a valid field name while
22
26
  * also assigning it to a generic
23
27
  */
24
- TName = unknown extends TFormData ? string : DeepKeys<TFormData>,
28
+ TName extends DeepKeys<TParentData>,
25
29
  /**
26
30
  * If TData is unknown, we can use the TName generic to determine the type
27
31
  */
28
- TData = unknown extends _TData ? DeepValue<TFormData, TName> : _TData,
32
+ TResolvedData = unknown extends TData ? DeepValue<TParentData, TName> : TData,
29
33
  > {
30
- name: TName
31
- index?: TData extends any[] ? number : never
32
- defaultValue?: TData
34
+ name: DeepKeys<TParentData>
35
+ index?: TResolvedData extends any[] ? number : never
36
+ defaultValue?: TResolvedData
33
37
  asyncDebounceMs?: number
34
38
  asyncAlways?: boolean
35
- onMount?: (formApi: FieldApi<TData, TFormData>) => void
36
- onChange?: ValidateFn<TData, TFormData>
37
- onChangeAsync?: ValidateAsyncFn<TData, TFormData>
39
+ onMount?: (formApi: FieldApi<TResolvedData, TParentData, TName>) => void
40
+ onChange?: ValidateFn<TResolvedData, TParentData, TName>
41
+ onChangeAsync?: ValidateAsyncFn<TResolvedData, TParentData, TName>
38
42
  onChangeAsyncDebounceMs?: number
39
- onBlur?: ValidateFn<TData, TFormData>
40
- onBlurAsync?: ValidateAsyncFn<TData, TFormData>
43
+ onBlur?: ValidateFn<TResolvedData, TParentData, TName>
44
+ onBlurAsync?: ValidateAsyncFn<TResolvedData, TParentData, TName>
41
45
  onBlurAsyncDebounceMs?: number
42
- onSubmitAsync?: ValidateAsyncFn<TData, TFormData>
46
+ onSubmitAsync?: ValidateAsyncFn<TResolvedData, TParentData, TName>
43
47
  defaultMeta?: Partial<FieldMeta>
44
48
  }
45
49
 
46
50
  export interface FieldApiOptions<
47
- _TData,
48
- TFormData,
51
+ TData,
52
+ TParentData,
49
53
  /**
50
54
  * This allows us to restrict the name to only be a valid field name while
51
55
  * also assigning it to a generic
52
56
  */
53
- TName = unknown extends TFormData ? string : DeepKeys<TFormData>,
57
+ TName extends DeepKeys<TParentData>,
54
58
  /**
55
59
  * If TData is unknown, we can use the TName generic to determine the type
56
60
  */
57
- TData = unknown extends _TData ? DeepValue<TFormData, TName> : _TData,
58
- > extends FieldOptions<_TData, TFormData, TName, TData> {
59
- form: FormApi<TFormData>
61
+ TResolvedData extends ResolveData<TData, TParentData, TName> = ResolveData<
62
+ TData,
63
+ TParentData,
64
+ TName
65
+ >,
66
+ > extends FieldOptions<TData, TParentData, TName, TResolvedData> {
67
+ form: FormApi<TParentData>
60
68
  }
61
69
 
62
70
  export type FieldMeta = {
@@ -74,43 +82,35 @@ export type FieldState<TData> = {
74
82
  meta: FieldMeta
75
83
  }
76
84
 
77
- type GetTData<
78
- TData,
79
- TFormData,
80
- Opts extends FieldApiOptions<TData, TFormData>,
81
- > = Opts extends FieldApiOptions<
82
- infer _TData,
83
- infer _TFormData,
84
- infer _TName,
85
- infer RealTData
86
- >
87
- ? RealTData
88
- : never
85
+ export type ResolveData<TData, TParentData, TName> = unknown extends TData
86
+ ? DeepValue<TParentData, TName>
87
+ : TData
88
+
89
+ export type ResolveName<TParentData> = unknown extends TParentData
90
+ ? string
91
+ : DeepKeys<TParentData>
89
92
 
90
93
  export class FieldApi<
91
- _TData,
92
- TFormData,
93
- Opts extends FieldApiOptions<_TData, TFormData> = FieldApiOptions<
94
- _TData,
95
- TFormData
96
- >,
97
- TData extends GetTData<_TData, TFormData, Opts> = GetTData<
98
- _TData,
99
- TFormData,
100
- Opts
94
+ TData,
95
+ TParentData,
96
+ TName extends DeepKeys<TParentData>,
97
+ TResolvedData extends ResolveData<TData, TParentData, TName> = ResolveData<
98
+ TData,
99
+ TParentData,
100
+ TName
101
101
  >,
102
102
  > {
103
103
  uid: number
104
- form: Opts['form']
105
- name!: DeepKeys<TFormData>
106
- options: Opts = {} as any
107
- store!: Store<FieldState<TData>>
108
- state!: FieldState<TData>
109
- prevState!: FieldState<TData>
104
+ form: FieldApiOptions<TData, TParentData, TName, TResolvedData>['form']
105
+ name!: DeepKeys<TParentData>
106
+ options: FieldApiOptions<TData, TParentData, TName> = {} as any
107
+ store!: Store<FieldState<TResolvedData>>
108
+ state!: FieldState<TResolvedData>
109
+ prevState!: FieldState<TResolvedData>
110
110
 
111
111
  constructor(
112
- opts: Opts & {
113
- form: FormApi<TFormData>
112
+ opts: FieldApiOptions<TData, TParentData, TName, TResolvedData> & {
113
+ form: FormApi<TParentData>
114
114
  },
115
115
  ) {
116
116
  this.form = opts.form
@@ -123,7 +123,7 @@ export class FieldApi<
123
123
 
124
124
  this.name = opts.name as any
125
125
 
126
- this.store = new Store<FieldState<TData>>(
126
+ this.store = new Store<FieldState<TResolvedData>>(
127
127
  {
128
128
  value: this.getValue(),
129
129
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@@ -140,6 +140,10 @@ export class FieldApi<
140
140
  onUpdate: () => {
141
141
  const state = this.store.state
142
142
 
143
+ state.meta.errors = Object.values(state.meta.errorMap).filter(
144
+ (val: unknown) => val !== undefined,
145
+ )
146
+
143
147
  state.meta.touchedErrors = state.meta.isTouched
144
148
  ? state.meta.errors
145
149
  : []
@@ -186,12 +190,12 @@ export class FieldApi<
186
190
  }
187
191
  }
188
192
 
189
- update = (opts: FieldApiOptions<TData, TFormData>) => {
193
+ update = (opts: FieldApiOptions<TResolvedData, TParentData, TName>) => {
190
194
  // Default Value
191
195
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
192
196
  if (this.state.value === undefined) {
193
197
  const formDefault =
194
- opts.form.options.defaultValues?.[opts.name as keyof TFormData]
198
+ opts.form.options.defaultValues?.[opts.name as keyof TParentData]
195
199
 
196
200
  if (opts.defaultValue !== undefined) {
197
201
  this.setValue(opts.defaultValue as never)
@@ -208,12 +212,12 @@ export class FieldApi<
208
212
  this.options = opts as never
209
213
  }
210
214
 
211
- getValue = (): TData => {
212
- return this.form.getFieldValue(this.name)
215
+ getValue = (): TResolvedData => {
216
+ return this.form.getFieldValue(this.name) as any
213
217
  }
214
218
 
215
219
  setValue = (
216
- updater: Updater<TData>,
220
+ updater: Updater<TResolvedData>,
217
221
  options?: { touch?: boolean; notify?: boolean },
218
222
  ) => {
219
223
  this.form.setFieldValue(this.name, updater as never, options)
@@ -237,12 +241,13 @@ export class FieldApi<
237
241
 
238
242
  getInfo = () => this.form.getFieldInfo(this.name)
239
243
 
240
- pushValue = (value: TData extends any[] ? TData[number] : never) =>
241
- this.form.pushFieldValue(this.name, value as any)
244
+ pushValue = (
245
+ value: TResolvedData extends any[] ? TResolvedData[number] : never,
246
+ ) => this.form.pushFieldValue(this.name, value as any)
242
247
 
243
248
  insertValue = (
244
249
  index: number,
245
- value: TData extends any[] ? TData[number] : never,
250
+ value: TResolvedData extends any[] ? TResolvedData[number] : never,
246
251
  ) => this.form.insertFieldValue(this.name, index, value as any)
247
252
 
248
253
  removeValue = (index: number) => this.form.removeFieldValue(this.name, index)
@@ -250,11 +255,21 @@ export class FieldApi<
250
255
  swapValues = (aIndex: number, bIndex: number) =>
251
256
  this.form.swapFieldValues(this.name, aIndex, bIndex)
252
257
 
253
- getSubField = <TName extends DeepKeys<TData>>(name: TName) =>
254
- new FieldApi<DeepValue<TData, TName>, TFormData>({
258
+ getSubField = <
259
+ TSubData,
260
+ TSubName extends DeepKeys<TResolvedData>,
261
+ TSubResolvedData extends ResolveData<
262
+ DeepValue<TResolvedData, TSubName>,
263
+ TResolvedData,
264
+ TSubName
265
+ >,
266
+ >(
267
+ name: TSubName,
268
+ ): FieldApi<TSubData, TResolvedData, TSubName, TSubResolvedData> =>
269
+ new FieldApi({
255
270
  name: `${this.name}.${name}` as never,
256
271
  form: this.form,
257
- })
272
+ }) as any
258
273
 
259
274
  validateSync = (value = this.state.value, cause: ValidationCause) => {
260
275
  const { onChange, onBlur } = this.options
@@ -268,10 +283,9 @@ export class FieldApi<
268
283
  this.getInfo().validationCount = validationCount
269
284
  const error = normalizeError(validate(value as never, this as never))
270
285
  const errorMapKey = getErrorMapKey(cause)
271
- if (error && this.state.meta.errorMap[errorMapKey] !== error) {
286
+ if (this.state.meta.errorMap[errorMapKey] !== error) {
272
287
  this.setMeta((prev) => ({
273
288
  ...prev,
274
- errors: [...prev.errors, error],
275
289
  errorMap: {
276
290
  ...prev.errorMap,
277
291
  [getErrorMapKey(cause)]: error,
@@ -358,7 +372,6 @@ export class FieldApi<
358
372
  this.setMeta((prev) => ({
359
373
  ...prev,
360
374
  isValidating: false,
361
- errors: [...prev.errors, error],
362
375
  errorMap: {
363
376
  ...prev.errorMap,
364
377
  [getErrorMapKey(cause)]: error,
@@ -385,7 +398,7 @@ export class FieldApi<
385
398
 
386
399
  validate = (
387
400
  cause: ValidationCause,
388
- value?: TData,
401
+ value?: TResolvedData,
389
402
  ): ValidationError[] | Promise<ValidationError[]> => {
390
403
  // If the field is pristine and validatePristine is false, do not validate
391
404
  if (!this.state.meta.isTouched) return []
@@ -403,7 +416,7 @@ export class FieldApi<
403
416
  return this.validateAsync(value, cause)
404
417
  }
405
418
 
406
- handleChange = (updater: Updater<TData>) => {
419
+ handleChange = (updater: Updater<TResolvedData>) => {
407
420
  this.setValue(updater, { touch: true })
408
421
  }
409
422
 
package/src/FormApi.ts CHANGED
@@ -31,7 +31,7 @@ export type FormOptions<TData> = {
31
31
  }
32
32
 
33
33
  export type FieldInfo<TFormData> = {
34
- instances: Record<string, FieldApi<any, TFormData>>
34
+ instances: Record<string, FieldApi<any, TFormData, any>>
35
35
  } & ValidationMeta
36
36
 
37
37
  export type ValidationMeta = {
@@ -106,7 +106,7 @@ export class FormApi<TFormData> {
106
106
  constructor(opts?: FormOptions<TFormData>) {
107
107
  this.store = new Store<FormState<TFormData>>(
108
108
  getDefaultFormState({
109
- ...opts?.defaultState,
109
+ ...(opts?.defaultState as any),
110
110
  values: opts?.defaultValues ?? opts?.defaultState?.values,
111
111
  isFormValid: true,
112
112
  }),
@@ -174,7 +174,7 @@ export class FormApi<TFormData> {
174
174
  getDefaultFormState(
175
175
  Object.assign(
176
176
  {},
177
- this.state,
177
+ this.state as any,
178
178
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
179
179
  shouldUpdateState ? options.defaultState : {},
180
180
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@@ -194,7 +194,7 @@ export class FormApi<TFormData> {
194
194
  reset = () =>
195
195
  this.store.setState(() =>
196
196
  getDefaultFormState({
197
- ...this.options.defaultState,
197
+ ...(this.options.defaultState as any),
198
198
  values: this.options.defaultValues ?? this.options.defaultState?.values,
199
199
  }),
200
200
  )
@@ -288,7 +288,9 @@ export class FormApi<TFormData> {
288
288
  return this.state.fieldMeta[field]
289
289
  }
290
290
 
291
- getFieldInfo = <TField extends DeepKeys<TFormData>>(field: TField) => {
291
+ getFieldInfo = <TField extends DeepKeys<TFormData>>(
292
+ field: TField,
293
+ ): FieldInfo<TFormData> => {
292
294
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
293
295
  return (this.fieldInfo[field] ||= {
294
296
  instances: {},
@@ -501,6 +501,36 @@ describe('field api', () => {
501
501
  })
502
502
  })
503
503
 
504
+ it('should reset onChange errors when the issue is resolved', () => {
505
+ const form = new FormApi({
506
+ defaultValues: {
507
+ name: 'other',
508
+ },
509
+ })
510
+
511
+ const field = new FieldApi({
512
+ form,
513
+ name: 'name',
514
+ onChange: (value) => {
515
+ if (value === 'other') return 'Please enter a different value'
516
+ return
517
+ },
518
+ })
519
+
520
+ field.mount()
521
+
522
+ field.setValue('other', { touch: true })
523
+ expect(field.getMeta().errors).toStrictEqual([
524
+ 'Please enter a different value',
525
+ ])
526
+ expect(field.getMeta().errorMap).toEqual({
527
+ onChange: 'Please enter a different value',
528
+ })
529
+ field.setValue('test', { touch: true })
530
+ expect(field.getMeta().errors).toStrictEqual([])
531
+ expect(field.getMeta().errorMap).toEqual({})
532
+ })
533
+
504
534
  it('should handle default value on field using state.value', async () => {
505
535
  interface Form {
506
536
  name: string
package/src/utils.ts CHANGED
@@ -133,22 +133,28 @@ type AllowedIndexes<
133
133
  ? AllowedIndexes<Tail, Keys | Tail['length']>
134
134
  : Keys
135
135
 
136
- export type DeepKeys<T> = unknown extends T
137
- ? keyof T
136
+ export type DeepKeys<T, TDepth extends any[] = []> = TDepth['length'] extends 5
137
+ ? never
138
+ : unknown extends T
139
+ ? string
138
140
  : object extends T
139
141
  ? string
140
142
  : T extends readonly any[] & IsTuple<T>
141
- ? AllowedIndexes<T> | DeepKeysPrefix<T, AllowedIndexes<T>>
143
+ ? AllowedIndexes<T> | DeepKeysPrefix<T, AllowedIndexes<T>, TDepth>
142
144
  : T extends any[]
143
- ? DeepKeys<T[number]>
145
+ ? DeepKeys<T[number], [...TDepth, any]>
144
146
  : T extends Date
145
147
  ? never
146
148
  : T extends object
147
- ? (keyof T & string) | DeepKeysPrefix<T, keyof T>
149
+ ? (keyof T & string) | DeepKeysPrefix<T, keyof T, TDepth>
148
150
  : never
149
151
 
150
- type DeepKeysPrefix<T, TPrefix> = TPrefix extends keyof T & (number | string)
151
- ? `${TPrefix}.${DeepKeys<T[TPrefix]> & string}`
152
+ type DeepKeysPrefix<
153
+ T,
154
+ TPrefix,
155
+ TDepth extends any[],
156
+ > = TPrefix extends keyof T & (number | string)
157
+ ? `${TPrefix}.${DeepKeys<T[TPrefix], [...TDepth, any]> & string}`
152
158
  : never
153
159
 
154
160
  export type DeepValue<T, TProp> = T extends Record<string | number, any>