diff --git a/examples/react/simple/src/index.tsx b/examples/react/simple/src/index.tsx
index 920f80588..60463e441 100644
--- a/examples/react/simple/src/index.tsx
+++ b/examples/react/simple/src/index.tsx
@@ -22,7 +22,7 @@ export default function App() {
},
onSubmit: async ({ value }) => {
// Do something with form data
- console.log(value)
+ console.log(value, 'Submitted')
},
})
@@ -76,6 +76,14 @@ export default function App() {
{
+ await new Promise((resolve) => setTimeout(resolve, 1000))
+ if (value === 'CHANGED') return
+ fieldApi.form.setFieldValue('lastName', 'CHANGED')
+ },
+ }}
children={(field) => (
<>
diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts
index 33fcd8cc5..f8034b8a3 100644
--- a/packages/form-core/src/FieldApi.ts
+++ b/packages/form-core/src/FieldApi.ts
@@ -238,11 +238,11 @@ export type UnwrapFieldAsyncValidateOrFn<
/**
* @private
*/
-export type FieldListenerFn<
+type FieldListenerFnProps<
TParentData,
TName extends DeepKeys,
TData extends DeepValue = DeepValue,
-> = (props: {
+> = {
value: TData
fieldApi: FieldApi<
TParentData,
@@ -267,7 +267,25 @@ export type FieldListenerFn<
any,
any
>
-}) => void
+}
+
+/**
+ * @private
+ */
+export type FieldListenerFn<
+ TParentData,
+ TName extends DeepKeys,
+ TData extends DeepValue = DeepValue,
+> = (props: FieldListenerFnProps) => void
+
+/**
+ * @private
+ */
+export type FieldListenerAsyncFn<
+ TParentData,
+ TName extends DeepKeys,
+ TData extends DeepValue = DeepValue,
+> = (props: FieldListenerFnProps) => Promise
export interface FieldValidators<
TParentData,
@@ -357,6 +375,8 @@ export interface FieldListeners<
> {
onChange?: FieldListenerFn
onChangeDebounceMs?: number
+ onChangeAsync?: FieldListenerAsyncFn
+ onChangeAsyncDebounceMs?: number
onBlur?: FieldListenerFn
onBlurDebounceMs?: number
onMount?: FieldListenerFn
@@ -983,12 +1003,17 @@ export class FieldApi<
get state() {
return this.store.state
}
+
timeoutIds: {
validations: Record | null>
listeners: Record | null>
formListeners: Record | null>
}
+ promises: {
+ listeners: Record | null>
+ }
+
/**
* Initializes a new `FieldApi` instance.
*/
@@ -1023,6 +1048,10 @@ export class FieldApi<
formListeners: {} as Record,
}
+ this.promises = {
+ listeners: {} as Record | null>,
+ }
+
this.store = new Derived({
deps: [this.form.store],
fn: () => {
@@ -1788,6 +1817,7 @@ export class FieldApi<
})
}
+ this.triggerOnChangeAsyncListener()
const fieldDebounceMs = this.options.listeners?.onChangeDebounceMs
if (fieldDebounceMs && fieldDebounceMs > 0) {
if (this.timeoutIds.listeners.change) {
@@ -1807,6 +1837,64 @@ export class FieldApi<
})
}
}
+
+ private abortController: AbortController = new AbortController()
+ private collapseController: AbortController = new AbortController()
+ private triggerOnChangeAsyncListener() {
+ const fieldDebounceMs = this.options.listeners?.onChangeAsyncDebounceMs
+ if (fieldDebounceMs && fieldDebounceMs > 0) {
+ if (this.timeoutIds.listeners.change) {
+ clearTimeout(this.timeoutIds.listeners.change)
+ this.abortController.abort()
+ }
+
+ const debouncePromise = new Promise((resolve) => {
+ this.abortController.signal.onabort = () => {
+ resolve()
+ }
+
+ this.collapseController.signal.onabort = () => {
+ this.options.listeners
+ ?.onChangeAsync?.({
+ value: this.state.value,
+ fieldApi: this,
+ })
+ .finally(resolve)
+ }
+
+ this.timeoutIds.listeners.change = setTimeout(() => {
+ this.options.listeners
+ ?.onChangeAsync?.({
+ value: this.state.value,
+ fieldApi: this,
+ })
+ .finally(resolve)
+ }, fieldDebounceMs)
+ }).finally(() => {
+ this.promises.listeners.change = null
+ })
+ this.promises.listeners.change = debouncePromise
+ } else {
+ const promise = this.options.listeners?.onChangeAsync?.({
+ value: this.state.value,
+ fieldApi: this,
+ })
+
+ if (promise) {
+ promise.finally(() => {
+ this.promises.listeners.change = null
+ })
+ this.promises.listeners.change = promise
+ }
+ }
+ }
+
+ collapseFieldOnChangeAsync = () => {
+ if (this.timeoutIds.listeners.change) {
+ clearTimeout(this.timeoutIds.listeners.change)
+ }
+ this.collapseController.abort()
+ }
}
function normalizeError(rawError?: ValidationError) {
diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts
index a048c21d2..8a3a7ada8 100644
--- a/packages/form-core/src/FormApi.ts
+++ b/packages/form-core/src/FormApi.ts
@@ -1306,6 +1306,24 @@ export class FormApi<
return fieldErrorMapMap.flat()
}
+ collapseAllFieldAsyncOnChange = async () => {
+ const proimises: Promise[] = []
+ batch(() => {
+ void (Object.values(this.fieldInfo) as FieldInfo[]).forEach(
+ (field) => {
+ if (!field.instance) return
+ const promise = field.instance.promises.listeners.change
+ field.instance.collapseFieldOnChangeAsync()
+ if (promise) {
+ proimises.push(promise)
+ }
+ },
+ )
+ })
+
+ await Promise.all(proimises)
+ }
+
/**
* Validates the children of a specified array in the form starting from a given index until the end using the correct handlers for a given validation type.
*/
@@ -1770,6 +1788,7 @@ export class FormApi<
this.baseStore.setState((prev) => ({ ...prev, isSubmitting: false }))
}
+ await this.collapseAllFieldAsyncOnChange()
await this.validateAllFields('submit')
if (!this.state.isFieldsValid) {