Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 44 additions & 5 deletions packages/form-core/src/FormApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,10 @@ export type BaseFormState<
* A record of field metadata for each field in the form, not including the derived properties, like `errors` and such
*/
fieldMetaBase: Record<DeepKeys<TFormData>, AnyFieldMetaBase>
/**
* The default values that correspond to the current form state, used for determining isDirty correctly
*/
_stateDefaultValues?: TFormData
/**
* A boolean indicating if the form is currently in the process of being submitted after `handleSubmit` is called.
*
Expand Down Expand Up @@ -814,6 +818,7 @@ function getDefaultFormState<
values: defaultState.values ?? ({} as never),
errorMap: defaultState.errorMap ?? {},
fieldMetaBase: defaultState.fieldMetaBase ?? ({} as never),
_stateDefaultValues: defaultState._stateDefaultValues,
isSubmitted: defaultState.isSubmitted ?? false,
isSubmitting: defaultState.isSubmitting ?? false,
isValidating: defaultState.isValidating ?? false,
Expand Down Expand Up @@ -957,6 +962,7 @@ export class FormApi<
getDefaultFormState({
...(opts?.defaultState as any),
values: opts?.defaultValues ?? opts?.defaultState?.values,
_stateDefaultValues: opts?.defaultValues ?? opts?.defaultState?.values,
isFormValid: true,
}),
)
Expand Down Expand Up @@ -1028,7 +1034,10 @@ export class FormApi<
const isDefaultValue =
evaluate(
curFieldVal,
getBy(this.options.defaultValues, fieldName),
getBy(
currBaseStore._stateDefaultValues ?? this.options.defaultValues,
fieldName,
),
) ||
evaluate(
curFieldVal,
Expand Down Expand Up @@ -1346,6 +1355,7 @@ export class FormApi<
shouldUpdateValues
? {
values: options.defaultValues,
_stateDefaultValues: options.defaultValues,
}
: {},

Expand Down Expand Up @@ -1376,16 +1386,45 @@ export class FormApi<
}
}

this.baseStore.setState(() =>
getDefaultFormState({
this.baseStore.setState((prev) => {
const newState = getDefaultFormState({
...(this.options.defaultState as any),
values:
values ??
this.options.defaultValues ??
this.options.defaultState?.values,
_stateDefaultValues:
values ??
this.options.defaultValues ??
this.options.defaultState?.values,
fieldMetaBase,
}),
)
}) as BaseFormState<
TFormData,
TOnMount,
TOnChange,
TOnChangeAsync,
TOnBlur,
TOnBlurAsync,
TOnSubmit,
TOnSubmitAsync,
TOnDynamic,
TOnDynamicAsync,
TOnServer
>

// If the form is currently submitting, preserve submission-related state
if (prev.isSubmitting) {
return {
...newState,
isSubmitting: prev.isSubmitting,
submissionAttempts: prev.submissionAttempts,
isSubmitted: prev.isSubmitted,
isSubmitSuccessful: prev.isSubmitSuccessful,
}
}

return newState
})
}

/**
Expand Down
139 changes: 139 additions & 0 deletions packages/form-core/tests/reset-during-submit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { describe, expect, it, vi } from 'vitest'
import { FormApi } from '../src/FormApi'
import type { AnyFormApi } from '../src/FormApi'

describe('Form reset during submit', () => {
it('should correctly reset to new default values when called during onSubmit', async () => {
const mockOnSubmit = vi.fn()

const form = new FormApi({
defaultValues: {
checked: true,
},
onSubmit: async ({ formApi }) => {
// Call reset with new default values during onSubmit
formApi.reset({ checked: false })
mockOnSubmit()
},
})

form.mount()

// Simulate user interaction: uncheck the checkbox
form.setFieldValue('checked', false)

// Verify the form is dirty before submit
expect(form.state.values.checked).toBe(false)
expect(form.state.isDirty).toBe(true)

// Submit the form
await form.handleSubmit()

// After reset with new default values, the form should show the new default (false)
// and should not be dirty anymore
expect(form.state.values.checked).toBe(false)
expect(form.state.isDirty).toBe(false)
expect(mockOnSubmit).toHaveBeenCalled()
})

it('should correctly handle isDirty when reset is called with different default values', async () => {
const form = new FormApi({
defaultValues: {
name: 'original',
},
})

form.mount()

// Change the field value
form.setFieldValue('name', 'changed')
expect(form.state.isDirty).toBe(true)

// Reset with new default values during a simulated submit
form.reset({ name: 'new-default' })

// After reset, form should not be dirty and should have the new default
expect(form.state.values.name).toBe('new-default')
expect(form.state.isDirty).toBe(false)

// Now if we change to the old default, it should be dirty
form.setFieldValue('name', 'original')
expect(form.state.isDirty).toBe(true)
})

it('should work correctly when reset is called multiple times', async () => {
const form = new FormApi({
defaultValues: {
value: 1,
},
onSubmit: async ({ formApi }) => {
// First reset
formApi.reset({ value: 2 })
// Second reset
formApi.reset({ value: 3 })
},
})

form.mount()

// Change value to make it dirty
form.setFieldValue('value', 10)
expect(form.state.isDirty).toBe(true)

// Submit
await form.handleSubmit()

// Should have the final reset value and not be dirty
expect(form.state.values.value).toBe(3)
expect(form.state.isDirty).toBe(false)
})

it('should handle reset with defaultState values fallback', async () => {
const form = new FormApi({
defaultState: {
values: {
name: 'from-default-state',
},
},
onSubmit: async ({ formApi }) => {
// Reset without providing values - should fall back to defaultState.values
formApi.reset()
},
})

form.mount()

// Change the field value
form.setFieldValue('name', 'changed')
expect(form.state.isDirty).toBe(true)

// Submit the form
await form.handleSubmit()

// After reset without values, should fall back to defaultState.values
expect(form.state.values.name).toBe('from-default-state')
expect(form.state.isDirty).toBe(false)
})

it('should handle reset with no default values or defaultState', async () => {
const form = new FormApi({
onSubmit: async ({ formApi }) => {
// Reset without providing values and no defaults - should reset to empty
formApi.reset()
},
}) as AnyFormApi

form.mount()

// Change the field value
form.setFieldValue('name', 'some-value')
expect(form.state.isDirty).toBe(true)

// Submit the form
await form.handleSubmit()

// After reset with no defaults, should be empty/undefined
expect(form.state.values.name).toBeUndefined()
expect(form.state.isDirty).toBe(false)
})
})
Loading
Loading