@tanstack/form-core 0.13.6 → 0.14.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/src/FormApi.ts CHANGED
@@ -8,7 +8,8 @@ import {
8
8
  isNonEmptyArray,
9
9
  setBy,
10
10
  } from './utils'
11
- import type { DeepKeys, DeepValue, Updater } from './utils'
11
+ import type { Updater } from './utils'
12
+ import type { DeepKeys, DeepValue } from './util-types'
12
13
  import type { FieldApi, FieldMeta } from './FieldApi'
13
14
  import type {
14
15
  ValidationCause,
@@ -104,15 +105,12 @@ export type FieldInfo<
104
105
  TFormData,
105
106
  TFormValidator extends Validator<TFormData, unknown> | undefined = undefined,
106
107
  > = {
107
- instances: Record<
108
- string,
109
- FieldApi<
110
- TFormData,
111
- any,
112
- Validator<unknown, unknown> | undefined,
113
- TFormValidator
114
- >
115
- >
108
+ instance: FieldApi<
109
+ TFormData,
110
+ any,
111
+ Validator<unknown, unknown> | undefined,
112
+ TFormValidator
113
+ > | null
116
114
  validationMetaMap: Record<ValidationErrorMapKeys, ValidationMeta | undefined>
117
115
  }
118
116
 
@@ -314,7 +312,9 @@ export class FormApi<
314
312
  Object.assign(
315
313
  {},
316
314
  this.state as any,
315
+
317
316
  shouldUpdateState ? options.defaultState : {},
317
+
318
318
  shouldUpdateValues
319
319
  ? {
320
320
  values: options.defaultValues,
@@ -340,17 +340,17 @@ export class FormApi<
340
340
  void (
341
341
  Object.values(this.fieldInfo) as FieldInfo<any, TFormValidator>[]
342
342
  ).forEach((field) => {
343
- Object.values(field.instances).forEach((instance) => {
344
- // Validate the field
345
- fieldValidationPromises.push(
346
- Promise.resolve().then(() => instance.validate(cause)),
347
- )
348
- // If any fields are not touched
349
- if (!instance.state.meta.isTouched) {
350
- // Mark them as touched
351
- instance.setMeta((prev) => ({ ...prev, isTouched: true }))
352
- }
353
- })
343
+ if (!field.instance) return
344
+ const fieldInstance = field.instance
345
+ // Validate the field
346
+ fieldValidationPromises.push(
347
+ Promise.resolve().then(() => fieldInstance.validate(cause)),
348
+ )
349
+ // If any fields are not touched
350
+ if (!field.instance.state.meta.isTouched) {
351
+ // Mark them as touched
352
+ field.instance.setMeta((prev) => ({ ...prev, isTouched: true }))
353
+ }
354
354
  })
355
355
  })
356
356
 
@@ -587,7 +587,7 @@ export class FormApi<
587
587
  ): FieldInfo<TFormData, TFormValidator> => {
588
588
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
589
589
  return (this.fieldInfo[field] ||= {
590
- instances: {},
590
+ instance: null,
591
591
  validationMetaMap: {
592
592
  onChange: undefined,
593
593
  onBlur: undefined,
@@ -645,6 +645,7 @@ export class FormApi<
645
645
 
646
646
  return newState
647
647
  })
648
+ delete this.fieldInfo[field]
648
649
  }
649
650
 
650
651
  pushFieldValue = <TField extends DeepKeys<TFormData>>(
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './FormApi'
2
2
  export * from './FieldApi'
3
3
  export * from './utils'
4
+ export * from './util-types'
4
5
  export * from './types'
5
6
  export * from './mergeForm'
@@ -1,4 +1,4 @@
1
- import { expect, vitest } from 'vitest'
1
+ import { describe, expect, it, vi } from 'vitest'
2
2
 
3
3
  import { FormApi } from '../FormApi'
4
4
  import { FieldApi } from '../FieldApi'
@@ -154,26 +154,6 @@ describe('field api', () => {
154
154
  expect(field.getValue()).toStrictEqual(['two', 'one'])
155
155
  })
156
156
 
157
- it('should get a subfield properly', () => {
158
- const form = new FormApi({
159
- defaultValues: {
160
- names: {
161
- first: 'one',
162
- second: 'two',
163
- },
164
- },
165
- })
166
-
167
- const field = new FieldApi({
168
- form,
169
- name: 'names',
170
- })
171
-
172
- const subfield = field.getSubField('first')
173
-
174
- expect(subfield.getValue()).toBe('one')
175
- })
176
-
177
157
  it('should not throw errors when no meta info is stored on a field and a form re-renders', async () => {
178
158
  const form = new FormApi({
179
159
  defaultValues: {
@@ -602,7 +582,7 @@ describe('field api', () => {
602
582
 
603
583
  const unmount = field.mount()
604
584
  unmount()
605
- expect(form.getFieldInfo(field.name).instances[field.uid]).toBeDefined()
585
+ expect(form.getFieldInfo(field.name).instance).toBeDefined()
606
586
  expect(form.getFieldInfo(field.name)).toBeDefined()
607
587
  })
608
588
 
@@ -619,13 +599,13 @@ describe('field api', () => {
619
599
  })
620
600
 
621
601
  const unmount = field.mount()
622
- const callback = vitest.fn()
602
+ const callback = vi.fn()
623
603
  const subscription = form.store.subscribe(callback)
624
604
  unmount()
625
605
  const info = form.getFieldInfo(field.name)
626
606
  subscription()
627
- expect(info.instances[field.uid]).toBeUndefined()
628
- expect(Object.keys(info.instances).length).toBe(0)
607
+ expect(info.instance).toBeNull()
608
+ expect(Object.keys(info.instance ?? {}).length).toBe(0)
629
609
 
630
610
  // Check that form store has been updated
631
611
  expect(callback).toHaveBeenCalledOnce()
@@ -1,4 +1,4 @@
1
- import { assertType } from 'vitest'
1
+ import { assertType, it } from 'vitest'
2
2
  import { FormApi } from '../FormApi'
3
3
  import { FieldApi } from '../FieldApi'
4
4
 
@@ -1,4 +1,4 @@
1
- import { expect } from 'vitest'
1
+ import { describe, expect, it, vi } from 'vitest'
2
2
 
3
3
  import { FormApi } from '../FormApi'
4
4
  import { FieldApi } from '../FieldApi'
@@ -272,7 +272,7 @@ describe('form api', () => {
272
272
 
273
273
  const fieldInArray = new FieldApi({
274
274
  form,
275
- name: `employees.${0}.firstName`,
275
+ name: `employees[0].firstName`,
276
276
  defaultValue: 'Darcy',
277
277
  })
278
278
  fieldInArray.mount()
@@ -300,11 +300,11 @@ describe('form api', () => {
300
300
 
301
301
  const fieldInArray = new FieldApi({
302
302
  form,
303
- name: `employees.${0}.firstName`,
303
+ name: `employees[0].firstName`,
304
304
  defaultValue: 'Darcy',
305
305
  })
306
306
  fieldInArray.mount()
307
- form.deleteField(`employees.${0}.firstName`)
307
+ form.deleteField(`employees[0].firstName`)
308
308
  expect(field.state.value.length).toBe(1)
309
309
  expect(Object.keys(field.state.value[0]!).length).toBe(0)
310
310
  })
@@ -1,4 +1,4 @@
1
- import { describe } from 'vitest'
1
+ import { describe, expect, test } from 'vitest'
2
2
  import { mutateMergeDeep } from '../mergeForm'
3
3
 
4
4
  describe('mutateMergeDeep', () => {
@@ -0,0 +1,106 @@
1
+ import { assertType } from 'vitest'
2
+ import type { DeepKeys, DeepValue } from '../util-types'
3
+
4
+ /**
5
+ * Properly recognizes that `0` is not an object and should not have subkeys
6
+ */
7
+ type TupleSupport = DeepKeys<{ topUsers: [User, 0, User] }>
8
+ assertType<
9
+ | 'topUsers'
10
+ | 'topUsers[0]'
11
+ | 'topUsers[0].name'
12
+ | 'topUsers[0].id'
13
+ | 'topUsers[0].age'
14
+ | 'topUsers[1]'
15
+ | 'topUsers[2]'
16
+ | 'topUsers[2].name'
17
+ | 'topUsers[2].id'
18
+ | 'topUsers[2].age'
19
+ >(0 as never as TupleSupport)
20
+
21
+ /**
22
+ * Properly recognizes that a normal number index won't cut it and should be `[number]` prefixed instead
23
+ */
24
+ type ArraySupport = DeepKeys<{ users: User[] }>
25
+ assertType<
26
+ | 'users'
27
+ | `users[${number}].name`
28
+ | `users[${number}].id`
29
+ | `users[${number}].age`
30
+ >(0 as never as ArraySupport)
31
+
32
+ /**
33
+ * Properly handles deep object nesting like so:
34
+ */
35
+ type NestedSupport = DeepKeys<{ meta: { mainUser: User } }>
36
+ assertType<
37
+ | 'meta'
38
+ | 'meta.mainUser'
39
+ | 'meta.mainUser.name'
40
+ | 'meta.mainUser.id'
41
+ | 'meta.mainUser.age'
42
+ >(0 as never as NestedSupport)
43
+
44
+ /**
45
+ * Properly handles `object` edgecase nesting like so:
46
+ */
47
+ type ObjectNestedEdgecase = DeepKeys<{ meta: { mainUser: object } }>
48
+ assertType<'meta' | 'meta.mainUser' | `meta.mainUser.${string}`>(
49
+ 0 as never as ObjectNestedEdgecase,
50
+ )
51
+
52
+ /**
53
+ * Properly handles `object` edgecase like so:
54
+ */
55
+ type ObjectEdgecase = DeepKeys<object>
56
+ assertType<string>(0 as never as ObjectEdgecase)
57
+
58
+ /**
59
+ * Properly handles `object` edgecase nesting like so:
60
+ */
61
+ type UnknownNestedEdgecase = DeepKeys<{ meta: { mainUser: unknown } }>
62
+ assertType<'meta' | 'meta.mainUser' | `meta.mainUser.${string}`>(
63
+ 0 as never as UnknownNestedEdgecase,
64
+ )
65
+
66
+ /**
67
+ * Properly handles `object` edgecase like so:
68
+ */
69
+ type UnknownEdgecase = DeepKeys<unknown>
70
+ assertType<string>(0 as never as UnknownEdgecase)
71
+
72
+ type NestedKeysExample = DeepValue<
73
+ { meta: { mainUser: User } },
74
+ 'meta.mainUser.age'
75
+ >
76
+ assertType<number>(0 as never as NestedKeysExample)
77
+
78
+ type NestedArrayExample = DeepValue<{ users: User[] }, 'users[0].age'>
79
+ assertType<number>(0 as never as NestedArrayExample)
80
+
81
+ type NestedLooseArrayExample = DeepValue<{ users: User[] }, 'users[number].age'>
82
+ assertType<number>(0 as never as NestedLooseArrayExample)
83
+
84
+ type NestedTupleExample = DeepValue<
85
+ { topUsers: [User, 0, User] },
86
+ 'topUsers[0].age'
87
+ >
88
+ assertType<number>(0 as never as NestedTupleExample)
89
+
90
+ type NestedTupleItemExample = DeepValue<
91
+ { topUsers: [User, 0, User] },
92
+ 'topUsers[1]'
93
+ >
94
+ assertType<0>(0 as never as NestedTupleItemExample)
95
+
96
+ type ArrayExample = DeepValue<[1, 2, 3], '[1]'>
97
+ assertType<2>(0 as never as ArrayExample)
98
+
99
+ type NonNestedObjExample = DeepValue<{ a: 1 }, 'a'>
100
+ assertType<1>(0 as never as NonNestedObjExample)
101
+
102
+ interface User {
103
+ name: string
104
+ id: string
105
+ age: number
106
+ }
@@ -19,8 +19,8 @@ describe('getBy', () => {
19
19
  })
20
20
 
21
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)
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
24
  })
25
25
  })
26
26
 
@@ -42,10 +42,10 @@ describe('setBy', () => {
42
42
  })
43
43
 
44
44
  it('should set array subfields by path', () => {
45
- expect(setBy(structure, 'kids.0.name', 'Taylor').kids[0].name).toBe(
45
+ expect(setBy(structure, 'kids[0].name', 'Taylor').kids[0].name).toBe(
46
46
  'Taylor',
47
47
  )
48
- expect(setBy(structure, 'kids.0.age', 20).kids[0].age).toBe(20)
48
+ expect(setBy(structure, 'kids[0].age', 20).kids[0].age).toBe(20)
49
49
  })
50
50
  })
51
51
 
@@ -67,14 +67,14 @@ describe('deleteBy', () => {
67
67
  })
68
68
 
69
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()
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
72
  })
73
73
 
74
74
  it('should delete non-existent paths like a noop', () => {
75
75
  expect(deleteBy(structure, 'nonexistent')).toEqual(structure)
76
76
  expect(deleteBy(structure, 'nonexistent.nonexistent')).toEqual(structure)
77
- expect(deleteBy(structure, 'kids.3.name')).toEqual(structure)
78
- expect(deleteBy(structure, 'nonexistent.3.nonexistent')).toEqual(structure)
77
+ expect(deleteBy(structure, 'kids[3].name')).toEqual(structure)
78
+ expect(deleteBy(structure, 'nonexistent[3].nonexistent')).toEqual(structure)
79
79
  })
80
80
  })
@@ -0,0 +1,99 @@
1
+ export type RequiredByKey<T, K extends keyof T> = Omit<T, K> &
2
+ Required<Pick<T, K>>
3
+
4
+ type Narrowable = string | number | bigint | boolean
5
+
6
+ type NarrowRaw<A> =
7
+ | (A extends [] ? [] : never)
8
+ | (A extends Narrowable ? A : never)
9
+ | {
10
+ [K in keyof A]: A[K] extends Function ? A[K] : NarrowRaw<A[K]>
11
+ }
12
+
13
+ export type Narrow<A> = Try<A, [], NarrowRaw<A>>
14
+
15
+ type Try<A1, A2, Catch = never> = A1 extends A2 ? A1 : Catch
16
+
17
+ // Hack to get TypeScript to show simplified types in error messages
18
+ export type Pretty<T> = { [K in keyof T]: T[K] } & {}
19
+
20
+ type ComputeRange<
21
+ N extends number,
22
+ Result extends Array<unknown> = [],
23
+ > = Result['length'] extends N
24
+ ? Result
25
+ : ComputeRange<N, [...Result, Result['length']]>
26
+ type Index40 = ComputeRange<40>[number]
27
+
28
+ // Is this type a tuple?
29
+ type IsTuple<T> = T extends readonly any[] & { length: infer Length }
30
+ ? Length extends Index40
31
+ ? T
32
+ : never
33
+ : never
34
+
35
+ // If this type is a tuple, what indices are allowed?
36
+ type AllowedIndexes<
37
+ Tuple extends ReadonlyArray<any>,
38
+ Keys extends number = never,
39
+ > = Tuple extends readonly []
40
+ ? Keys
41
+ : Tuple extends readonly [infer _, ...infer Tail]
42
+ ? AllowedIndexes<Tail, Keys | Tail['length']>
43
+ : Keys
44
+
45
+ type PrefixArrayAccessor<T extends any[], TDepth extends any[]> = {
46
+ [K in keyof T]: `[${number}]${DeepKeys<T[K], TDepth>}`
47
+ }[number]
48
+
49
+ type PrefixTupleAccessor<
50
+ T extends any[],
51
+ TIndex extends number,
52
+ TDepth extends any[],
53
+ > = {
54
+ [K in TIndex]: `[${K}]` | `[${K}]${DeepKeys<T[K], TDepth>}`
55
+ }[TIndex]
56
+
57
+ type PrefixObjectAccessor<T extends object, TDepth extends any[]> = {
58
+ [K in keyof T]: K extends string | number
59
+ ?
60
+ | PrefixFromDepth<K, TDepth>
61
+ | `${PrefixFromDepth<K, TDepth>}${DeepKeys<T[K], [TDepth]>}`
62
+ : never
63
+ }[keyof T]
64
+
65
+ export type DeepKeys<T, TDepth extends any[] = []> = TDepth['length'] extends 5
66
+ ? never
67
+ : unknown extends T
68
+ ? PrefixFromDepth<string, TDepth>
69
+ : object extends T
70
+ ? PrefixFromDepth<string, TDepth>
71
+ : T extends readonly any[] & IsTuple<T>
72
+ ? PrefixTupleAccessor<T, AllowedIndexes<T>, TDepth>
73
+ : T extends any[]
74
+ ? PrefixArrayAccessor<T, [...TDepth, any]>
75
+ : T extends Date
76
+ ? never
77
+ : T extends object
78
+ ? PrefixObjectAccessor<T, TDepth>
79
+ : never
80
+
81
+ type PrefixFromDepth<
82
+ T extends string | number,
83
+ TDepth extends any[],
84
+ > = TDepth['length'] extends 0 ? T : `.${T}`
85
+
86
+ export type DeepValue<TValue, TAccessor> = TValue extends Record<
87
+ string | number,
88
+ any
89
+ >
90
+ ? TAccessor extends `${infer TBefore}[${infer TBrackets}].${infer TAfter}`
91
+ ? DeepValue<TValue[TBefore][TBrackets], TAfter>
92
+ : TAccessor extends `[${infer TBrackets}]`
93
+ ? DeepValue<TValue, TBrackets>
94
+ : TAccessor extends `${infer TBefore}[${infer TBrackets}]`
95
+ ? DeepValue<TValue[TBefore], TBrackets>
96
+ : TAccessor extends `${infer TBefore}.${infer TAfter}`
97
+ ? DeepValue<TValue[TBefore], TAfter>
98
+ : TValue[TAccessor & string]
99
+ : never
package/src/utils.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ValidationCause, Validator } from './types'
1
+ import type { ValidationCause } from './types'
2
2
  import type { FormValidators } from './FormApi'
3
3
  import type { FieldValidators } from './FieldApi'
4
4
 
@@ -267,75 +267,3 @@ export function getSyncValidatorArray<T>(
267
267
  return [changeValidator, serverValidator] as never
268
268
  }
269
269
  }
270
-
271
- export type RequiredByKey<T, K extends keyof T> = Omit<T, K> &
272
- Required<Pick<T, K>>
273
-
274
- type ComputeRange<
275
- N extends number,
276
- Result extends Array<unknown> = [],
277
- > = Result['length'] extends N
278
- ? Result
279
- : ComputeRange<N, [...Result, Result['length']]>
280
- type Index40 = ComputeRange<40>[number]
281
-
282
- // Is this type a tuple?
283
- type IsTuple<T> = T extends readonly any[] & { length: infer Length }
284
- ? Length extends Index40
285
- ? T
286
- : never
287
- : never
288
-
289
- // If this type is a tuple, what indices are allowed?
290
- type AllowedIndexes<
291
- Tuple extends ReadonlyArray<any>,
292
- Keys extends number = never,
293
- > = Tuple extends readonly []
294
- ? Keys
295
- : Tuple extends readonly [infer _, ...infer Tail]
296
- ? AllowedIndexes<Tail, Keys | Tail['length']>
297
- : Keys
298
-
299
- export type DeepKeys<T, TDepth extends any[] = []> = TDepth['length'] extends 5
300
- ? never
301
- : unknown extends T
302
- ? string
303
- : T extends readonly any[] & IsTuple<T>
304
- ? AllowedIndexes<T> | DeepKeysPrefix<T, AllowedIndexes<T>, TDepth>
305
- : T extends any[]
306
- ? DeepKeysPrefix<T, number, TDepth>
307
- : T extends Date
308
- ? never
309
- : T extends object
310
- ? (keyof T & string) | DeepKeysPrefix<T, keyof T, TDepth>
311
- : never
312
-
313
- type DeepKeysPrefix<
314
- T,
315
- TPrefix,
316
- TDepth extends any[],
317
- > = TPrefix extends keyof T & (number | string)
318
- ? `${TPrefix}.${DeepKeys<T[TPrefix], [...TDepth, any]> & string}`
319
- : never
320
-
321
- export type DeepValue<T, TProp> = T extends Record<string | number, any>
322
- ? TProp extends `${infer TBranch}.${infer TDeepProp}`
323
- ? DeepValue<T[TBranch], TDeepProp>
324
- : T[TProp & string]
325
- : never
326
-
327
- type Narrowable = string | number | bigint | boolean
328
-
329
- type NarrowRaw<A> =
330
- | (A extends [] ? [] : never)
331
- | (A extends Narrowable ? A : never)
332
- | {
333
- [K in keyof A]: A[K] extends Function ? A[K] : NarrowRaw<A[K]>
334
- }
335
-
336
- export type Narrow<A> = Try<A, [], NarrowRaw<A>>
337
-
338
- type Try<A1, A2, Catch = never> = A1 extends A2 ? A1 : Catch
339
-
340
- // Hack to get TypeScript to show simplified types in error messages
341
- export type Pretty<T> = { [K in keyof T]: T[K] } & {}