From bf2755c226782f758f093bfdc98a8b4d2fbe4e5d Mon Sep 17 00:00:00 2001 From: Djordje Stevanovic Date: Thu, 28 Aug 2025 19:39:09 +0200 Subject: [PATCH 1/3] fix(form-core): fix formApi.reset() in form onSubmit --- packages/form-core/src/FormApi.ts | 16 +++- .../tests/reset-during-submit.test.ts | 89 +++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 packages/form-core/tests/reset-during-submit.test.ts diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 78e7fa8a4..8dc3e8446 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -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, 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. * @@ -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, @@ -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, }), ) @@ -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, @@ -1346,6 +1355,7 @@ export class FormApi< shouldUpdateValues ? { values: options.defaultValues, + _stateDefaultValues: options.defaultValues, } : {}, @@ -1383,6 +1393,10 @@ export class FormApi< values ?? this.options.defaultValues ?? this.options.defaultState?.values, + _stateDefaultValues: + values ?? + this.options.defaultValues ?? + this.options.defaultState?.values, fieldMetaBase, }), ) diff --git a/packages/form-core/tests/reset-during-submit.test.ts b/packages/form-core/tests/reset-during-submit.test.ts new file mode 100644 index 000000000..7890b95cc --- /dev/null +++ b/packages/form-core/tests/reset-during-submit.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, it, vi } from 'vitest' +import { FormApi } 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) + }) +}) From 4cc698570f6ce464bffcdd434cf8c0ac441d480c Mon Sep 17 00:00:00 2001 From: Djordje Stevanovic Date: Thu, 28 Aug 2025 22:35:40 +0200 Subject: [PATCH 2/3] fix(form-core): enhance formApi.reset() to preserve submission state during reset --- packages/form-core/src/FormApi.ts | 33 +++- .../reset-submit-meta-properties.test.ts | 175 ++++++++++++++++++ 2 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 packages/form-core/tests/reset-submit-meta-properties.test.ts diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 8dc3e8446..09b3090ff 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1386,8 +1386,8 @@ export class FormApi< } } - this.baseStore.setState(() => - getDefaultFormState({ + this.baseStore.setState((prev) => { + const newState = getDefaultFormState({ ...(this.options.defaultState as any), values: values ?? @@ -1398,8 +1398,33 @@ export class FormApi< 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 + }) } /** diff --git a/packages/form-core/tests/reset-submit-meta-properties.test.ts b/packages/form-core/tests/reset-submit-meta-properties.test.ts new file mode 100644 index 000000000..761619ad9 --- /dev/null +++ b/packages/form-core/tests/reset-submit-meta-properties.test.ts @@ -0,0 +1,175 @@ +import { describe, expect, it, vi } from 'vitest' +import { FormApi } from '../src/FormApi' + +describe('Form reset during submit - meta properties', () => { + it('should preserve isSubmitting state when reset is called during onSubmit', async () => { + let isSubmittingDuringReset: boolean | undefined + let isSubmittingAfterReset: boolean | undefined + + const form = new FormApi({ + defaultValues: { + checked: true, + }, + onSubmit: async ({ formApi }) => { + // Check isSubmitting before reset + isSubmittingDuringReset = formApi.state.isSubmitting + + // Call reset with new default values + formApi.reset({ checked: false }) + + // Check isSubmitting after reset - it should still be true + isSubmittingAfterReset = formApi.state.isSubmitting + }, + }) + + form.mount() + + // Simulate user interaction + form.setFieldValue('checked', false) + + // Submit the form + await form.handleSubmit() + + // During onSubmit, isSubmitting should remain true even after reset + expect(isSubmittingDuringReset).toBe(true) + expect(isSubmittingAfterReset).toBe(true) // This will likely fail with current implementation + + // After submit completes, isSubmitting should be false + expect(form.state.isSubmitting).toBe(false) + expect(form.state.isSubmitted).toBe(true) + expect(form.state.isSubmitSuccessful).toBe(true) + }) + + it('should preserve submissionAttempts when reset is called during onSubmit', async () => { + let submissionAttemptsDuringReset: number | undefined + let submissionAttemptsAfterReset: number | undefined + + const form = new FormApi({ + defaultValues: { + value: 'initial', + }, + onSubmit: async ({ formApi }) => { + submissionAttemptsDuringReset = formApi.state.submissionAttempts + formApi.reset({ value: 'reset' }) + submissionAttemptsAfterReset = formApi.state.submissionAttempts + }, + }) + + form.mount() + + // First submission attempt + form.setFieldValue('value', 'changed') + await form.handleSubmit() + + expect(submissionAttemptsDuringReset).toBe(1) + expect(submissionAttemptsAfterReset).toBe(1) // Should preserve the attempt count + expect(form.state.submissionAttempts).toBe(1) + }) + + it('should not reset isSubmitted when reset is called during onSubmit', async () => { + let isSubmittedDuringReset: boolean | undefined + + const form = new FormApi({ + defaultValues: { + value: 'initial', + }, + onSubmit: async ({ formApi }) => { + formApi.reset({ value: 'reset' }) + // isSubmitted should not be affected by reset during submit + isSubmittedDuringReset = formApi.state.isSubmitted + }, + }) + + form.mount() + form.setFieldValue('value', 'changed') + await form.handleSubmit() + + // After handleSubmit completes, isSubmitted should be true + expect(form.state.isSubmitted).toBe(true) + }) + + it('should handle multiple resets during onSubmit without affecting submission state', async () => { + const submitStates: Array<{ + isSubmitting: boolean + submissionAttempts: number + }> = [] + + const form = new FormApi({ + defaultValues: { + value: 1, + }, + onSubmit: async ({ formApi }) => { + // Capture state before first reset + submitStates.push({ + isSubmitting: formApi.state.isSubmitting, + submissionAttempts: formApi.state.submissionAttempts, + }) + + formApi.reset({ value: 2 }) + + // Capture state after first reset + submitStates.push({ + isSubmitting: formApi.state.isSubmitting, + submissionAttempts: formApi.state.submissionAttempts, + }) + + formApi.reset({ value: 3 }) + + // Capture state after second reset + submitStates.push({ + isSubmitting: formApi.state.isSubmitting, + submissionAttempts: formApi.state.submissionAttempts, + }) + }, + }) + + form.mount() + form.setFieldValue('value', 10) + await form.handleSubmit() + + // All states during submission should show isSubmitting: true and submissionAttempts: 1 + expect(submitStates).toEqual([ + { isSubmitting: true, submissionAttempts: 1 }, + { isSubmitting: true, submissionAttempts: 1 }, + { isSubmitting: true, submissionAttempts: 1 }, + ]) + + // Final state should be completed + expect(form.state.isSubmitting).toBe(false) + expect(form.state.isSubmitted).toBe(true) + expect(form.state.submissionAttempts).toBe(1) + }) + + it('should reset submission state when reset is called outside of submission', async () => { + const form = new FormApi({ + defaultValues: { + value: 'initial', + }, + onSubmit: async () => { + // Do nothing during submit + }, + }) + + form.mount() + form.setFieldValue('value', 'changed') + + // Submit the form first to get submission state + await form.handleSubmit() + + // Verify submission completed + expect(form.state.isSubmitted).toBe(true) + expect(form.state.isSubmitSuccessful).toBe(true) + expect(form.state.submissionAttempts).toBe(1) + + // Now reset outside of submission - should reset all submission state + form.reset({ value: 'reset-value' }) + + // All submission state should be reset to defaults + expect(form.state.values.value).toBe('reset-value') + expect(form.state.isSubmitted).toBe(false) + expect(form.state.isSubmitSuccessful).toBe(false) + expect(form.state.submissionAttempts).toBe(0) + expect(form.state.isSubmitting).toBe(false) + expect(form.state.isDirty).toBe(false) + }) +}) From 3dbed4af226a7d06d54bfe852d087e94546e24c1 Mon Sep 17 00:00:00 2001 From: Djordje Stevanovic Date: Sat, 30 Aug 2025 10:00:51 +0200 Subject: [PATCH 3/3] test(form-core): add tests for formApi.reset() with defaultState and no defaults --- .../tests/reset-during-submit.test.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/form-core/tests/reset-during-submit.test.ts b/packages/form-core/tests/reset-during-submit.test.ts index 7890b95cc..6d23bfbc9 100644 --- a/packages/form-core/tests/reset-during-submit.test.ts +++ b/packages/form-core/tests/reset-during-submit.test.ts @@ -1,5 +1,6 @@ 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 () => { @@ -86,4 +87,53 @@ describe('Form reset during submit', () => { 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) + }) })