@tanstack/form-core 0.1.3 → 0.3.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.
Files changed (61) hide show
  1. package/package.json +23 -12
  2. package/src/FieldApi.ts +53 -29
  3. package/src/FormApi.ts +14 -7
  4. package/src/tests/FieldApi.spec.ts +84 -17
  5. package/src/utils.ts +4 -0
  6. package/build/lib/FieldApi.cjs +0 -293
  7. package/build/lib/FieldApi.cjs.map +0 -1
  8. package/build/lib/FieldApi.d.ts +0 -95
  9. package/build/lib/FieldApi.d.ts.map +0 -1
  10. package/build/lib/FieldApi.js +0 -291
  11. package/build/lib/FieldApi.js.map +0 -1
  12. package/build/lib/FieldApi.legacy.cjs +0 -293
  13. package/build/lib/FieldApi.legacy.cjs.map +0 -1
  14. package/build/lib/FieldApi.legacy.js +0 -291
  15. package/build/lib/FieldApi.legacy.js.map +0 -1
  16. package/build/lib/FormApi.cjs +0 -239
  17. package/build/lib/FormApi.cjs.map +0 -1
  18. package/build/lib/FormApi.d.ts +0 -78
  19. package/build/lib/FormApi.d.ts.map +0 -1
  20. package/build/lib/FormApi.js +0 -237
  21. package/build/lib/FormApi.js.map +0 -1
  22. package/build/lib/FormApi.legacy.cjs +0 -239
  23. package/build/lib/FormApi.legacy.cjs.map +0 -1
  24. package/build/lib/FormApi.legacy.js +0 -237
  25. package/build/lib/FormApi.legacy.js.map +0 -1
  26. package/build/lib/_virtual/_rollupPluginBabelHelpers.cjs +0 -65
  27. package/build/lib/_virtual/_rollupPluginBabelHelpers.cjs.map +0 -1
  28. package/build/lib/_virtual/_rollupPluginBabelHelpers.js +0 -56
  29. package/build/lib/_virtual/_rollupPluginBabelHelpers.js.map +0 -1
  30. package/build/lib/_virtual/_rollupPluginBabelHelpers.legacy.cjs +0 -65
  31. package/build/lib/_virtual/_rollupPluginBabelHelpers.legacy.cjs.map +0 -1
  32. package/build/lib/_virtual/_rollupPluginBabelHelpers.legacy.js +0 -56
  33. package/build/lib/_virtual/_rollupPluginBabelHelpers.legacy.js.map +0 -1
  34. package/build/lib/index.cjs +0 -14
  35. package/build/lib/index.cjs.map +0 -1
  36. package/build/lib/index.d.ts +0 -4
  37. package/build/lib/index.d.ts.map +0 -1
  38. package/build/lib/index.js +0 -4
  39. package/build/lib/index.js.map +0 -1
  40. package/build/lib/index.legacy.cjs +0 -14
  41. package/build/lib/index.legacy.cjs.map +0 -1
  42. package/build/lib/index.legacy.js +0 -4
  43. package/build/lib/index.legacy.js.map +0 -1
  44. package/build/lib/tests/FieldApi.spec.d.ts +0 -2
  45. package/build/lib/tests/FieldApi.spec.d.ts.map +0 -1
  46. package/build/lib/tests/FieldApi.test-d.d.ts +0 -2
  47. package/build/lib/tests/FieldApi.test-d.d.ts.map +0 -1
  48. package/build/lib/tests/FormApi.spec.d.ts +0 -2
  49. package/build/lib/tests/FormApi.spec.d.ts.map +0 -1
  50. package/build/lib/tests/utils.d.ts +0 -2
  51. package/build/lib/tests/utils.d.ts.map +0 -1
  52. package/build/lib/utils.cjs +0 -81
  53. package/build/lib/utils.cjs.map +0 -1
  54. package/build/lib/utils.d.ts +0 -32
  55. package/build/lib/utils.d.ts.map +0 -1
  56. package/build/lib/utils.js +0 -77
  57. package/build/lib/utils.js.map +0 -1
  58. package/build/lib/utils.legacy.cjs +0 -81
  59. package/build/lib/utils.legacy.cjs.map +0 -1
  60. package/build/lib/utils.legacy.js +0 -77
  61. package/build/lib/utils.legacy.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/form-core",
3
- "version": "0.1.3",
3
+ "version": "0.3.0",
4
4
  "description": "Powerful, type-safe, framework agnostic forms.",
5
5
  "author": "tannerlinsley",
6
6
  "license": "MIT",
@@ -11,23 +11,36 @@
11
11
  "url": "https://github.com/sponsors/tannerlinsley"
12
12
  },
13
13
  "type": "module",
14
- "types": "build/lib/index.d.ts",
15
- "main": "build/lib/index.legacy.cjs",
16
- "module": "build/lib/index.legacy.js",
14
+ "types": "build/legacy/index.d.ts",
15
+ "main": "build/legacy/index.cjs",
16
+ "module": "build/legacy/index.js",
17
17
  "exports": {
18
18
  ".": {
19
- "types": "./build/lib/index.d.ts",
20
- "import": "./build/lib/index.js",
21
- "require": "./build/lib/index.cjs",
22
- "default": "./build/lib/index.cjs"
19
+ "import": {
20
+ "types": "./build/modern/index.d.ts",
21
+ "default": "./build/modern/index.js"
22
+ },
23
+ "require": {
24
+ "types": "./build/modern/index.d.cts",
25
+ "default": "./build/modern/index.cjs"
26
+ }
23
27
  },
24
28
  "./package.json": "./package.json"
25
29
  },
26
30
  "sideEffects": false,
27
31
  "files": [
28
- "build/lib/*",
32
+ "build",
29
33
  "src"
30
34
  ],
35
+ "nx": {
36
+ "targets": {
37
+ "test:build": {
38
+ "dependsOn": [
39
+ "build"
40
+ ]
41
+ }
42
+ }
43
+ },
31
44
  "dependencies": {
32
45
  "@tanstack/store": "0.1.3"
33
46
  },
@@ -38,8 +51,6 @@
38
51
  "test:lib": "vitest run --coverage",
39
52
  "test:lib:dev": "pnpm run test:lib --watch",
40
53
  "test:build": "publint --strict",
41
- "build": "pnpm build:rollup && pnpm build:types",
42
- "build:rollup": "rollup --config rollup.config.js",
43
- "build:types": "tsc --emitDeclarationOnly"
54
+ "build": "tsup"
44
55
  }
45
56
  }
package/src/FieldApi.ts CHANGED
@@ -1,8 +1,8 @@
1
- import type { DeepKeys, DeepValue, Updater } from './utils'
2
- import type { FormApi, ValidationError } from './FormApi'
1
+ import { type DeepKeys, type DeepValue, type Updater } from './utils'
2
+ import type { FormApi, ValidationError, ValidationErrorMap } from './FormApi'
3
3
  import { Store } from '@tanstack/store'
4
4
 
5
- export type ValidationCause = 'change' | 'blur' | 'submit'
5
+ export type ValidationCause = 'change' | 'blur' | 'submit' | 'mount'
6
6
 
7
7
  type ValidateFn<TData, TFormData> = (
8
8
  value: TData,
@@ -52,8 +52,9 @@ export type FieldApiOptions<TData, TFormData> = FieldOptions<
52
52
 
53
53
  export type FieldMeta = {
54
54
  isTouched: boolean
55
- touchedError?: ValidationError
56
- error?: ValidationError
55
+ touchedErrors: ValidationError[]
56
+ errors: ValidationError[]
57
+ errorMap: ValidationErrorMap
57
58
  isValidating: boolean
58
59
  }
59
60
 
@@ -110,6 +111,9 @@ export class FieldApi<TData, TFormData> {
110
111
  meta: this._getMeta() ?? {
111
112
  isValidating: false,
112
113
  isTouched: false,
114
+ touchedErrors: [],
115
+ errors: [],
116
+ errorMap: {},
113
117
  ...opts.defaultMeta,
114
118
  },
115
119
  },
@@ -117,9 +121,9 @@ export class FieldApi<TData, TFormData> {
117
121
  onUpdate: () => {
118
122
  const state = this.store.state
119
123
 
120
- state.meta.touchedError = state.meta.isTouched
121
- ? state.meta.error
122
- : undefined
124
+ state.meta.touchedErrors = state.meta.isTouched
125
+ ? state.meta.errors
126
+ : []
123
127
 
124
128
  this.prevState = state
125
129
  this.state = state
@@ -203,6 +207,9 @@ export class FieldApi<TData, TFormData> {
203
207
  ({
204
208
  isValidating: false,
205
209
  isTouched: false,
210
+ touchedErrors: [],
211
+ errors: [],
212
+ errorMap: {},
206
213
  ...this.options.defaultMeta,
207
214
  } as FieldMeta)
208
215
 
@@ -239,7 +246,6 @@ export class FieldApi<TData, TFormData> {
239
246
  const { onChange, onBlur } = this.options
240
247
  const validate =
241
248
  cause === 'submit' ? undefined : cause === 'change' ? onChange : onBlur
242
-
243
249
  if (!validate) return
244
250
 
245
251
  // Use the validationCount for all field instances to
@@ -247,16 +253,20 @@ export class FieldApi<TData, TFormData> {
247
253
  const validationCount = (this.getInfo().validationCount || 0) + 1
248
254
  this.getInfo().validationCount = validationCount
249
255
  const error = normalizeError(validate(value as never, this as never))
250
-
251
- if (this.state.meta.error !== error) {
256
+ const errorMapKey = getErrorMapKey(cause)
257
+ if (error && this.state.meta.errorMap[errorMapKey] !== error) {
252
258
  this.setMeta((prev) => ({
253
259
  ...prev,
254
- error,
260
+ errors: [...prev.errors, error],
261
+ errorMap: {
262
+ ...prev.errorMap,
263
+ [getErrorMapKey(cause)]: error,
264
+ },
255
265
  }))
256
266
  }
257
267
 
258
- // If a sync error is encountered, cancel any async validation
259
- if (this.state.meta.error) {
268
+ // If a sync error is encountered for the errorMapKey (eg. onChange), cancel any async validation
269
+ if (this.state.meta.errorMap[errorMapKey]) {
260
270
  this.cancelValidateAsync()
261
271
  }
262
272
  }
@@ -293,9 +303,7 @@ export class FieldApi<TData, TFormData> {
293
303
  : cause === 'submit'
294
304
  ? onSubmitAsync
295
305
  : onBlurAsync
296
-
297
- if (!validate) return
298
-
306
+ if (!validate) return []
299
307
  const debounceMs =
300
308
  cause === 'submit'
301
309
  ? 0
@@ -328,21 +336,25 @@ export class FieldApi<TData, TFormData> {
328
336
 
329
337
  // Only kick off validation if this validation is the latest attempt
330
338
  if (checkLatest()) {
339
+ const prevErrors = this.getMeta().errors
331
340
  try {
332
341
  const rawError = await validate(value as never, this as never)
333
-
334
342
  if (checkLatest()) {
335
343
  const error = normalizeError(rawError)
336
344
  this.setMeta((prev) => ({
337
345
  ...prev,
338
346
  isValidating: false,
339
- error,
347
+ errors: [...prev.errors, error],
348
+ errorMap: {
349
+ ...prev.errorMap,
350
+ [getErrorMapKey(cause)]: error,
351
+ },
340
352
  }))
341
- this.getInfo().validationResolve?.(error)
353
+ this.getInfo().validationResolve?.([...prevErrors, error])
342
354
  }
343
355
  } catch (error) {
344
356
  if (checkLatest()) {
345
- this.getInfo().validationReject?.(error)
357
+ this.getInfo().validationReject?.([...prevErrors, error])
346
358
  throw error
347
359
  }
348
360
  } finally {
@@ -354,26 +366,25 @@ export class FieldApi<TData, TFormData> {
354
366
  }
355
367
 
356
368
  // Always return the latest validation promise to the caller
357
- return this.getInfo().validationPromise
369
+ return this.getInfo().validationPromise ?? []
358
370
  }
359
371
 
360
372
  validate = (
361
373
  cause: ValidationCause,
362
374
  value?: typeof this._tdata,
363
- ): ValidationError | Promise<ValidationError> => {
375
+ ): ValidationError[] | Promise<ValidationError[]> => {
364
376
  // If the field is pristine and validatePristine is false, do not validate
365
- if (!this.state.meta.isTouched) return
366
-
377
+ if (!this.state.meta.isTouched) return []
367
378
  // Attempt to sync validate first
368
379
  this.validateSync(value, cause)
369
380
 
370
- // If there is an error, return it, do not attempt async validation
371
- if (this.state.meta.error) {
381
+ const errorMapKey = getErrorMapKey(cause)
382
+ // If there is an error mapped to the errorMapKey (eg. onChange, onBlur, onSubmit), return the errors array, do not attempt async validation
383
+ if (this.getMeta().errorMap[errorMapKey]) {
372
384
  if (!this.options.asyncAlways) {
373
- return this.state.meta.error
385
+ return this.state.meta.errors
374
386
  }
375
387
  }
376
-
377
388
  // No error? Attempt async validation
378
389
  return this.validateAsync(value, cause)
379
390
  }
@@ -403,3 +414,16 @@ function normalizeError(rawError?: ValidationError) {
403
414
 
404
415
  return undefined
405
416
  }
417
+
418
+ function getErrorMapKey(cause: ValidationCause) {
419
+ switch (cause) {
420
+ case 'submit':
421
+ return 'onSubmit'
422
+ case 'change':
423
+ return 'onChange'
424
+ case 'blur':
425
+ return 'onBlur'
426
+ case 'mount':
427
+ return 'onMount'
428
+ }
429
+ }
package/src/FormApi.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Store } from '@tanstack/store'
2
2
  //
3
3
  import type { DeepKeys, DeepValue, Updater } from './utils'
4
- import { functionalUpdate, getBy, setBy } from './utils'
4
+ import { functionalUpdate, getBy, isNonEmptyArray, setBy } from './utils'
5
5
  import type { FieldApi, FieldMeta, ValidationCause } from './FieldApi'
6
6
 
7
7
  export type FormOptions<TData> = {
@@ -37,13 +37,19 @@ export type FieldInfo<TFormData> = {
37
37
  export type ValidationMeta = {
38
38
  validationCount?: number
39
39
  validationAsyncCount?: number
40
- validationPromise?: Promise<ValidationError>
41
- validationResolve?: (error: ValidationError) => void
42
- validationReject?: (error: unknown) => void
40
+ validationPromise?: Promise<ValidationError[]>
41
+ validationResolve?: (errors: ValidationError[]) => void
42
+ validationReject?: (errors: unknown) => void
43
43
  }
44
44
 
45
45
  export type ValidationError = undefined | false | null | string
46
46
 
47
+ export type ValidationErrorMapKeys = `on${Capitalize<ValidationCause>}`
48
+
49
+ export type ValidationErrorMap = {
50
+ [K in ValidationErrorMapKeys]?: ValidationError
51
+ }
52
+
47
53
  export type FormState<TData> = {
48
54
  values: TData
49
55
  // Form Validation
@@ -117,7 +123,9 @@ export class FormApi<TFormData> {
117
123
  (field) => field?.isValidating,
118
124
  )
119
125
 
120
- const isFieldsValid = !fieldMetaValues.some((field) => field?.error)
126
+ const isFieldsValid = !fieldMetaValues.some((field) =>
127
+ isNonEmptyArray(field?.errors),
128
+ )
121
129
 
122
130
  const isTouched = fieldMetaValues.some((field) => field?.isTouched)
123
131
 
@@ -192,8 +200,7 @@ export class FormApi<TFormData> {
192
200
  )
193
201
 
194
202
  validateAllFields = async (cause: ValidationCause) => {
195
- const fieldValidationPromises: Promise<ValidationError>[] = [] as any
196
-
203
+ const fieldValidationPromises: Promise<ValidationError[]>[] = [] as any
197
204
  this.store.batch(() => {
198
205
  void (Object.values(this.fieldInfo) as FieldInfo<any>[]).forEach(
199
206
  (field) => {
@@ -30,6 +30,9 @@ describe('field api', () => {
30
30
  expect(field.getMeta()).toEqual({
31
31
  isTouched: false,
32
32
  isValidating: false,
33
+ touchedErrors: [],
34
+ errors: [],
35
+ errorMap: {},
33
36
  })
34
37
  })
35
38
 
@@ -44,6 +47,9 @@ describe('field api', () => {
44
47
  expect(field.getMeta()).toEqual({
45
48
  isTouched: true,
46
49
  isValidating: false,
50
+ touchedErrors: [],
51
+ errors: [],
52
+ errorMap: {},
47
53
  })
48
54
  })
49
55
 
@@ -193,9 +199,12 @@ describe('field api', () => {
193
199
 
194
200
  field.mount()
195
201
 
196
- expect(field.getMeta().error).toBeUndefined()
202
+ expect(field.getMeta().errors.length).toBe(0)
197
203
  field.setValue('other', { touch: true })
198
- expect(field.getMeta().error).toBe('Please enter a different value')
204
+ expect(field.getMeta().errors).toContain('Please enter a different value')
205
+ expect(field.getMeta().errorMap).toMatchObject({
206
+ onChange: 'Please enter a different value',
207
+ })
199
208
  })
200
209
 
201
210
  it('should run async validation onChange', async () => {
@@ -219,10 +228,13 @@ describe('field api', () => {
219
228
 
220
229
  field.mount()
221
230
 
222
- expect(field.getMeta().error).toBeUndefined()
231
+ expect(field.getMeta().errors.length).toBe(0)
223
232
  field.setValue('other', { touch: true })
224
233
  await vi.runAllTimersAsync()
225
- expect(field.getMeta().error).toBe('Please enter a different value')
234
+ expect(field.getMeta().errors).toContain('Please enter a different value')
235
+ expect(field.getMeta().errorMap).toMatchObject({
236
+ onChange: 'Please enter a different value',
237
+ })
226
238
  })
227
239
 
228
240
  it('should run async validation onChange with debounce', async () => {
@@ -248,13 +260,16 @@ describe('field api', () => {
248
260
 
249
261
  field.mount()
250
262
 
251
- expect(field.getMeta().error).toBeUndefined()
263
+ expect(field.getMeta().errors.length).toBe(0)
252
264
  field.setValue('other', { touch: true })
253
265
  field.setValue('other')
254
266
  await vi.runAllTimersAsync()
255
267
  // sleepMock will have been called 2 times without onChangeAsyncDebounceMs
256
268
  expect(sleepMock).toHaveBeenCalledTimes(1)
257
- expect(field.getMeta().error).toBe('Please enter a different value')
269
+ expect(field.getMeta().errors).toContain('Please enter a different value')
270
+ expect(field.getMeta().errorMap).toMatchObject({
271
+ onChange: 'Please enter a different value',
272
+ })
258
273
  })
259
274
 
260
275
  it('should run async validation onChange with asyncDebounceMs', async () => {
@@ -280,13 +295,16 @@ describe('field api', () => {
280
295
 
281
296
  field.mount()
282
297
 
283
- expect(field.getMeta().error).toBeUndefined()
298
+ expect(field.getMeta().errors.length).toBe(0)
284
299
  field.setValue('other', { touch: true })
285
300
  field.setValue('other')
286
301
  await vi.runAllTimersAsync()
287
302
  // sleepMock will have been called 2 times without asyncDebounceMs
288
303
  expect(sleepMock).toHaveBeenCalledTimes(1)
289
- expect(field.getMeta().error).toBe('Please enter a different value')
304
+ expect(field.getMeta().errors).toContain('Please enter a different value')
305
+ expect(field.getMeta().errorMap).toMatchObject({
306
+ onChange: 'Please enter a different value',
307
+ })
290
308
  })
291
309
 
292
310
  it('should run validation onBlur', () => {
@@ -309,7 +327,10 @@ describe('field api', () => {
309
327
 
310
328
  field.setValue('other', { touch: true })
311
329
  field.validate('blur')
312
- expect(field.getMeta().error).toBe('Please enter a different value')
330
+ expect(field.getMeta().errors).toContain('Please enter a different value')
331
+ expect(field.getMeta().errorMap).toMatchObject({
332
+ onBlur: 'Please enter a different value',
333
+ })
313
334
  })
314
335
 
315
336
  it('should run async validation onBlur', async () => {
@@ -333,11 +354,14 @@ describe('field api', () => {
333
354
 
334
355
  field.mount()
335
356
 
336
- expect(field.getMeta().error).toBeUndefined()
357
+ expect(field.getMeta().errors.length).toBe(0)
337
358
  field.setValue('other', { touch: true })
338
359
  field.validate('blur')
339
360
  await vi.runAllTimersAsync()
340
- expect(field.getMeta().error).toBe('Please enter a different value')
361
+ expect(field.getMeta().errors).toContain('Please enter a different value')
362
+ expect(field.getMeta().errorMap).toMatchObject({
363
+ onBlur: 'Please enter a different value',
364
+ })
341
365
  })
342
366
 
343
367
  it('should run async validation onBlur with debounce', async () => {
@@ -363,14 +387,17 @@ describe('field api', () => {
363
387
 
364
388
  field.mount()
365
389
 
366
- expect(field.getMeta().error).toBeUndefined()
390
+ expect(field.getMeta().errors.length).toBe(0)
367
391
  field.setValue('other', { touch: true })
368
392
  field.validate('blur')
369
393
  field.validate('blur')
370
394
  await vi.runAllTimersAsync()
371
395
  // sleepMock will have been called 2 times without onBlurAsyncDebounceMs
372
396
  expect(sleepMock).toHaveBeenCalledTimes(1)
373
- expect(field.getMeta().error).toBe('Please enter a different value')
397
+ expect(field.getMeta().errors).toContain('Please enter a different value')
398
+ expect(field.getMeta().errorMap).toMatchObject({
399
+ onBlur: 'Please enter a different value',
400
+ })
374
401
  })
375
402
 
376
403
  it('should run async validation onBlur with asyncDebounceMs', async () => {
@@ -396,14 +423,17 @@ describe('field api', () => {
396
423
 
397
424
  field.mount()
398
425
 
399
- expect(field.getMeta().error).toBeUndefined()
426
+ expect(field.getMeta().errors.length).toBe(0)
400
427
  field.setValue('other', { touch: true })
401
428
  field.validate('blur')
402
429
  field.validate('blur')
403
430
  await vi.runAllTimersAsync()
404
431
  // sleepMock will have been called 2 times without asyncDebounceMs
405
432
  expect(sleepMock).toHaveBeenCalledTimes(1)
406
- expect(field.getMeta().error).toBe('Please enter a different value')
433
+ expect(field.getMeta().errors).toContain('Please enter a different value')
434
+ expect(field.getMeta().errorMap).toMatchObject({
435
+ onBlur: 'Please enter a different value',
436
+ })
407
437
  })
408
438
 
409
439
  it('should run async validation onSubmit', async () => {
@@ -427,11 +457,48 @@ describe('field api', () => {
427
457
 
428
458
  field.mount()
429
459
 
430
- expect(field.getMeta().error).toBeUndefined()
460
+ expect(field.getMeta().errors.length).toBe(0)
431
461
  field.setValue('other', { touch: true })
432
462
  field.validate('submit')
433
463
  await vi.runAllTimersAsync()
434
- expect(field.getMeta().error).toBe('Please enter a different value')
464
+ expect(field.getMeta().errors).toContain('Please enter a different value')
465
+ expect(field.getMeta().errorMap).toMatchObject({
466
+ onSubmit: 'Please enter a different value',
467
+ })
468
+ })
469
+
470
+ it('should contain multiple errors when running validation onBlur and onChange', () => {
471
+ const form = new FormApi({
472
+ defaultValues: {
473
+ name: 'other',
474
+ },
475
+ })
476
+
477
+ const field = new FieldApi({
478
+ form,
479
+ name: 'name',
480
+ onBlur: (value) => {
481
+ if (value === 'other') return 'Please enter a different value'
482
+ return
483
+ },
484
+ onChange: (value) => {
485
+ if (value === 'other') return 'Please enter a different value'
486
+ return
487
+ },
488
+ })
489
+
490
+ field.mount()
491
+
492
+ field.setValue('other', { touch: true })
493
+ field.validate('blur')
494
+ expect(field.getMeta().errors).toStrictEqual([
495
+ 'Please enter a different value',
496
+ 'Please enter a different value',
497
+ ])
498
+ expect(field.getMeta().errorMap).toEqual({
499
+ onBlur: 'Please enter a different value',
500
+ onChange: 'Please enter a different value',
501
+ })
435
502
  })
436
503
 
437
504
  it('should handle default value on field using state.value', async () => {
package/src/utils.ts CHANGED
@@ -101,6 +101,10 @@ function makePathArray(str: string) {
101
101
  })
102
102
  }
103
103
 
104
+ export function isNonEmptyArray(obj: any) {
105
+ return !(Array.isArray(obj) && obj.length === 0)
106
+ }
107
+
104
108
  export type RequiredByKey<T, K extends keyof T> = Omit<T, K> &
105
109
  Required<Pick<T, K>>
106
110