Skip to content
11 changes: 9 additions & 2 deletions packages/form-core/src/FormApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1915,11 +1915,18 @@ export class FormApi<
)
})

if (!this.state.canSubmit) return

const submitMetaArg =
submitMeta ?? (this.options.onSubmitMeta as TSubmitMeta)

if (!this.state.canSubmit) {
this.options.onSubmitInvalid?.({
value: this.state.values,
formApi: this,
meta: submitMetaArg,
})
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this ensures onSubmitInvalid gets called, it happens before the submit validation. This means the formApi will have a stale error state when the callback is triggered.

}

this.baseStore.setState((d) => ({ ...d, isSubmitting: true }))

const done = () => {
Expand Down
26 changes: 26 additions & 0 deletions packages/form-core/tests/FormApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3156,6 +3156,32 @@ describe('form api', () => {
await form.handleSubmit()
})

it('should call onSubmitInvalid when submitting while canSubmit is false (e.g., onMount error present)', async () => {
const onInvalid = vi.fn()

const form = new FormApi({
defaultValues: { name: '' },
validators: {
onMount: ({ value }) => (!value.name ? 'Name required' : undefined),
},
onSubmitInvalid: ({ value, formApi }) => {
onInvalid(value, formApi)
},
})

form.mount()

// Mount a field to participate in touched/dirty state
new FieldApi({ form, name: 'name' }).mount()

// With an onMount error present, the form is invalid and cannot submit
expect(form.state.canSubmit).toBe(false)

await form.handleSubmit()

expect(onInvalid).toHaveBeenCalledTimes(1)
})

it('should pass the handleSubmit default meta data to onSubmitInvalid', async () => {
const form = new FormApi({
onSubmitMeta: { dinosaur: 'Frank' } as { dinosaur: string },
Expand Down