From c2a3a27c9fb09fa5d31b27385e1307456b9f804f Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Tue, 31 Mar 2026 17:28:51 +0200 Subject: [PATCH 1/5] feat(core): add form listners for reset --- .changeset/reset-listeners.md | 5 +++++ packages/form-core/src/FieldApi.ts | 14 ++++++++++++++ packages/form-core/src/FormApi.ts | 19 +++++++++++++++++++ packages/form-core/src/types.ts | 8 +++++++- 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 .changeset/reset-listeners.md diff --git a/.changeset/reset-listeners.md b/.changeset/reset-listeners.md new file mode 100644 index 000000000..0a35863a1 --- /dev/null +++ b/.changeset/reset-listeners.md @@ -0,0 +1,5 @@ +--- +"@tanstack/form-core": minor +--- + +Add `onReset` listener support for forms and fields, add `reset` method to `FieldApi`, and expand `ListenerCause` type with `'reset'` and `'unmount'` values diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index a064a83de..520b0c3b3 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -8,8 +8,10 @@ import { determineFieldLevelErrorSourceAndValue, evaluate, getAsyncValidatorArray, + getBy, getSyncValidatorArray, mergeOpts, + setBy, } from './utils' import { defaultValidationLogic } from './ValidationLogic' import type { ReadonlyStore } from '@tanstack/store' @@ -384,6 +386,7 @@ export interface FieldListeners< onMount?: FieldListenerFn onUnmount?: FieldListenerFn onSubmit?: FieldListenerFn + onReset?: FieldListenerFn } /** @@ -2090,6 +2093,17 @@ export class FieldApi< ) } + /** + * Resets the field value and meta to default state. + */ + reset = () => { + this.form.resetField(this.name) + this.options.listeners?.onReset?.({ + value: this.state.value, + fieldApi: this, + }) + } + private triggerOnBlurListener() { const formDebounceMs = this.form.options.listeners?.onBlurDebounceMs if (formDebounceMs && formDebounceMs > 0) { diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 748fca0fe..9c88879a1 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -324,6 +324,23 @@ export interface FormListeners< > fieldApi: AnyFieldApi }) => void + + onReset?: (props: { + formApi: FormApi< + TFormData, + TOnMount, + TOnChange, + TOnChangeAsync, + TOnBlur, + TOnBlurAsync, + TOnSubmit, + TOnSubmitAsync, + TOnDynamic, + TOnDynamicAsync, + TOnServer, + TSubmitMeta + > + }) => void } /** @@ -1541,6 +1558,8 @@ export class FormApi< fieldMetaBase, }), ) + + this.options.listeners?.onReset?.({ formApi: this }) } /** diff --git a/packages/form-core/src/types.ts b/packages/form-core/src/types.ts index ff5824283..b2a2f39b3 100644 --- a/packages/form-core/src/types.ts +++ b/packages/form-core/src/types.ts @@ -21,7 +21,13 @@ export type ValidationCause = /** * @private */ -export type ListenerCause = 'change' | 'blur' | 'submit' | 'mount' +export type ListenerCause = + | 'change' + | 'blur' + | 'submit' + | 'mount' + | 'reset' + | 'unmount' /** * @private From 0d972d43a4bf7ec3b992e68de27ccd20f71b11b3 Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Tue, 31 Mar 2026 17:43:22 +0200 Subject: [PATCH 2/5] chore:test --- packages/form-core/tests/FieldApi.spec.ts | 77 +++++++++++++++++++++++ packages/form-core/tests/FormApi.spec.ts | 57 +++++++++++++++++ 2 files changed, 134 insertions(+) diff --git a/packages/form-core/tests/FieldApi.spec.ts b/packages/form-core/tests/FieldApi.spec.ts index 1fa3d4d65..113c166e7 100644 --- a/packages/form-core/tests/FieldApi.spec.ts +++ b/packages/form-core/tests/FieldApi.spec.ts @@ -3529,4 +3529,81 @@ describe('edge cases and error handling', () => { field.handleChange(undefined) expect(field.state.value).toBeUndefined() }) + + it('should run listener onReset when field.reset() is called', () => { + const form = new FormApi({ + defaultValues: { + name: 'test', + }, + }) + + let triggered: string | undefined + const field = new FieldApi({ + form, + name: 'name', + listeners: { + onReset: ({ value }) => { + triggered = value + }, + }, + }) + + form.mount() + field.mount() + field.setValue('changed') + field.reset() + + expect(triggered).toStrictEqual('test') + }) + + it('should not run onReset listener when form.reset() is called directly', () => { + const form = new FormApi({ + defaultValues: { + name: 'test', + }, + }) + + const onReset = vi.fn() + const field = new FieldApi({ + form, + name: 'name', + listeners: { + onReset, + }, + }) + + form.mount() + field.mount() + form.reset() + + expect(onReset).not.toHaveBeenCalled() + }) + + it('should provide value and fieldApi in the onReset listener', () => { + const form = new FormApi({ + defaultValues: { + name: 'test', + }, + }) + + let capturedFieldApi: any + let capturedValue: string | undefined + const field = new FieldApi({ + form, + name: 'name', + listeners: { + onReset: ({ value, fieldApi }) => { + capturedValue = value + capturedFieldApi = fieldApi + }, + }, + }) + + form.mount() + field.mount() + field.reset() + + expect(capturedValue).toStrictEqual('test') + expect(capturedFieldApi).toBe(field) + }) }) diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index c1b36a85c..47c1786d4 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -4183,4 +4183,61 @@ describe('form transform', () => { expect(field.state.meta.errorMap.onChange).toBe('Error') }) + + it('should run the form listener onReset', () => { + let triggered!: string + const form = new FormApi({ + defaultValues: { + name: 'test', + }, + listeners: { + onReset: ({ formApi }) => { + triggered = formApi.state.values.name + }, + }, + }) + + form.mount() + form.reset() + + expect(triggered).toStrictEqual('test') + }) + + it('should run the form listener onReset with new values', () => { + let triggered!: string + const form = new FormApi({ + defaultValues: { + name: 'test', + }, + listeners: { + onReset: ({ formApi }) => { + triggered = formApi.state.values.name + }, + }, + }) + + form.mount() + form.reset({ name: 'reset-value' }) + + expect(triggered).toStrictEqual('reset-value') + }) + + it('should provide formApi in the onReset listener', () => { + let capturedFormApi: any + const form = new FormApi({ + defaultValues: { + name: 'test', + }, + listeners: { + onReset: ({ formApi }) => { + capturedFormApi = formApi + }, + }, + }) + + form.mount() + form.reset() + + expect(capturedFormApi).toBe(form) + }) }) From 59c621094c062aa62fe59847c0b53dd7332e2a68 Mon Sep 17 00:00:00 2001 From: Harry Whorlow Date: Tue, 31 Mar 2026 17:47:45 +0200 Subject: [PATCH 3/5] docs: update listener docs --- docs/framework/angular/guides/listeners.md | 1 + docs/framework/react/guides/listeners.md | 1 + docs/framework/vue/guides/listeners.md | 1 + 3 files changed, 3 insertions(+) diff --git a/docs/framework/angular/guides/listeners.md b/docs/framework/angular/guides/listeners.md index 729e00a86..ee444354a 100644 --- a/docs/framework/angular/guides/listeners.md +++ b/docs/framework/angular/guides/listeners.md @@ -20,6 +20,7 @@ Events that can be "listened" to are: - `onMount` - `onSubmit` - `onUnmount` +- `onReset` ```angular-ts @Component({ diff --git a/docs/framework/react/guides/listeners.md b/docs/framework/react/guides/listeners.md index f42cb6dcd..5c7a7d706 100644 --- a/docs/framework/react/guides/listeners.md +++ b/docs/framework/react/guides/listeners.md @@ -20,6 +20,7 @@ Events that can be "listened" to are: - `onMount` - `onSubmit` - `onUnmount` +- `onReset` ```tsx function App() { diff --git a/docs/framework/vue/guides/listeners.md b/docs/framework/vue/guides/listeners.md index 946af1e7f..b3dda0390 100644 --- a/docs/framework/vue/guides/listeners.md +++ b/docs/framework/vue/guides/listeners.md @@ -20,6 +20,7 @@ Events that can be "listened" to are: - `onMount` - `onSubmit` - `onUnmount` +- `onReset` ```vue