@tanstack/form-core 0.0.9 → 0.0.12

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.
Files changed (79) hide show
  1. package/build/lib/FieldApi.cjs +307 -0
  2. package/build/lib/FieldApi.cjs.map +1 -0
  3. package/build/lib/FieldApi.d.ts +112 -0
  4. package/build/lib/FieldApi.d.ts.map +1 -0
  5. package/build/lib/FieldApi.js +305 -0
  6. package/build/lib/FieldApi.js.map +1 -0
  7. package/build/lib/FieldApi.legacy.cjs +307 -0
  8. package/build/lib/FieldApi.legacy.cjs.map +1 -0
  9. package/build/lib/FieldApi.legacy.js +305 -0
  10. package/build/lib/FieldApi.legacy.js.map +1 -0
  11. package/build/lib/FormApi.cjs +248 -0
  12. package/build/lib/FormApi.cjs.map +1 -0
  13. package/build/{types → lib}/FormApi.d.ts +16 -11
  14. package/build/lib/FormApi.d.ts.map +1 -0
  15. package/build/lib/FormApi.js +246 -0
  16. package/build/lib/FormApi.js.map +1 -0
  17. package/build/lib/FormApi.legacy.cjs +248 -0
  18. package/build/lib/FormApi.legacy.cjs.map +1 -0
  19. package/build/lib/FormApi.legacy.js +246 -0
  20. package/build/lib/FormApi.legacy.js.map +1 -0
  21. package/build/lib/_virtual/_rollupPluginBabelHelpers.cjs +65 -0
  22. package/build/lib/_virtual/_rollupPluginBabelHelpers.cjs.map +1 -0
  23. package/build/lib/_virtual/_rollupPluginBabelHelpers.js +56 -0
  24. package/build/{cjs → lib}/_virtual/_rollupPluginBabelHelpers.js.map +1 -1
  25. package/build/lib/_virtual/_rollupPluginBabelHelpers.legacy.cjs +65 -0
  26. package/build/lib/_virtual/_rollupPluginBabelHelpers.legacy.cjs.map +1 -0
  27. package/build/lib/_virtual/_rollupPluginBabelHelpers.legacy.js +56 -0
  28. package/build/lib/_virtual/_rollupPluginBabelHelpers.legacy.js.map +1 -0
  29. package/build/lib/index.cjs +14 -0
  30. package/build/lib/index.cjs.map +1 -0
  31. package/build/{types → lib}/index.d.ts +1 -0
  32. package/build/lib/index.d.ts.map +1 -0
  33. package/build/lib/index.js +4 -0
  34. package/build/{cjs → lib}/index.js.map +1 -1
  35. package/build/lib/index.legacy.cjs +14 -0
  36. package/build/lib/index.legacy.cjs.map +1 -0
  37. package/build/lib/index.legacy.js +4 -0
  38. package/build/lib/index.legacy.js.map +1 -0
  39. package/build/lib/tests/FieldApi.spec.d.ts +2 -0
  40. package/build/lib/tests/FieldApi.spec.d.ts.map +1 -0
  41. package/build/lib/tests/FieldApi.test-d.d.ts +2 -0
  42. package/build/lib/tests/FieldApi.test-d.d.ts.map +1 -0
  43. package/build/lib/tests/FormApi.spec.d.ts +2 -0
  44. package/build/lib/tests/FormApi.spec.d.ts.map +1 -0
  45. package/build/{cjs/utils.js → lib/utils.cjs} +18 -27
  46. package/build/lib/utils.cjs.map +1 -0
  47. package/build/{types → lib}/utils.d.ts +10 -0
  48. package/build/lib/utils.d.ts.map +1 -0
  49. package/build/lib/utils.js +77 -0
  50. package/build/lib/utils.js.map +1 -0
  51. package/build/lib/utils.legacy.cjs +81 -0
  52. package/build/lib/utils.legacy.cjs.map +1 -0
  53. package/build/lib/utils.legacy.js +77 -0
  54. package/build/lib/utils.legacy.js.map +1 -0
  55. package/package.json +23 -10
  56. package/src/FieldApi.ts +190 -138
  57. package/src/FormApi.ts +84 -118
  58. package/src/tests/FieldApi.spec.ts +143 -0
  59. package/src/tests/FieldApi.test-d.ts +41 -0
  60. package/src/tests/FormApi.spec.ts +216 -0
  61. package/src/utils.ts +10 -1
  62. package/build/cjs/FieldApi.js +0 -345
  63. package/build/cjs/FieldApi.js.map +0 -1
  64. package/build/cjs/FormApi.js +0 -317
  65. package/build/cjs/FormApi.js.map +0 -1
  66. package/build/cjs/_virtual/_rollupPluginBabelHelpers.js +0 -31
  67. package/build/cjs/index.js +0 -26
  68. package/build/cjs/utils.js.map +0 -1
  69. package/build/esm/index.js +0 -724
  70. package/build/esm/index.js.map +0 -1
  71. package/build/stats-html.html +0 -2689
  72. package/build/stats-react.json +0 -190
  73. package/build/types/FieldApi.d.ts +0 -79
  74. package/build/types/tests/test.test.d.ts +0 -0
  75. package/build/umd/index.development.js +0 -785
  76. package/build/umd/index.development.js.map +0 -1
  77. package/build/umd/index.production.js +0 -22
  78. package/build/umd/index.production.js.map +0 -1
  79. package/src/tests/test.test.tsx +0 -5
package/src/FormApi.ts CHANGED
@@ -17,13 +17,27 @@ export type FormSubmitEvent = Register extends {
17
17
  export type FormOptions<TData> = {
18
18
  defaultValues?: TData
19
19
  defaultState?: Partial<FormState<TData>>
20
- onSubmit?: (values: TData, formApi: FormApi<TData>) => void
21
- onInvalidSubmit?: (values: TData, formApi: FormApi<TData>) => void
22
- validate?: (values: TData, formApi: FormApi<TData>) => Promise<any>
23
- defaultValidatePristine?: boolean
24
- defaultValidateOn?: ValidationCause
25
- defaultValidateAsyncOn?: ValidationCause
26
- defaultValidateAsyncDebounceMs?: number
20
+ asyncDebounceMs?: number
21
+ onMount?: (values: TData, formApi: FormApi<TData>) => ValidationError
22
+ onMountAsync?: (
23
+ values: TData,
24
+ formApi: FormApi<TData>,
25
+ ) => ValidationError | Promise<ValidationError>
26
+ onMountAsyncDebounceMs?: number
27
+ onChange?: (values: TData, formApi: FormApi<TData>) => ValidationError
28
+ onChangeAsync?: (
29
+ values: TData,
30
+ formApi: FormApi<TData>,
31
+ ) => ValidationError | Promise<ValidationError>
32
+ onChangeAsyncDebounceMs?: number
33
+ onBlur?: (values: TData, formApi: FormApi<TData>) => ValidationError
34
+ onBlurAsync?: (
35
+ values: TData,
36
+ formApi: FormApi<TData>,
37
+ ) => ValidationError | Promise<ValidationError>
38
+ onBlurAsyncDebounceMs?: number
39
+ onSubmit?: (values: TData, formApi: FormApi<TData>) => any | Promise<any>
40
+ onSubmitInvalid?: (values: TData, formApi: FormApi<TData>) => void
27
41
  }
28
42
 
29
43
  export type FieldInfo<TFormData> = {
@@ -99,12 +113,13 @@ export class FormApi<TFormData> {
99
113
  getDefaultFormState({
100
114
  ...opts?.defaultState,
101
115
  values: opts?.defaultValues ?? opts?.defaultState?.values,
102
- isFormValid: !opts?.validate,
116
+ isFormValid: true,
103
117
  }),
104
118
  {
105
- onUpdate: (next) => {
119
+ onUpdate: () => {
120
+ let { state } = this.store
106
121
  // Computed state
107
- const fieldMetaValues = Object.values(next.fieldMeta) as (
122
+ const fieldMetaValues = Object.values(state.fieldMeta) as (
108
123
  | FieldMeta
109
124
  | undefined
110
125
  )[]
@@ -117,15 +132,15 @@ export class FormApi<TFormData> {
117
132
 
118
133
  const isTouched = fieldMetaValues.some((field) => field?.isTouched)
119
134
 
120
- const isValidating = isFieldsValidating || next.isFormValidating
121
- const isFormValid = !next.formError
135
+ const isValidating = isFieldsValidating || state.isFormValidating
136
+ const isFormValid = !state.formError
122
137
  const isValid = isFieldsValid && isFormValid
123
138
  const canSubmit =
124
- (next.submissionAttempts === 0 && !isTouched) ||
125
- (!isValidating && !next.isSubmitting && isValid)
139
+ (state.submissionAttempts === 0 && !isTouched) ||
140
+ (!isValidating && !state.isSubmitting && isValid)
126
141
 
127
- next = {
128
- ...next,
142
+ state = {
143
+ ...state,
129
144
  isFieldsValidating,
130
145
  isFieldsValid,
131
146
  isFormValid,
@@ -134,10 +149,8 @@ export class FormApi<TFormData> {
134
149
  isTouched,
135
150
  }
136
151
 
137
- // Create a shortcut for the state
138
- // Write it back to the store
139
- this.store.state = next
140
- this.state = next
152
+ this.store.state = state
153
+ this.state = state
141
154
  },
142
155
  },
143
156
  )
@@ -151,31 +164,47 @@ export class FormApi<TFormData> {
151
164
  if (!options) return
152
165
 
153
166
  this.store.batch(() => {
154
- if (
155
- options.defaultState &&
167
+ const shouldUpdateValues =
168
+ options.defaultValues &&
169
+ options.defaultValues !== this.options.defaultValues
170
+
171
+ const shouldUpdateState =
156
172
  options.defaultState !== this.options.defaultState
157
- ) {
158
- this.store.setState((prev) => ({
159
- ...prev,
160
- ...options.defaultState,
161
- }))
162
- }
163
173
 
164
- if (options.defaultValues !== this.options.defaultValues) {
165
- this.store.setState((prev) => ({
166
- ...prev,
167
- values: options.defaultValues as TFormData,
168
- }))
174
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
175
+ if (!shouldUpdateValues || !shouldUpdateValues) {
176
+ return
169
177
  }
178
+
179
+ this.store.setState(() =>
180
+ getDefaultFormState(
181
+ Object.assign(
182
+ {},
183
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
184
+ shouldUpdateState ? options.defaultState : {},
185
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
186
+ shouldUpdateValues
187
+ ? {
188
+ values: options.defaultValues,
189
+ }
190
+ : {},
191
+ ),
192
+ ),
193
+ )
170
194
  })
171
195
 
172
196
  this.options = options
173
197
  }
174
198
 
175
199
  reset = () =>
176
- this.store.setState(() => getDefaultFormState(this.options.defaultValues!))
200
+ this.store.setState(() =>
201
+ getDefaultFormState({
202
+ ...this.options.defaultState,
203
+ values: this.options.defaultValues ?? this.options.defaultState?.values,
204
+ }),
205
+ )
177
206
 
178
- validateAllFields = async () => {
207
+ validateAllFields = async (cause: ValidationCause) => {
179
208
  const fieldValidationPromises: Promise<ValidationError>[] = [] as any
180
209
 
181
210
  this.store.batch(() => {
@@ -187,9 +216,9 @@ export class FormApi<TFormData> {
187
216
  // Mark them as touched
188
217
  instance.setMeta((prev) => ({ ...prev, isTouched: true }))
189
218
  // Validate the field
190
- if (instance.options.validate) {
191
- fieldValidationPromises.push(instance.validate())
192
- }
219
+ fieldValidationPromises.push(
220
+ Promise.resolve().then(() => instance.validate(cause)),
221
+ )
193
222
  }
194
223
  })
195
224
  },
@@ -199,63 +228,7 @@ export class FormApi<TFormData> {
199
228
  return Promise.all(fieldValidationPromises)
200
229
  }
201
230
 
202
- validateForm = async () => {
203
- const { validate } = this.options
204
-
205
- if (!validate) {
206
- return
207
- }
208
-
209
- // Use the formValidationCount for all field instances to
210
- // track freshness of the validation
211
- this.store.setState((prev) => ({
212
- ...prev,
213
- isValidating: true,
214
- formValidationCount: prev.formValidationCount + 1,
215
- }))
216
-
217
- const formValidationCount = this.state.formValidationCount
218
-
219
- const checkLatest = () =>
220
- formValidationCount === this.state.formValidationCount
221
-
222
- if (!this.validationMeta.validationPromise) {
223
- this.validationMeta.validationPromise = new Promise((resolve, reject) => {
224
- this.validationMeta.validationResolve = resolve
225
- this.validationMeta.validationReject = reject
226
- })
227
- }
228
-
229
- const doValidation = async () => {
230
- try {
231
- const error = await validate(this.state.values, this)
232
-
233
- if (checkLatest()) {
234
- this.store.setState((prev) => ({
235
- ...prev,
236
- isValidating: false,
237
- error: error
238
- ? typeof error === 'string'
239
- ? error
240
- : 'Invalid Form Values'
241
- : null,
242
- }))
243
-
244
- this.validationMeta.validationResolve?.(error)
245
- }
246
- } catch (err) {
247
- if (checkLatest()) {
248
- this.validationMeta.validationReject?.(err)
249
- }
250
- } finally {
251
- delete this.validationMeta.validationPromise
252
- }
253
- }
254
-
255
- doValidation()
256
-
257
- return this.validationMeta.validationPromise
258
- }
231
+ // validateForm = async () => {}
259
232
 
260
233
  handleSubmit = async (e: FormSubmitEvent) => {
261
234
  e.preventDefault()
@@ -284,21 +257,21 @@ export class FormApi<TFormData> {
284
257
  }
285
258
 
286
259
  // Validate all fields
287
- await this.validateAllFields()
260
+ await this.validateAllFields('submit')
288
261
 
289
262
  // Fields are invalid, do not submit
290
263
  if (!this.state.isFieldsValid) {
291
264
  done()
292
- this.options.onInvalidSubmit?.(this.state.values, this)
265
+ this.options.onSubmitInvalid?.(this.state.values, this)
293
266
  return
294
267
  }
295
268
 
296
269
  // Run validation for the form
297
- await this.validateForm()
270
+ // await this.validateForm()
298
271
 
299
272
  if (!this.state.isValid) {
300
273
  done()
301
- this.options.onInvalidSubmit?.(this.state.values, this)
274
+ this.options.onSubmitInvalid?.(this.state.values, this)
302
275
  return
303
276
  }
304
277
 
@@ -327,6 +300,7 @@ export class FormApi<TFormData> {
327
300
  }
328
301
 
329
302
  getFieldInfo = <TField extends DeepKeys<TFormData>>(field: TField) => {
303
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
330
304
  return (this.fieldInfo[field] ||= {
331
305
  instances: {},
332
306
  })
@@ -352,28 +326,28 @@ export class FormApi<TFormData> {
352
326
  updater: Updater<DeepValue<TFormData, TField>>,
353
327
  opts?: { touch?: boolean },
354
328
  ) => {
355
- const touch = opts?.touch ?? true
329
+ const touch = opts?.touch
356
330
 
357
331
  this.store.batch(() => {
358
- this.store.setState((prev) => {
359
- return {
360
- ...prev,
361
- values: setBy(prev.values, field, updater),
362
- }
363
- })
364
-
365
332
  if (touch) {
366
333
  this.setFieldMeta(field, (prev) => ({
367
334
  ...prev,
368
335
  isTouched: true,
369
336
  }))
370
337
  }
338
+
339
+ this.store.setState((prev) => {
340
+ return {
341
+ ...prev,
342
+ values: setBy(prev.values, field, updater),
343
+ }
344
+ })
371
345
  })
372
346
  }
373
347
 
374
348
  pushFieldValue = <TField extends DeepKeys<TFormData>>(
375
349
  field: TField,
376
- value: DeepValue<TFormData, TField>,
350
+ value: DeepValue<TFormData, TField>[number],
377
351
  opts?: { touch?: boolean },
378
352
  ) => {
379
353
  return this.setFieldValue(
@@ -386,16 +360,12 @@ export class FormApi<TFormData> {
386
360
  insertFieldValue = <TField extends DeepKeys<TFormData>>(
387
361
  field: TField,
388
362
  index: number,
389
- value: DeepValue<TFormData, TField>,
363
+ value: DeepValue<TFormData, TField>[number],
390
364
  opts?: { touch?: boolean },
391
365
  ) => {
392
366
  this.setFieldValue(
393
367
  field,
394
368
  (prev) => {
395
- // invariant( // TODO: bring in invariant
396
- // Array.isArray(prev),
397
- // `Cannot insert a field value into a non-array field. Check that this field's existing value is an array: ${field}.`
398
- // )
399
369
  return (prev as DeepValue<TFormData, TField>[]).map((d, i) =>
400
370
  i === index ? value : d,
401
371
  ) as any
@@ -412,10 +382,6 @@ export class FormApi<TFormData> {
412
382
  this.setFieldValue(
413
383
  field,
414
384
  (prev) => {
415
- // invariant( // TODO: bring in invariant
416
- // Array.isArray(prev),
417
- // `Cannot insert a field value into a non-array field. Check that this field's existing value is an array: ${field}.`
418
- // )
419
385
  return (prev as DeepValue<TFormData, TField>[]).filter(
420
386
  (_d, i) => i !== index,
421
387
  ) as any
@@ -432,7 +398,7 @@ export class FormApi<TFormData> {
432
398
  this.setFieldValue(field, (prev: any) => {
433
399
  const prev1 = prev[index1]!
434
400
  const prev2 = prev[index2]!
435
- return setBy(setBy(prev, [index1], prev2), [index2], prev1)
401
+ return setBy(setBy(prev, `${index1}`, prev2), `${index2}`, prev1)
436
402
  })
437
403
  }
438
404
  }
@@ -0,0 +1,143 @@
1
+ import { expect } from 'vitest'
2
+
3
+ import { FormApi } from '../FormApi'
4
+ import { FieldApi } from '../FieldApi'
5
+
6
+ describe('field api', () => {
7
+ it('should have an initial value', () => {
8
+ const form = new FormApi({
9
+ defaultValues: {
10
+ name: 'test',
11
+ },
12
+ })
13
+
14
+ const field = new FieldApi({
15
+ form,
16
+ name: 'name',
17
+ })
18
+
19
+ expect(field.getValue()).toBe('test')
20
+ })
21
+
22
+ it('should set a value correctly', () => {
23
+ const form = new FormApi({
24
+ defaultValues: {
25
+ name: 'test',
26
+ },
27
+ })
28
+
29
+ const field = new FieldApi({
30
+ form,
31
+ name: 'name',
32
+ })
33
+
34
+ field.setValue('other')
35
+
36
+ expect(field.getValue()).toBe('other')
37
+ })
38
+
39
+ it('should set a value correctly', () => {
40
+ const form = new FormApi({
41
+ defaultValues: {
42
+ name: 'test',
43
+ },
44
+ })
45
+
46
+ const field = new FieldApi({
47
+ form,
48
+ name: 'name',
49
+ })
50
+
51
+ field.setValue('other')
52
+
53
+ expect(field.getValue()).toBe('other')
54
+ })
55
+
56
+ it('should push an array value correctly', () => {
57
+ const form = new FormApi({
58
+ defaultValues: {
59
+ names: ['one'],
60
+ },
61
+ })
62
+
63
+ const field = new FieldApi({
64
+ form,
65
+ name: 'names',
66
+ })
67
+
68
+ field.pushValue('other')
69
+
70
+ expect(field.getValue()).toStrictEqual(['one', 'other'])
71
+ })
72
+
73
+ it('should insert a value into an array value correctly', () => {
74
+ const form = new FormApi({
75
+ defaultValues: {
76
+ names: ['one', 'two'],
77
+ },
78
+ })
79
+
80
+ const field = new FieldApi({
81
+ form,
82
+ name: 'names',
83
+ })
84
+
85
+ field.insertValue(1, 'other')
86
+
87
+ expect(field.getValue()).toStrictEqual(['one', 'other'])
88
+ })
89
+
90
+ it('should remove a value from an array value correctly', () => {
91
+ const form = new FormApi({
92
+ defaultValues: {
93
+ names: ['one', 'two'],
94
+ },
95
+ })
96
+
97
+ const field = new FieldApi({
98
+ form,
99
+ name: 'names',
100
+ })
101
+
102
+ field.removeValue(1)
103
+
104
+ expect(field.getValue()).toStrictEqual(['one'])
105
+ })
106
+
107
+ it('should swap a value from an array value correctly', () => {
108
+ const form = new FormApi({
109
+ defaultValues: {
110
+ names: ['one', 'two'],
111
+ },
112
+ })
113
+
114
+ const field = new FieldApi({
115
+ form,
116
+ name: 'names',
117
+ })
118
+
119
+ field.swapValues(0, 1)
120
+
121
+ expect(field.getValue()).toStrictEqual(['two', 'one'])
122
+ })
123
+
124
+ it('should get a subfield properly', () => {
125
+ const form = new FormApi({
126
+ defaultValues: {
127
+ names: {
128
+ first: 'one',
129
+ second: 'two',
130
+ },
131
+ },
132
+ })
133
+
134
+ const field = new FieldApi({
135
+ form,
136
+ name: 'names',
137
+ })
138
+
139
+ const subfield = field.getSubField('first')
140
+
141
+ expect(subfield.getValue()).toBe('one')
142
+ })
143
+ })
@@ -0,0 +1,41 @@
1
+ import { assertType } from 'vitest'
2
+ import { FormApi } from '../FormApi'
3
+ import { FieldApi } from '../FieldApi'
4
+
5
+ it('should type a subfield properly', () => {
6
+ const form = new FormApi({
7
+ defaultValues: {
8
+ names: {
9
+ first: 'one',
10
+ second: 'two',
11
+ },
12
+ } as const,
13
+ })
14
+
15
+ const field = new FieldApi({
16
+ form,
17
+ name: 'names',
18
+ })
19
+
20
+ const subfield = field.getSubField('first')
21
+
22
+ assertType<'one'>(subfield.getValue())
23
+ })
24
+
25
+ it('should type onChange properly', () => {
26
+ const form = new FormApi({
27
+ defaultValues: {
28
+ name: 'test',
29
+ },
30
+ } as const)
31
+
32
+ const field = new FieldApi({
33
+ form,
34
+ name: 'name',
35
+ onChange: (value) => {
36
+ assertType<'test'>(value)
37
+
38
+ return undefined
39
+ },
40
+ })
41
+ })