@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/build/legacy/FieldApi.cjs +4 -3
- package/build/legacy/FieldApi.cjs.map +1 -1
- package/build/legacy/FieldApi.d.cts +1 -1
- package/build/legacy/FieldApi.d.ts +1 -1
- package/build/legacy/FieldApi.js +4 -3
- package/build/legacy/FieldApi.js.map +1 -1
- package/build/legacy/FormApi.cjs.map +1 -1
- package/build/legacy/FormApi.js.map +1 -1
- package/build/legacy/index.d.cts +43 -42
- package/build/legacy/index.d.ts +43 -42
- package/build/legacy/utils.cjs.map +1 -1
- package/build/legacy/utils.d.cts +2 -2
- package/build/legacy/utils.d.ts +2 -2
- package/build/legacy/utils.js.map +1 -1
- package/build/modern/FieldApi.cjs +4 -3
- package/build/modern/FieldApi.cjs.map +1 -1
- package/build/modern/FieldApi.d.cts +1 -1
- package/build/modern/FieldApi.d.ts +1 -1
- package/build/modern/FieldApi.js +4 -3
- package/build/modern/FieldApi.js.map +1 -1
- package/build/modern/FormApi.cjs.map +1 -1
- package/build/modern/FormApi.js.map +1 -1
- package/build/modern/index.d.cts +43 -42
- package/build/modern/index.d.ts +43 -42
- package/build/modern/utils.cjs.map +1 -1
- package/build/modern/utils.d.cts +2 -2
- package/build/modern/utils.d.ts +2 -2
- package/build/modern/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/FieldApi.ts +83 -70
- package/src/FormApi.ts +7 -5
- package/src/tests/FieldApi.spec.ts +30 -0
- package/src/utils.ts +13 -7
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,
|
|
7
|
+
type ValidateFn<TData, TParentData, TName extends DeepKeys<TParentData>> = (
|
|
8
8
|
value: TData,
|
|
9
|
-
fieldApi: FieldApi<TData,
|
|
9
|
+
fieldApi: FieldApi<TData, TParentData, TName>,
|
|
10
10
|
) => ValidationError
|
|
11
11
|
|
|
12
|
-
type ValidateAsyncFn<
|
|
12
|
+
type ValidateAsyncFn<
|
|
13
|
+
TData,
|
|
14
|
+
TParentData,
|
|
15
|
+
TName extends DeepKeys<TParentData>,
|
|
16
|
+
> = (
|
|
13
17
|
value: TData,
|
|
14
|
-
fieldApi: FieldApi<TData,
|
|
18
|
+
fieldApi: FieldApi<TData, TParentData, TName>,
|
|
15
19
|
) => ValidationError | Promise<ValidationError>
|
|
16
20
|
|
|
17
21
|
export interface FieldOptions<
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
-
|
|
32
|
+
TResolvedData = unknown extends TData ? DeepValue<TParentData, TName> : TData,
|
|
29
33
|
> {
|
|
30
|
-
name:
|
|
31
|
-
index?:
|
|
32
|
-
defaultValue?:
|
|
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<
|
|
36
|
-
onChange?: ValidateFn<
|
|
37
|
-
onChangeAsync?: ValidateAsyncFn<
|
|
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<
|
|
40
|
-
onBlurAsync?: ValidateAsyncFn<
|
|
43
|
+
onBlur?: ValidateFn<TResolvedData, TParentData, TName>
|
|
44
|
+
onBlurAsync?: ValidateAsyncFn<TResolvedData, TParentData, TName>
|
|
41
45
|
onBlurAsyncDebounceMs?: number
|
|
42
|
-
onSubmitAsync?: ValidateAsyncFn<
|
|
46
|
+
onSubmitAsync?: ValidateAsyncFn<TResolvedData, TParentData, TName>
|
|
43
47
|
defaultMeta?: Partial<FieldMeta>
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
export interface FieldApiOptions<
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
> =
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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:
|
|
105
|
-
name!: DeepKeys<
|
|
106
|
-
options:
|
|
107
|
-
store!: Store<FieldState<
|
|
108
|
-
state!: FieldState<
|
|
109
|
-
prevState!: FieldState<
|
|
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:
|
|
113
|
-
form: FormApi<
|
|
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<
|
|
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<
|
|
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
|
|
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 = ():
|
|
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<
|
|
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 = (
|
|
241
|
-
|
|
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:
|
|
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 = <
|
|
254
|
-
|
|
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 (
|
|
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?:
|
|
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<
|
|
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>>(
|
|
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> =
|
|
137
|
-
?
|
|
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<
|
|
151
|
-
|
|
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>
|