Skip to content

Commit 6bbed2c

Browse files
committed
fix: flatten errors consistently when validating before field mount
1 parent 38f2e5d commit 6bbed2c

File tree

3 files changed

+87
-1
lines changed

3 files changed

+87
-1
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@tanstack/form-core': patch
3+
'@tanstack/react-form': patch
4+
'@tanstack/angular-form': patch
5+
'@tanstack/vue-form': patch
6+
'@tanstack/solid-form': patch
7+
---
8+
9+
fix: flatten errors consistently when validating before field mount
10+
11+
Fixed an issue where `field.errors` was incorrectly nested as `[[error]]` instead of `[error]` when `form.validate()` was called manually before a field was mounted. The `flat(1)` operation is now applied by default unless `disableErrorFlat` is explicitly set to true, ensuring consistent error structure regardless of when validation occurs.

packages/form-core/src/FormApi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1083,7 +1083,7 @@ export class FormApi<
10831083
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
10841084
const fieldInstance = this.getFieldInfo(fieldName)?.instance
10851085

1086-
if (fieldInstance && !fieldInstance.options.disableErrorFlat) {
1086+
if (!fieldInstance || !fieldInstance.options.disableErrorFlat) {
10871087
fieldErrors = fieldErrors.flat(1)
10881088
}
10891089
}

packages/form-core/tests/FieldApi.spec.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2638,6 +2638,81 @@ describe('field api', () => {
26382638
expect(field3.state.meta.errors).toContain('Field 3 error')
26392639
vi.useRealTimers()
26402640
})
2641+
2642+
it('should flatten errors when manually calling form.validate() before field mount', async () => {
2643+
vi.useFakeTimers()
2644+
const form = new FormApi({
2645+
defaultValues: {
2646+
name: '',
2647+
},
2648+
validators: {
2649+
onChange: ({ value }) => {
2650+
if (!value.name) {
2651+
return {
2652+
fields: {
2653+
name: 'Name is required',
2654+
},
2655+
}
2656+
}
2657+
return undefined
2658+
},
2659+
},
2660+
})
2661+
2662+
form.mount()
2663+
2664+
// Manually validate BEFORE field mount
2665+
await form.validate('change')
2666+
2667+
// Now mount the field
2668+
const field = new FieldApi({ form, name: 'name' })
2669+
field.mount()
2670+
2671+
// Errors should be flattened [error], not [[error]]
2672+
expect(field.state.meta.errors).toEqual(['Name is required'])
2673+
2674+
vi.useRealTimers()
2675+
})
2676+
2677+
it('should respect disableErrorFlat option for mounted fields', async () => {
2678+
vi.useFakeTimers()
2679+
const form = new FormApi({
2680+
defaultValues: {
2681+
name: '',
2682+
},
2683+
validators: {
2684+
onChange: ({ value }) => {
2685+
if (!value.name) {
2686+
return {
2687+
fields: {
2688+
name: [['Error level 1', 'Error level 2']],
2689+
},
2690+
}
2691+
}
2692+
return undefined
2693+
},
2694+
},
2695+
})
2696+
2697+
form.mount()
2698+
2699+
// Mount field with disableErrorFlat: true FIRST
2700+
const field = new FieldApi({
2701+
form,
2702+
name: 'name',
2703+
disableErrorFlat: true,
2704+
})
2705+
field.mount()
2706+
2707+
// Trigger validation after mount
2708+
field.setValue('')
2709+
await vi.advanceTimersByTimeAsync(50)
2710+
2711+
// Errors should NOT be flattened when disableErrorFlat is true
2712+
expect(field.state.meta.errors).toEqual([[['Error level 1', 'Error level 2']]])
2713+
2714+
vi.useRealTimers()
2715+
})
26412716
})
26422717

26432718
describe('deleteField functionality', () => {

0 commit comments

Comments
 (0)