diff --git a/packages/angular-form/tests/test.component.spec.ts b/packages/angular-form/tests/test.component.spec.ts index db994a786..feda9ac15 100644 --- a/packages/angular-form/tests/test.component.spec.ts +++ b/packages/angular-form/tests/test.component.spec.ts @@ -363,3 +363,52 @@ describe('TanStackFieldDirective', () => { expect(getByText(onBlurError)).toBeInTheDocument() }) }) + +describe('form should reset default value when resetting in onSubmit', () => { + it('should be able to handle async resets', async () => { + @Component({ + selector: 'test-component', + standalone: true, + template: ` + + + + + `, + imports: [TanStackField], + }) + class TestComponent { + form = injectForm({ + defaultValues: { + name: '', + }, + onSubmit: ({ value }) => { + expect(value).toEqual({ name: 'test' }) + this.form.reset({ name: 'test' }) + }, + }) + } + + const { getByTestId } = await render(TestComponent) + + const input = getByTestId('fieldinput') + const submit = getByTestId('submit') + + await user.type(input, 'test') + await expect(input).toHaveValue('test') + + await user.click(submit) + + await expect(input).toHaveValue('test') + }) +}) diff --git a/packages/form-core/src/utils.ts b/packages/form-core/src/utils.ts index 6060cb68f..81210057a 100644 --- a/packages/form-core/src/utils.ts +++ b/packages/form-core/src/utils.ts @@ -341,6 +341,10 @@ export function evaluate(objA: T, objB: T) { return true } + if (typeof objA === 'function' && typeof objB === 'function') { + return objA.toString() === objB.toString() + } + if ( typeof objA !== 'object' || objA === null || diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index 09fbf87c4..5c63845a6 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -120,6 +120,25 @@ describe('form api', () => { }) }) + it('form should reset default value when resetting in onSubmit', async () => { + const defaultValues = { + name: '', + } + const form = new FormApi({ + defaultValues: defaultValues, + onSubmit: ({ value }) => { + form.reset(value) + + expect(form.options.defaultValues).toMatchObject({ + name: 'test', + }) + }, + }) + form.mount() + form.setFieldValue('name', 'test') + form.handleSubmit() + }) + it('should reset and set the new default values that are restored after an empty reset', () => { const form = new FormApi({ defaultValues: { name: 'initial' } }) form.mount() diff --git a/packages/form-core/tests/utils.spec.ts b/packages/form-core/tests/utils.spec.ts index 18a493ae0..3a0a866cb 100644 --- a/packages/form-core/tests/utils.spec.ts +++ b/packages/form-core/tests/utils.spec.ts @@ -562,5 +562,17 @@ describe('evaluate', () => { { test: { testTwo: '' }, arr: [[1]] }, ) expect(objComplexTrue).toEqual(true) + + const funcTrue = evaluate( + { test: () => console.log() }, + { test: () => console.log() }, + ) + expect(funcTrue).toEqual(true) + + const funcFalse = evaluate( + { test: () => console.log() }, + { test: () => console.warn() }, + ) + expect(funcFalse).toEqual(false) }) }) diff --git a/packages/react-form/src/useForm.tsx b/packages/react-form/src/useForm.tsx index 729064aba..c0cc51be3 100644 --- a/packages/react-form/src/useForm.tsx +++ b/packages/react-form/src/useForm.tsx @@ -1,6 +1,6 @@ -import { FormApi, functionalUpdate } from '@tanstack/form-core' +import { FormApi, evaluate, functionalUpdate } from '@tanstack/form-core' import { useStore } from '@tanstack/react-store' -import React, { useState } from 'react' +import { useRef, useState } from 'react' import { Field } from './useField' import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect' import type { @@ -190,9 +190,11 @@ export function useForm< TOnServer, TSubmitMeta > = api as never + extendedApi.Field = function APIField(props) { return } + extendedApi.Subscribe = (props: any) => { return ( state.isSubmitting) + // stable reference of form options, needs to be tracked so form.update is only called + // when props are changed. + const stableOptsRef = useRef(opts) + /** * formApi.update should not have any side effects. Think of it like a `useRef` * that we need to keep updated every render with the most up-to-date information. */ useIsomorphicLayoutEffect(() => { - formApi.update(opts) + if (!evaluate(opts, stableOptsRef.current)) { + stableOptsRef.current = opts + formApi.update(opts) + } }) return formApi diff --git a/packages/react-form/tests/useForm.test.tsx b/packages/react-form/tests/useForm.test.tsx index ac62ed80b..7b012db76 100644 --- a/packages/react-form/tests/useForm.test.tsx +++ b/packages/react-form/tests/useForm.test.tsx @@ -794,4 +794,129 @@ describe('useForm', () => { expect(fn).toHaveBeenCalledTimes(1) }) + + it('form should reset default value when resetting in onSubmit', async () => { + function Comp() { + const form = useForm({ + defaultValues: { + name: '', + }, + onSubmit: ({ value }) => { + expect(value).toEqual({ name: 'another-test' }) + + form.reset(value) + }, + }) + + return ( +
{ + e.preventDefault() + e.stopPropagation() + form.handleSubmit() + }} + > + ( + field.handleChange(e.target.value)} + /> + )} + /> + + + + + + ) + } + + const { getByTestId } = render() + const input = getByTestId('fieldinput') + const submit = getByTestId('submit') + const reset = getByTestId('reset') + + await user.type(input, 'test') + await waitFor(() => expect(input).toHaveValue('test')) + + await user.click(reset) + await waitFor(() => expect(input).toHaveValue('')) + + await user.type(input, 'another-test') + await user.click(submit) + await waitFor(() => expect(input).toHaveValue('another-test')) + }) + + it('form should update when props are changed', async () => { + function Comp() { + const [defaultValue, setDefault] = useState<{ + name: string + }>({ name: '' }) + + const form = useForm({ + defaultValues: defaultValue, + onSubmit: ({ value }) => { + form.reset(value) + }, + }) + + return ( +
{ + e.preventDefault() + e.stopPropagation() + form.handleSubmit() + }} + > + ( + field.handleChange(e.target.value)} + /> + )} + /> + + + + + + ) + } + + const { getByTestId } = render() + const input = getByTestId('fieldinput') + const changeProps = getByTestId('change-props') + const changePropsAgain = getByTestId('change-props-again') + + await user.click(changeProps) + await waitFor(() => expect(input).toHaveValue('props-change')) + + // checks stableRef is comparing against previously set default + await user.click(changePropsAgain) + await waitFor(() => expect(input).toHaveValue('props-change-again')) + }) }) diff --git a/packages/vue-form/tests/useForm.test.tsx b/packages/vue-form/tests/useForm.test.tsx index 7d03f5896..66abd8e5a 100644 --- a/packages/vue-form/tests/useForm.test.tsx +++ b/packages/vue-form/tests/useForm.test.tsx @@ -476,4 +476,55 @@ describe('useForm', () => { await waitFor(() => getByText(error)) expect(getByText(error)).toBeInTheDocument() }) + + it('form should reset default value when resetting in onSubmit', async () => { + const Comp = defineComponent(() => { + const form = useForm({ + defaultValues: { + name: '', + }, + onSubmit: ({ value }) => { + expect(value).toEqual({ name: 'test' }) + + form.reset({ name: 'test' }) + }, + }) + + return () => ( +
+ + {({ field }: { field: AnyFieldApi }) => ( + + field.handleChange((e.target as HTMLInputElement).value) + } + /> + )} + + + +
+ ) + }) + + const { getByTestId } = render() + const input = getByTestId('fieldinput') + const submit = getByTestId('submit') + + await user.type(input, 'test') + await waitFor(() => expect(input).toHaveValue('test')) + + await user.click(submit) + + await waitFor(() => expect(input).toHaveValue('test')) + }) })