@tanstack/form-core 0.3.6 → 0.4.0
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 +49 -26
- package/build/legacy/FieldApi.cjs.map +1 -1
- package/build/legacy/FieldApi.d.cts +1 -0
- package/build/legacy/FieldApi.d.ts +1 -0
- package/build/legacy/FieldApi.js +49 -18
- package/build/legacy/FieldApi.js.map +1 -1
- package/build/legacy/FormApi.cjs +10 -12
- package/build/legacy/FormApi.cjs.map +1 -1
- package/build/legacy/FormApi.d.cts +2 -1
- package/build/legacy/FormApi.d.ts +2 -1
- package/build/legacy/FormApi.js +10 -14
- package/build/legacy/FormApi.js.map +1 -1
- package/build/legacy/index.cjs +3 -1
- package/build/legacy/index.cjs.map +1 -1
- package/build/legacy/index.d.cts +48 -52
- package/build/legacy/index.d.ts +48 -52
- package/build/legacy/index.js +1 -0
- package/build/legacy/index.js.map +1 -1
- package/build/legacy/types.cjs +19 -0
- package/build/legacy/types.cjs.map +1 -0
- package/build/legacy/types.d.cts +7 -0
- package/build/legacy/types.d.ts +7 -0
- package/build/legacy/types.js +1 -0
- package/build/legacy/utils.js +0 -2
- package/build/legacy/utils.js.map +1 -1
- package/build/modern/FieldApi.cjs +47 -9
- package/build/modern/FieldApi.cjs.map +1 -1
- package/build/modern/FieldApi.d.cts +1 -0
- package/build/modern/FieldApi.d.ts +1 -0
- package/build/modern/FieldApi.js +47 -9
- package/build/modern/FieldApi.js.map +1 -1
- package/build/modern/FormApi.cjs +10 -12
- package/build/modern/FormApi.cjs.map +1 -1
- package/build/modern/FormApi.d.cts +2 -1
- package/build/modern/FormApi.d.ts +2 -1
- package/build/modern/FormApi.js +10 -12
- package/build/modern/FormApi.js.map +1 -1
- package/build/modern/index.cjs +3 -1
- package/build/modern/index.cjs.map +1 -1
- package/build/modern/index.d.cts +48 -52
- package/build/modern/index.d.ts +48 -52
- package/build/modern/index.js +1 -0
- package/build/modern/index.js.map +1 -1
- package/build/modern/types.cjs +19 -0
- package/build/modern/types.cjs.map +1 -0
- package/build/modern/types.d.cts +7 -0
- package/build/modern/types.d.ts +7 -0
- package/build/modern/types.js +1 -0
- package/build/modern/types.js.map +1 -0
- package/package.json +1 -1
- package/src/FieldApi.ts +175 -44
- package/src/FormApi.ts +61 -46
- package/src/index.ts +1 -0
- package/src/tests/FieldApi.spec.ts +17 -1
- package/src/tests/FieldApi.test-d.ts +26 -11
- package/src/tests/FormApi.spec.ts +1 -1
- package/src/types.ts +7 -0
- package/build/legacy/chunk-4QZDOMDG.js +0 -19
- /package/build/legacy/{chunk-4QZDOMDG.js.map → types.js.map} +0 -0
package/src/FieldApi.ts
CHANGED
|
@@ -1,57 +1,126 @@
|
|
|
1
1
|
import { type DeepKeys, type DeepValue, type Updater } from './utils'
|
|
2
|
-
import type { FormApi,
|
|
2
|
+
import type { FormApi, ValidationErrorMap } from './FormApi'
|
|
3
3
|
import { Store } from '@tanstack/store'
|
|
4
|
+
import type { Validator, ValidationError } from './types'
|
|
4
5
|
|
|
5
6
|
export type ValidationCause = 'change' | 'blur' | 'submit' | 'mount'
|
|
6
7
|
|
|
7
|
-
type ValidateFn<
|
|
8
|
+
type ValidateFn<
|
|
9
|
+
TParentData,
|
|
10
|
+
TName extends DeepKeys<TParentData>,
|
|
11
|
+
ValidatorType,
|
|
12
|
+
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
|
13
|
+
> = (
|
|
8
14
|
value: TData,
|
|
9
|
-
fieldApi: FieldApi<TParentData, TName>,
|
|
15
|
+
fieldApi: FieldApi<TParentData, TName, ValidatorType, TData>,
|
|
10
16
|
) => ValidationError
|
|
11
17
|
|
|
18
|
+
type ValidateOrFn<
|
|
19
|
+
TParentData,
|
|
20
|
+
TName extends DeepKeys<TParentData>,
|
|
21
|
+
ValidatorType,
|
|
22
|
+
FormValidator,
|
|
23
|
+
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
|
24
|
+
> = ValidatorType extends Validator<TData>
|
|
25
|
+
?
|
|
26
|
+
| Parameters<ReturnType<ValidatorType>['validate']>[1]
|
|
27
|
+
| ValidateFn<TParentData, TName, ValidatorType, TData>
|
|
28
|
+
: FormValidator extends Validator<TData>
|
|
29
|
+
?
|
|
30
|
+
| Parameters<ReturnType<FormValidator>['validate']>[1]
|
|
31
|
+
| ValidateFn<TParentData, TName, ValidatorType, TData>
|
|
32
|
+
: ValidateFn<TParentData, TName, ValidatorType, TData>
|
|
33
|
+
|
|
12
34
|
type ValidateAsyncFn<
|
|
13
35
|
TParentData,
|
|
14
36
|
TName extends DeepKeys<TParentData>,
|
|
15
|
-
|
|
37
|
+
ValidatorType,
|
|
38
|
+
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
|
16
39
|
> = (
|
|
17
40
|
value: TData,
|
|
18
|
-
fieldApi: FieldApi<TParentData, TName>,
|
|
41
|
+
fieldApi: FieldApi<TParentData, TName, ValidatorType, TData>,
|
|
19
42
|
) => ValidationError | Promise<ValidationError>
|
|
20
43
|
|
|
44
|
+
type AsyncValidateOrFn<
|
|
45
|
+
TParentData,
|
|
46
|
+
TName extends DeepKeys<TParentData>,
|
|
47
|
+
ValidatorType,
|
|
48
|
+
FormValidator,
|
|
49
|
+
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
|
50
|
+
> = ValidatorType extends Validator<TData>
|
|
51
|
+
?
|
|
52
|
+
| Parameters<ReturnType<ValidatorType>['validate']>[1]
|
|
53
|
+
| ValidateAsyncFn<TParentData, TName, ValidatorType, TData>
|
|
54
|
+
: FormValidator extends Validator<TData>
|
|
55
|
+
?
|
|
56
|
+
| Parameters<ReturnType<FormValidator>['validate']>[1]
|
|
57
|
+
| ValidateAsyncFn<TParentData, TName, ValidatorType, TData>
|
|
58
|
+
: ValidateAsyncFn<TParentData, TName, ValidatorType, TData>
|
|
59
|
+
|
|
21
60
|
export interface FieldOptions<
|
|
22
61
|
TParentData,
|
|
23
|
-
/**
|
|
24
|
-
* This allows us to restrict the name to only be a valid field name while
|
|
25
|
-
* also assigning it to a generic
|
|
26
|
-
*/
|
|
27
62
|
TName extends DeepKeys<TParentData>,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
TData = DeepValue<TParentData, TName>,
|
|
63
|
+
ValidatorType,
|
|
64
|
+
FormValidator,
|
|
65
|
+
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
|
32
66
|
> {
|
|
33
|
-
name:
|
|
67
|
+
name: TName
|
|
34
68
|
index?: TData extends any[] ? number : never
|
|
35
69
|
defaultValue?: TData
|
|
36
70
|
asyncDebounceMs?: number
|
|
37
71
|
asyncAlways?: boolean
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
72
|
+
validator?: ValidatorType
|
|
73
|
+
onMount?: (
|
|
74
|
+
formApi: FieldApi<TParentData, TName, ValidatorType, TData>,
|
|
75
|
+
) => void
|
|
76
|
+
onChange?: ValidateOrFn<
|
|
77
|
+
TParentData,
|
|
78
|
+
TName,
|
|
79
|
+
ValidatorType,
|
|
80
|
+
FormValidator,
|
|
81
|
+
TData
|
|
82
|
+
>
|
|
83
|
+
onChangeAsync?: AsyncValidateOrFn<
|
|
84
|
+
TParentData,
|
|
85
|
+
TName,
|
|
86
|
+
ValidatorType,
|
|
87
|
+
FormValidator,
|
|
88
|
+
TData
|
|
89
|
+
>
|
|
41
90
|
onChangeAsyncDebounceMs?: number
|
|
42
|
-
onBlur?:
|
|
43
|
-
onBlurAsync?:
|
|
91
|
+
onBlur?: ValidateOrFn<TParentData, TName, ValidatorType, FormValidator, TData>
|
|
92
|
+
onBlurAsync?: AsyncValidateOrFn<
|
|
93
|
+
TParentData,
|
|
94
|
+
TName,
|
|
95
|
+
ValidatorType,
|
|
96
|
+
FormValidator,
|
|
97
|
+
TData
|
|
98
|
+
>
|
|
44
99
|
onBlurAsyncDebounceMs?: number
|
|
45
|
-
onSubmitAsync?:
|
|
100
|
+
onSubmitAsync?: AsyncValidateOrFn<
|
|
101
|
+
TParentData,
|
|
102
|
+
TName,
|
|
103
|
+
ValidatorType,
|
|
104
|
+
FormValidator,
|
|
105
|
+
TData
|
|
106
|
+
>
|
|
46
107
|
defaultMeta?: Partial<FieldMeta>
|
|
47
108
|
}
|
|
48
109
|
|
|
49
110
|
export interface FieldApiOptions<
|
|
50
111
|
TParentData,
|
|
51
112
|
TName extends DeepKeys<TParentData>,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
113
|
+
ValidatorType,
|
|
114
|
+
FormValidator,
|
|
115
|
+
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
|
116
|
+
> extends FieldOptions<
|
|
117
|
+
TParentData,
|
|
118
|
+
TName,
|
|
119
|
+
ValidatorType,
|
|
120
|
+
FormValidator,
|
|
121
|
+
TData
|
|
122
|
+
> {
|
|
123
|
+
form: FormApi<TParentData, FormValidator>
|
|
55
124
|
}
|
|
56
125
|
|
|
57
126
|
export type FieldMeta = {
|
|
@@ -76,22 +145,28 @@ export type ResolveName<TParentData> = unknown extends TParentData
|
|
|
76
145
|
export class FieldApi<
|
|
77
146
|
TParentData,
|
|
78
147
|
TName extends DeepKeys<TParentData>,
|
|
79
|
-
|
|
148
|
+
ValidatorType,
|
|
149
|
+
FormValidator,
|
|
150
|
+
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
|
80
151
|
> {
|
|
81
152
|
uid: number
|
|
82
|
-
form: FieldApiOptions<TParentData, TName, TData>['form']
|
|
153
|
+
form: FieldApiOptions<TParentData, TName, ValidatorType, TData>['form']
|
|
83
154
|
name!: DeepKeys<TParentData>
|
|
84
|
-
options: FieldApiOptions<TParentData, TName> = {} as any
|
|
155
|
+
options: FieldApiOptions<TParentData, TName, ValidatorType, TData> = {} as any
|
|
85
156
|
store!: Store<FieldState<TData>>
|
|
86
157
|
state!: FieldState<TData>
|
|
87
158
|
prevState!: FieldState<TData>
|
|
88
159
|
|
|
89
160
|
constructor(
|
|
90
|
-
opts: FieldApiOptions<
|
|
91
|
-
|
|
92
|
-
|
|
161
|
+
opts: FieldApiOptions<
|
|
162
|
+
TParentData,
|
|
163
|
+
TName,
|
|
164
|
+
ValidatorType,
|
|
165
|
+
FormValidator,
|
|
166
|
+
TData
|
|
167
|
+
>,
|
|
93
168
|
) {
|
|
94
|
-
this.form = opts.form
|
|
169
|
+
this.form = opts.form as never
|
|
95
170
|
this.uid = uid++
|
|
96
171
|
// Support field prefixing from FieldScope
|
|
97
172
|
// let fieldPrefix = ''
|
|
@@ -99,7 +174,11 @@ export class FieldApi<
|
|
|
99
174
|
// fieldPrefix = `${this.form.fieldName}.`
|
|
100
175
|
// }
|
|
101
176
|
|
|
102
|
-
this.name = opts.name as
|
|
177
|
+
this.name = opts.name as never
|
|
178
|
+
|
|
179
|
+
if (opts.defaultValue !== undefined) {
|
|
180
|
+
this.form.setFieldValue(this.name, opts.defaultValue as never)
|
|
181
|
+
}
|
|
103
182
|
|
|
104
183
|
this.store = new Store<FieldState<TData>>(
|
|
105
184
|
{
|
|
@@ -163,12 +242,14 @@ export class FieldApi<
|
|
|
163
242
|
unsubscribe()
|
|
164
243
|
delete info.instances[this.uid]
|
|
165
244
|
if (!Object.keys(info.instances).length) {
|
|
166
|
-
delete this.form.fieldInfo[this.name]
|
|
245
|
+
delete this.form.fieldInfo[this.name as never]
|
|
167
246
|
}
|
|
168
247
|
}
|
|
169
248
|
}
|
|
170
249
|
|
|
171
|
-
update = (
|
|
250
|
+
update = (
|
|
251
|
+
opts: FieldApiOptions<TParentData, TName, ValidatorType, TData>,
|
|
252
|
+
) => {
|
|
172
253
|
// Default Value
|
|
173
254
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
174
255
|
if (this.state.value === undefined) {
|
|
@@ -234,10 +315,10 @@ export class FieldApi<
|
|
|
234
315
|
|
|
235
316
|
getSubField = <
|
|
236
317
|
TSubName extends DeepKeys<TData>,
|
|
237
|
-
TSubData = DeepValue<TData, TSubName>,
|
|
318
|
+
TSubData extends DeepValue<TData, TSubName> = DeepValue<TData, TSubName>,
|
|
238
319
|
>(
|
|
239
320
|
name: TSubName,
|
|
240
|
-
): FieldApi<TData, TSubName, TSubData> =>
|
|
321
|
+
): FieldApi<TData, TSubName, ValidatorType, TSubData> =>
|
|
241
322
|
new FieldApi({
|
|
242
323
|
name: `${this.name}.${name}` as never,
|
|
243
324
|
form: this.form,
|
|
@@ -247,13 +328,36 @@ export class FieldApi<
|
|
|
247
328
|
const { onChange, onBlur } = this.options
|
|
248
329
|
const validate =
|
|
249
330
|
cause === 'submit' ? undefined : cause === 'change' ? onChange : onBlur
|
|
331
|
+
|
|
250
332
|
if (!validate) return
|
|
251
333
|
|
|
252
334
|
// Use the validationCount for all field instances to
|
|
253
335
|
// track freshness of the validation
|
|
254
336
|
const validationCount = (this.getInfo().validationCount || 0) + 1
|
|
255
337
|
this.getInfo().validationCount = validationCount
|
|
256
|
-
|
|
338
|
+
|
|
339
|
+
const doValidate = () => {
|
|
340
|
+
if (this.options.validator && typeof validate !== 'function') {
|
|
341
|
+
return (this.options.validator as Validator<TData>)().validate(
|
|
342
|
+
value,
|
|
343
|
+
validate,
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (this.form.options.validator && typeof validate !== 'function') {
|
|
348
|
+
return (this.form.options.validator as Validator<TData>)().validate(
|
|
349
|
+
value,
|
|
350
|
+
validate,
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return (validate as ValidateFn<TParentData, TName, ValidatorType, TData>)(
|
|
355
|
+
value,
|
|
356
|
+
this as never,
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const error = normalizeError(doValidate())
|
|
257
361
|
const errorMapKey = getErrorMapKey(cause)
|
|
258
362
|
if (this.state.meta.errorMap[errorMapKey] !== error) {
|
|
259
363
|
this.setMeta((prev) => ({
|
|
@@ -271,7 +375,7 @@ export class FieldApi<
|
|
|
271
375
|
}
|
|
272
376
|
}
|
|
273
377
|
|
|
274
|
-
|
|
378
|
+
__leaseValidateAsync = () => {
|
|
275
379
|
const count = (this.getInfo().validationAsyncCount || 0) + 1
|
|
276
380
|
this.getInfo().validationAsyncCount = count
|
|
277
381
|
return count
|
|
@@ -279,7 +383,7 @@ export class FieldApi<
|
|
|
279
383
|
|
|
280
384
|
cancelValidateAsync = () => {
|
|
281
385
|
// Lease a new validation count to ignore any pending validations
|
|
282
|
-
this
|
|
386
|
+
this.__leaseValidateAsync()
|
|
283
387
|
// Cancel any pending validation state
|
|
284
388
|
this.setMeta((prev) => ({
|
|
285
389
|
...prev,
|
|
@@ -313,12 +417,13 @@ export class FieldApi<
|
|
|
313
417
|
asyncDebounceMs ??
|
|
314
418
|
0
|
|
315
419
|
|
|
316
|
-
if (this.state.meta.isValidating !== true)
|
|
420
|
+
if (this.state.meta.isValidating !== true) {
|
|
317
421
|
this.setMeta((prev) => ({ ...prev, isValidating: true }))
|
|
422
|
+
}
|
|
318
423
|
|
|
319
424
|
// Use the validationCount for all field instances to
|
|
320
425
|
// track freshness of the validation
|
|
321
|
-
const validationAsyncCount = this
|
|
426
|
+
const validationAsyncCount = this.__leaseValidateAsync()
|
|
322
427
|
|
|
323
428
|
const checkLatest = () =>
|
|
324
429
|
validationAsyncCount === this.getInfo().validationAsyncCount
|
|
@@ -334,11 +439,31 @@ export class FieldApi<
|
|
|
334
439
|
await new Promise((r) => setTimeout(r, debounceMs))
|
|
335
440
|
}
|
|
336
441
|
|
|
442
|
+
const doValidate = () => {
|
|
443
|
+
if (this.options.validator && typeof validate !== 'function') {
|
|
444
|
+
return (this.options.validator as Validator<TData>)().validateAsync(
|
|
445
|
+
value,
|
|
446
|
+
validate,
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (this.form.options.validator && typeof validate !== 'function') {
|
|
451
|
+
return (
|
|
452
|
+
this.form.options.validator as Validator<TData>
|
|
453
|
+
)().validateAsync(value, validate)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return (validate as ValidateFn<TParentData, TName, ValidatorType, TData>)(
|
|
457
|
+
value,
|
|
458
|
+
this as never,
|
|
459
|
+
)
|
|
460
|
+
}
|
|
461
|
+
|
|
337
462
|
// Only kick off validation if this validation is the latest attempt
|
|
338
463
|
if (checkLatest()) {
|
|
339
464
|
const prevErrors = this.getMeta().errors
|
|
340
465
|
try {
|
|
341
|
-
const rawError = await
|
|
466
|
+
const rawError = await doValidate()
|
|
342
467
|
if (checkLatest()) {
|
|
343
468
|
const error = normalizeError(rawError)
|
|
344
469
|
this.setMeta((prev) => ({
|
|
@@ -374,12 +499,18 @@ export class FieldApi<
|
|
|
374
499
|
): ValidationError[] | Promise<ValidationError[]> => {
|
|
375
500
|
// If the field is pristine and validatePristine is false, do not validate
|
|
376
501
|
if (!this.state.meta.isTouched) return []
|
|
502
|
+
|
|
503
|
+
// Store the previous error for the errorMapKey (eg. onChange, onBlur, onSubmit)
|
|
504
|
+
const errorMapKey = getErrorMapKey(cause)
|
|
505
|
+
const prevError = this.getMeta().errorMap[errorMapKey]
|
|
506
|
+
|
|
377
507
|
// Attempt to sync validate first
|
|
378
508
|
this.validateSync(value, cause)
|
|
379
509
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
510
|
+
// If there is a new error mapped to the errorMapKey (eg. onChange, onBlur, onSubmit), return the errors array, do not attempt async validation
|
|
511
|
+
const newError = this.getMeta().errorMap[errorMapKey]
|
|
512
|
+
|
|
513
|
+
if (prevError !== newError) {
|
|
383
514
|
if (!this.options.asyncAlways) {
|
|
384
515
|
return this.state.meta.errors
|
|
385
516
|
}
|
package/src/FormApi.ts
CHANGED
|
@@ -1,37 +1,49 @@
|
|
|
1
1
|
import { Store } from '@tanstack/store'
|
|
2
|
-
//
|
|
3
2
|
import type { DeepKeys, DeepValue, Updater } from './utils'
|
|
4
3
|
import { functionalUpdate, getBy, isNonEmptyArray, setBy } from './utils'
|
|
5
4
|
import type { FieldApi, FieldMeta, ValidationCause } from './FieldApi'
|
|
5
|
+
import type { ValidationError, Validator } from './types'
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
type ValidateFn<TData, ValidatorType> = (
|
|
8
|
+
values: TData,
|
|
9
|
+
formApi: FormApi<TData, ValidatorType>,
|
|
10
|
+
) => ValidationError
|
|
11
|
+
|
|
12
|
+
type ValidateOrFn<TData, ValidatorType> = ValidatorType extends Validator<TData>
|
|
13
|
+
? Parameters<ReturnType<ValidatorType>['validate']>[1]
|
|
14
|
+
: ValidateFn<TData, ValidatorType>
|
|
15
|
+
|
|
16
|
+
type ValidateAsyncFn<TData, ValidatorType> = (
|
|
17
|
+
value: TData,
|
|
18
|
+
fieldApi: FormApi<TData, ValidatorType>,
|
|
19
|
+
) => ValidationError | Promise<ValidationError>
|
|
20
|
+
|
|
21
|
+
export type FormOptions<TData, ValidatorType> = {
|
|
8
22
|
defaultValues?: TData
|
|
9
23
|
defaultState?: Partial<FormState<TData>>
|
|
10
24
|
asyncDebounceMs?: number
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
formApi: FormApi<TData>,
|
|
15
|
-
) => ValidationError | Promise<ValidationError>
|
|
25
|
+
validator?: ValidatorType
|
|
26
|
+
onMount?: ValidateOrFn<TData, ValidatorType>
|
|
27
|
+
onMountAsync?: ValidateAsyncFn<TData, ValidatorType>
|
|
16
28
|
onMountAsyncDebounceMs?: number
|
|
17
|
-
onChange?:
|
|
18
|
-
onChangeAsync?:
|
|
19
|
-
values: TData,
|
|
20
|
-
formApi: FormApi<TData>,
|
|
21
|
-
) => ValidationError | Promise<ValidationError>
|
|
29
|
+
onChange?: ValidateOrFn<TData, ValidatorType>
|
|
30
|
+
onChangeAsync?: ValidateAsyncFn<TData, ValidatorType>
|
|
22
31
|
onChangeAsyncDebounceMs?: number
|
|
23
|
-
onBlur?:
|
|
24
|
-
onBlurAsync?:
|
|
25
|
-
values: TData,
|
|
26
|
-
formApi: FormApi<TData>,
|
|
27
|
-
) => ValidationError | Promise<ValidationError>
|
|
32
|
+
onBlur?: ValidateOrFn<TData, ValidatorType>
|
|
33
|
+
onBlurAsync?: ValidateAsyncFn<TData, ValidatorType>
|
|
28
34
|
onBlurAsyncDebounceMs?: number
|
|
29
|
-
onSubmit?: (
|
|
30
|
-
|
|
35
|
+
onSubmit?: (
|
|
36
|
+
values: TData,
|
|
37
|
+
formApi: FormApi<TData, ValidatorType>,
|
|
38
|
+
) => any | Promise<any>
|
|
39
|
+
onSubmitInvalid?: (
|
|
40
|
+
values: TData,
|
|
41
|
+
formApi: FormApi<TData, ValidatorType>,
|
|
42
|
+
) => void
|
|
31
43
|
}
|
|
32
44
|
|
|
33
|
-
export type FieldInfo<TFormData> = {
|
|
34
|
-
instances: Record<string, FieldApi<TFormData, any,
|
|
45
|
+
export type FieldInfo<TFormData, ValidatorType> = {
|
|
46
|
+
instances: Record<string, FieldApi<TFormData, any, unknown, ValidatorType>>
|
|
35
47
|
} & ValidationMeta
|
|
36
48
|
|
|
37
49
|
export type ValidationMeta = {
|
|
@@ -42,8 +54,6 @@ export type ValidationMeta = {
|
|
|
42
54
|
validationReject?: (errors: unknown) => void
|
|
43
55
|
}
|
|
44
56
|
|
|
45
|
-
export type ValidationError = undefined | false | null | string
|
|
46
|
-
|
|
47
57
|
export type ValidationErrorMapKeys = `on${Capitalize<ValidationCause>}`
|
|
48
58
|
|
|
49
59
|
export type ValidationErrorMap = {
|
|
@@ -92,18 +102,19 @@ function getDefaultFormState<TData>(
|
|
|
92
102
|
}
|
|
93
103
|
}
|
|
94
104
|
|
|
95
|
-
export class FormApi<TFormData> {
|
|
105
|
+
export class FormApi<TFormData, ValidatorType> {
|
|
96
106
|
// // This carries the context for nested fields
|
|
97
|
-
options: FormOptions<TFormData> = {}
|
|
107
|
+
options: FormOptions<TFormData, ValidatorType> = {}
|
|
98
108
|
store!: Store<FormState<TFormData>>
|
|
99
109
|
// Do not use __state directly, as it is not reactive.
|
|
100
110
|
// Please use form.useStore() utility to subscribe to state
|
|
101
111
|
state!: FormState<TFormData>
|
|
102
|
-
fieldInfo: Record<DeepKeys<TFormData>, FieldInfo<TFormData>> =
|
|
112
|
+
fieldInfo: Record<DeepKeys<TFormData>, FieldInfo<TFormData, ValidatorType>> =
|
|
113
|
+
{} as any
|
|
103
114
|
fieldName?: string
|
|
104
115
|
validationMeta: ValidationMeta = {}
|
|
105
116
|
|
|
106
|
-
constructor(opts?: FormOptions<TFormData>) {
|
|
117
|
+
constructor(opts?: FormOptions<TFormData, ValidatorType>) {
|
|
107
118
|
this.store = new Store<FormState<TFormData>>(
|
|
108
119
|
getDefaultFormState({
|
|
109
120
|
...(opts?.defaultState as any),
|
|
@@ -157,7 +168,7 @@ export class FormApi<TFormData> {
|
|
|
157
168
|
this.update(opts || {})
|
|
158
169
|
}
|
|
159
170
|
|
|
160
|
-
update = (options?: FormOptions<TFormData>) => {
|
|
171
|
+
update = (options?: FormOptions<TFormData, ValidatorType>) => {
|
|
161
172
|
if (!options) return
|
|
162
173
|
|
|
163
174
|
this.store.batch(() => {
|
|
@@ -202,21 +213,21 @@ export class FormApi<TFormData> {
|
|
|
202
213
|
validateAllFields = async (cause: ValidationCause) => {
|
|
203
214
|
const fieldValidationPromises: Promise<ValidationError[]>[] = [] as any
|
|
204
215
|
this.store.batch(() => {
|
|
205
|
-
void (
|
|
206
|
-
(
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
)
|
|
216
|
+
void (
|
|
217
|
+
Object.values(this.fieldInfo) as FieldInfo<any, ValidatorType>[]
|
|
218
|
+
).forEach((field) => {
|
|
219
|
+
Object.values(field.instances).forEach((instance) => {
|
|
220
|
+
// If any fields are not touched
|
|
221
|
+
if (!instance.state.meta.isTouched) {
|
|
222
|
+
// Mark them as touched
|
|
223
|
+
instance.setMeta((prev) => ({ ...prev, isTouched: true }))
|
|
224
|
+
// Validate the field
|
|
225
|
+
fieldValidationPromises.push(
|
|
226
|
+
Promise.resolve().then(() => instance.validate(cause)),
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
})
|
|
220
231
|
})
|
|
221
232
|
|
|
222
233
|
return Promise.all(fieldValidationPromises)
|
|
@@ -290,7 +301,7 @@ export class FormApi<TFormData> {
|
|
|
290
301
|
|
|
291
302
|
getFieldInfo = <TField extends DeepKeys<TFormData>>(
|
|
292
303
|
field: TField,
|
|
293
|
-
): FieldInfo<TFormData> => {
|
|
304
|
+
): FieldInfo<TFormData, ValidatorType> => {
|
|
294
305
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
295
306
|
return (this.fieldInfo[field] ||= {
|
|
296
307
|
instances: {},
|
|
@@ -338,7 +349,9 @@ export class FormApi<TFormData> {
|
|
|
338
349
|
|
|
339
350
|
pushFieldValue = <TField extends DeepKeys<TFormData>>(
|
|
340
351
|
field: TField,
|
|
341
|
-
value: DeepValue<TFormData, TField>[
|
|
352
|
+
value: DeepValue<TFormData, TField> extends any[]
|
|
353
|
+
? DeepValue<TFormData, TField>[number]
|
|
354
|
+
: never,
|
|
342
355
|
opts?: { touch?: boolean },
|
|
343
356
|
) => {
|
|
344
357
|
return this.setFieldValue(
|
|
@@ -351,7 +364,9 @@ export class FormApi<TFormData> {
|
|
|
351
364
|
insertFieldValue = <TField extends DeepKeys<TFormData>>(
|
|
352
365
|
field: TField,
|
|
353
366
|
index: number,
|
|
354
|
-
value: DeepValue<TFormData, TField>[
|
|
367
|
+
value: DeepValue<TFormData, TField> extends any[]
|
|
368
|
+
? DeepValue<TFormData, TField>[number]
|
|
369
|
+
: never,
|
|
355
370
|
opts?: { touch?: boolean },
|
|
356
371
|
) => {
|
|
357
372
|
this.setFieldValue(
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,22 @@ describe('field api', () => {
|
|
|
20
20
|
expect(field.getValue()).toBe('test')
|
|
21
21
|
})
|
|
22
22
|
|
|
23
|
+
it('should use field default value first', () => {
|
|
24
|
+
const form = new FormApi({
|
|
25
|
+
defaultValues: {
|
|
26
|
+
name: 'test',
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const field = new FieldApi({
|
|
31
|
+
form,
|
|
32
|
+
defaultValue: 'other',
|
|
33
|
+
name: 'name',
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
expect(field.getValue()).toBe('other')
|
|
37
|
+
})
|
|
38
|
+
|
|
23
39
|
it('should get default meta', () => {
|
|
24
40
|
const form = new FormApi()
|
|
25
41
|
const field = new FieldApi({
|
|
@@ -535,7 +551,7 @@ describe('field api', () => {
|
|
|
535
551
|
interface Form {
|
|
536
552
|
name: string
|
|
537
553
|
}
|
|
538
|
-
const form = new FormApi<Form>()
|
|
554
|
+
const form = new FormApi<Form, unknown>()
|
|
539
555
|
|
|
540
556
|
const field = new FieldApi({
|
|
541
557
|
form,
|
|
@@ -2,24 +2,21 @@ import { assertType } from 'vitest'
|
|
|
2
2
|
import { FormApi } from '../FormApi'
|
|
3
3
|
import { FieldApi } from '../FieldApi'
|
|
4
4
|
|
|
5
|
-
it('should type
|
|
5
|
+
it('should type value properly', () => {
|
|
6
6
|
const form = new FormApi({
|
|
7
7
|
defaultValues: {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
},
|
|
12
|
-
} as const,
|
|
13
|
-
})
|
|
8
|
+
name: 'test',
|
|
9
|
+
},
|
|
10
|
+
} as const)
|
|
14
11
|
|
|
15
12
|
const field = new FieldApi({
|
|
16
13
|
form,
|
|
17
|
-
name: '
|
|
14
|
+
name: 'name',
|
|
18
15
|
})
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
assertType<'
|
|
17
|
+
assertType<'test'>(field.state.value)
|
|
18
|
+
assertType<'name'>(field.options.name)
|
|
19
|
+
assertType<'test'>(field.getValue())
|
|
23
20
|
})
|
|
24
21
|
|
|
25
22
|
it('should type onChange properly', () => {
|
|
@@ -39,3 +36,21 @@ it('should type onChange properly', () => {
|
|
|
39
36
|
},
|
|
40
37
|
})
|
|
41
38
|
})
|
|
39
|
+
|
|
40
|
+
it('should type onChangeAsync properly', () => {
|
|
41
|
+
const form = new FormApi({
|
|
42
|
+
defaultValues: {
|
|
43
|
+
name: 'test',
|
|
44
|
+
},
|
|
45
|
+
} as const)
|
|
46
|
+
|
|
47
|
+
const field = new FieldApi({
|
|
48
|
+
form,
|
|
49
|
+
name: 'name',
|
|
50
|
+
onChangeAsync: async (value) => {
|
|
51
|
+
assertType<'test'>(value)
|
|
52
|
+
|
|
53
|
+
return undefined
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
})
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type ValidationError = undefined | false | null | string
|
|
2
|
+
|
|
3
|
+
// If/when TypeScript supports higher-kinded types, this should not be `unknown` anymore
|
|
4
|
+
export type Validator<Type, Fn = unknown> = () => {
|
|
5
|
+
validate(value: Type, fn: Fn): ValidationError
|
|
6
|
+
validateAsync(value: Type, fn: Fn): Promise<ValidationError>
|
|
7
|
+
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
var __accessCheck = (obj, member, msg) => {
|
|
2
|
-
if (!member.has(obj))
|
|
3
|
-
throw TypeError("Cannot " + msg);
|
|
4
|
-
};
|
|
5
|
-
var __privateGet = (obj, member, getter) => {
|
|
6
|
-
__accessCheck(obj, member, "read from private field");
|
|
7
|
-
return getter ? getter.call(obj) : member.get(obj);
|
|
8
|
-
};
|
|
9
|
-
var __privateAdd = (obj, member, value) => {
|
|
10
|
-
if (member.has(obj))
|
|
11
|
-
throw TypeError("Cannot add the same private member more than once");
|
|
12
|
-
member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export {
|
|
16
|
-
__privateGet,
|
|
17
|
-
__privateAdd
|
|
18
|
-
};
|
|
19
|
-
//# sourceMappingURL=chunk-4QZDOMDG.js.map
|
|
File without changes
|