Skip to content

Commit 2cfe44c

Browse files
Pascalmhjiji-hoon96LeCarbonatorautofix-ci[bot]
authored
fix(form-core): call onSubmitInvalid even when canSubmit is false (#1697)
* fix(form-core): `form.onSubmitInvalid` not called when `canSubmit` is false Fixes #1696 * test: add validation and submission handling tests for invalid form states * test: improve validation and submission handling tests for invalid form states Signed-off-by: Pascal Küsgen <[email protected]> * Create brown-cars-smell.md * ci: apply automated fixes and generate docs * Fix wrong devtools override logic --------- Signed-off-by: Pascal Küsgen <[email protected]> Co-authored-by: JIHOON LEE <[email protected]> Co-authored-by: LeCarbonator <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 22c49dc commit 2cfe44c

File tree

3 files changed

+117
-2
lines changed

3 files changed

+117
-2
lines changed

.changeset/brown-cars-smell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/form-core': patch
3+
---
4+
5+
fix(form-core): call `onSubmitInvalid` even when `canSubmit` is false

packages/form-core/src/FormApi.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2014,11 +2014,18 @@ export class FormApi<
20142014
)
20152015
})
20162016

2017-
if (!this.state.canSubmit && !this._devtoolsSubmissionOverride) return
2018-
20192017
const submitMetaArg =
20202018
submitMeta ?? (this.options.onSubmitMeta as TSubmitMeta)
20212019

2020+
if (!this.state.canSubmit && !this._devtoolsSubmissionOverride) {
2021+
this.options.onSubmitInvalid?.({
2022+
value: this.state.values,
2023+
formApi: this,
2024+
meta: submitMetaArg,
2025+
})
2026+
return
2027+
}
2028+
20222029
this.baseStore.setState((d) => ({ ...d, isSubmitting: true }))
20232030

20242031
const done = () => {

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

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3156,6 +3156,32 @@ describe('form api', () => {
31563156
await form.handleSubmit()
31573157
})
31583158

3159+
it('should call onSubmitInvalid when submitting while canSubmit is false (e.g., onMount error present)', async () => {
3160+
const onInvalid = vi.fn()
3161+
3162+
const form = new FormApi({
3163+
defaultValues: { name: '' },
3164+
validators: {
3165+
onMount: ({ value }) => (!value.name ? 'Name required' : undefined),
3166+
},
3167+
onSubmitInvalid: ({ value, formApi }) => {
3168+
onInvalid(value, formApi)
3169+
},
3170+
})
3171+
3172+
form.mount()
3173+
3174+
// Mount a field to participate in touched/dirty state
3175+
new FieldApi({ form, name: 'name' }).mount()
3176+
3177+
// With an onMount error present, the form is invalid and cannot submit
3178+
expect(form.state.canSubmit).toBe(false)
3179+
3180+
await form.handleSubmit()
3181+
3182+
expect(onInvalid).toHaveBeenCalledTimes(1)
3183+
})
3184+
31593185
it('should pass the handleSubmit default meta data to onSubmitInvalid', async () => {
31603186
const form = new FormApi({
31613187
onSubmitMeta: { dinosaur: 'Frank' } as { dinosaur: string },
@@ -3955,6 +3981,83 @@ it('should accept formId and return it', () => {
39553981
expect(form.formId).toEqual('age')
39563982
})
39573983

3984+
it('should call onSubmitInvalid when submitted with onMount error', async () => {
3985+
const onInvalidSpy = vi.fn()
3986+
3987+
const form = new FormApi({
3988+
defaultValues: { name: '' },
3989+
validators: {
3990+
onMount: () => ({ name: 'Name is required' }),
3991+
},
3992+
onSubmitInvalid: () => onInvalidSpy(),
3993+
})
3994+
form.mount()
3995+
3996+
const field = new FieldApi({ form, name: 'name' })
3997+
field.mount()
3998+
3999+
expect(form.state.canSubmit).toBe(false)
4000+
4001+
await form.handleSubmit()
4002+
4003+
expect(onInvalidSpy).toHaveBeenCalledTimes(1)
4004+
})
4005+
4006+
it('should not run submit validation when canSubmit is false', async () => {
4007+
const onSubmitValidatorSpy = vi
4008+
.fn()
4009+
.mockImplementation(() => 'Submit validation failed')
4010+
const onInvalidSpy = vi.fn()
4011+
4012+
const form = new FormApi({
4013+
defaultValues: { name: '' },
4014+
validators: {
4015+
onMount: () => 'Name required',
4016+
onSubmit: () => onSubmitValidatorSpy,
4017+
},
4018+
onSubmitInvalid: () => onInvalidSpy(),
4019+
})
4020+
form.mount()
4021+
4022+
const field = new FieldApi({ form, name: 'name' })
4023+
field.mount()
4024+
4025+
expect(form.state.canSubmit).toBe(false)
4026+
4027+
await form.handleSubmit()
4028+
4029+
expect(onSubmitValidatorSpy).not.toHaveBeenCalled()
4030+
expect(onInvalidSpy).toHaveBeenCalledTimes(1)
4031+
})
4032+
4033+
it('should respect canSubmitWhenInvalid option and run validation even when canSubmit is false', async () => {
4034+
const onSubmitValidatorSpy = vi
4035+
.fn()
4036+
.mockImplementation(() => 'Submit validation failed')
4037+
const onInvalidSpy = vi.fn()
4038+
4039+
const form = new FormApi({
4040+
defaultValues: { name: '' },
4041+
canSubmitWhenInvalid: true,
4042+
validators: {
4043+
onMount: () => 'Name required',
4044+
onSubmit: () => onSubmitValidatorSpy(),
4045+
},
4046+
onSubmitInvalid: () => onInvalidSpy(),
4047+
})
4048+
form.mount()
4049+
4050+
const field = new FieldApi({ form, name: 'name' })
4051+
field.mount()
4052+
4053+
expect(form.state.canSubmit).toBe(true)
4054+
4055+
await form.handleSubmit()
4056+
4057+
expect(onSubmitValidatorSpy).toHaveBeenCalledTimes(1)
4058+
expect(onInvalidSpy).toHaveBeenCalledTimes(1)
4059+
})
4060+
39584061
it('should generate a formId if not provided', () => {
39594062
const form = new FormApi({
39604063
defaultValues: { age: 0 },

0 commit comments

Comments
 (0)