@tanstack/form-core 0.13.7 → 0.16.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.
@@ -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'
@@ -46,6 +46,8 @@ describe('field api', () => {
46
46
  expect(field.getMeta()).toEqual({
47
47
  isTouched: false,
48
48
  isValidating: false,
49
+ isPristine: true,
50
+ isDirty: false,
49
51
  touchedErrors: [],
50
52
  errors: [],
51
53
  errorMap: {},
@@ -57,12 +59,14 @@ describe('field api', () => {
57
59
  const field = new FieldApi({
58
60
  form,
59
61
  name: 'name',
60
- defaultMeta: { isTouched: true },
62
+ defaultMeta: { isTouched: true, isDirty: true, isPristine: false },
61
63
  })
62
64
 
63
65
  expect(field.getMeta()).toEqual({
64
66
  isTouched: true,
65
67
  isValidating: false,
68
+ isDirty: true,
69
+ isPristine: false,
66
70
  touchedErrors: [],
67
71
  errors: [],
68
72
  errorMap: {},
@@ -154,26 +158,6 @@ describe('field api', () => {
154
158
  expect(field.getValue()).toStrictEqual(['two', 'one'])
155
159
  })
156
160
 
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
161
  it('should not throw errors when no meta info is stored on a field and a form re-renders', async () => {
178
162
  const form = new FormApi({
179
163
  defaultValues: {
@@ -619,7 +603,7 @@ describe('field api', () => {
619
603
  })
620
604
 
621
605
  const unmount = field.mount()
622
- const callback = vitest.fn()
606
+ const callback = vi.fn()
623
607
  const subscription = form.store.subscribe(callback)
624
608
  unmount()
625
609
  const info = form.getFieldInfo(field.name)
@@ -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'
@@ -21,6 +21,8 @@ describe('form api', () => {
21
21
  errorMap: {},
22
22
  isSubmitting: false,
23
23
  isTouched: false,
24
+ isPristine: true,
25
+ isDirty: false,
24
26
  isValid: true,
25
27
  isValidating: false,
26
28
  submissionAttempts: 0,
@@ -55,6 +57,8 @@ describe('form api', () => {
55
57
  isSubmitted: false,
56
58
  isSubmitting: false,
57
59
  isTouched: false,
60
+ isPristine: true,
61
+ isDirty: false,
58
62
  isValid: true,
59
63
  isValidating: false,
60
64
  submissionAttempts: 0,
@@ -87,6 +91,8 @@ describe('form api', () => {
87
91
  isSubmitted: false,
88
92
  isSubmitting: false,
89
93
  isTouched: false,
94
+ isPristine: true,
95
+ isDirty: false,
90
96
  isValid: true,
91
97
  isValidating: false,
92
98
  submissionAttempts: 30,
@@ -130,6 +136,8 @@ describe('form api', () => {
130
136
  isSubmitted: false,
131
137
  isSubmitting: false,
132
138
  isTouched: false,
139
+ isPristine: true,
140
+ isDirty: false,
133
141
  isValid: true,
134
142
  isValidating: false,
135
143
  submissionAttempts: 300,
@@ -170,6 +178,8 @@ describe('form api', () => {
170
178
  isSubmitted: false,
171
179
  isSubmitting: false,
172
180
  isTouched: false,
181
+ isPristine: true,
182
+ isDirty: false,
173
183
  isValid: true,
174
184
  isValidating: false,
175
185
  submissionAttempts: 0,
@@ -204,6 +214,40 @@ describe('form api', () => {
204
214
  expect(form.getFieldValue('name')).toEqual('other')
205
215
  })
206
216
 
217
+ it("should be dirty after a field's value has been set", () => {
218
+ const form = new FormApi({
219
+ defaultValues: {
220
+ name: 'test',
221
+ },
222
+ })
223
+ form.mount()
224
+
225
+ form.setFieldValue('name', 'other', { touch: true })
226
+
227
+ expect(form.state.isDirty).toBe(true)
228
+ expect(form.state.isPristine).toBe(false)
229
+ })
230
+
231
+ it('should be clean again after being reset from a dirty state', () => {
232
+ const form = new FormApi({
233
+ defaultValues: {
234
+ name: 'test',
235
+ },
236
+ })
237
+ form.mount()
238
+
239
+ form.setFieldMeta('name', (meta) => ({
240
+ ...meta,
241
+ isDirty: true,
242
+ isPristine: false,
243
+ }))
244
+
245
+ form.reset()
246
+
247
+ expect(form.state.isDirty).toBe(false)
248
+ expect(form.state.isPristine).toBe(true)
249
+ })
250
+
207
251
  it("should push an array field's value", () => {
208
252
  const form = new FormApi({
209
253
  defaultValues: {
@@ -272,7 +316,7 @@ describe('form api', () => {
272
316
 
273
317
  const fieldInArray = new FieldApi({
274
318
  form,
275
- name: `employees.${0}.firstName`,
319
+ name: `employees[0].firstName`,
276
320
  defaultValue: 'Darcy',
277
321
  })
278
322
  fieldInArray.mount()
@@ -300,11 +344,11 @@ describe('form api', () => {
300
344
 
301
345
  const fieldInArray = new FieldApi({
302
346
  form,
303
- name: `employees.${0}.firstName`,
347
+ name: `employees[0].firstName`,
304
348
  defaultValue: 'Darcy',
305
349
  })
306
350
  fieldInArray.mount()
307
- form.deleteField(`employees.${0}.firstName`)
351
+ form.deleteField(`employees[0].firstName`)
308
352
  expect(field.state.value.length).toBe(1)
309
353
  expect(Object.keys(field.state.value[0]!).length).toBe(0)
310
354
  })
@@ -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] } & {}