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
5 changes: 5 additions & 0 deletions .changeset/reset-listeners.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions docs/framework/angular/guides/listeners.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Events that can be "listened" to are:
- `onMount`
- `onSubmit`
- `onUnmount`
- `onReset`

```angular-ts
@Component({
Expand Down
7 changes: 4 additions & 3 deletions docs/framework/react/guides/listeners.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ Imagine the following user flow:

In this example, when the user changes the country, the selected province needs to be reset as it's no longer valid. With the listener API, we can subscribe to the `onChange` event and dispatch a reset to the "province" field when the listener is fired.

Events that can be "listened" to are:
Field level events that can be "listened" to are:

- `onChange`
- `onBlur`
- `onMount`
- `onSubmit`
- `onUnmount`
- `onReset`

```tsx
function App() {
Expand Down Expand Up @@ -93,9 +94,9 @@ We enable an easy method for debouncing your listeners by adding a `onChangeDebo

### Form listeners

At a higher level, listeners are also available at the form level, allowing you access to the `onMount` and `onSubmit` events, and having `onChange` and `onBlur` propagated to all the form's children. Form-level listeners can also be debounced in the same way as previously discussed.
At a higher level, listeners are also available at the form level, allowing you access to the `onMount`, `onSubmit`, and `onReset` events, and having `onChange` and `onBlur` propagated to all the form's children. Form-level listeners can also be debounced in the same way as previously discussed.

`onMount` and `onSubmit` listeners have the following parameters:
`onMount`, `onSubmit`, `onReset` listeners have the following parameters:

- `formApi`

Expand Down
1 change: 1 addition & 0 deletions docs/framework/vue/guides/listeners.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Events that can be "listened" to are:
- `onMount`
- `onSubmit`
- `onUnmount`
- `onReset`

```vue
<script setup>
Expand Down
12 changes: 12 additions & 0 deletions packages/form-core/src/FieldApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ export interface FieldListeners<
onMount?: FieldListenerFn<TParentData, TName, TData>
onUnmount?: FieldListenerFn<TParentData, TName, TData>
onSubmit?: FieldListenerFn<TParentData, TName, TData>
onReset?: FieldListenerFn<TParentData, TName, TData>
}

/**
Expand Down Expand Up @@ -2090,6 +2091,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) {
Expand Down
19 changes: 19 additions & 0 deletions packages/form-core/src/FormApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down Expand Up @@ -1541,6 +1558,8 @@ export class FormApi<
fieldMetaBase,
}),
)

this.options.listeners?.onReset?.({ formApi: this })
}

/**
Expand Down
8 changes: 7 additions & 1 deletion packages/form-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 77 additions & 0 deletions packages/form-core/tests/FieldApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
57 changes: 57 additions & 0 deletions packages/form-core/tests/FormApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
Loading