diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index 925a096e2..ade4c18d6 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -187,8 +187,6 @@ export class FieldApi { update = (opts: FieldApiOptions) => { this.options = { asyncDebounceMs: this.form.options.asyncDebounceMs ?? 0, - onChangeAsyncDebounceMs: this.form.options.onChangeAsyncDebounceMs ?? 0, - onBlurAsyncDebounceMs: this.form.options.onBlurAsyncDebounceMs ?? 0, ...opts, } as never @@ -334,7 +332,7 @@ export class FieldApi { ? onChangeAsyncDebounceMs : onBlurAsyncDebounceMs) ?? asyncDebounceMs ?? - 500 + 0 if (this.state.meta.isValidating !== true) this.setMeta((prev) => ({ ...prev, isValidating: true })) diff --git a/packages/form-core/src/tests/FieldApi.spec.ts b/packages/form-core/src/tests/FieldApi.spec.ts index 7869446e4..c23a3eee7 100644 --- a/packages/form-core/src/tests/FieldApi.spec.ts +++ b/packages/form-core/src/tests/FieldApi.spec.ts @@ -2,6 +2,7 @@ import { expect } from 'vitest' import { FormApi } from '../FormApi' import { FieldApi } from '../FieldApi' +import { sleep } from './utils' describe('field api', () => { it('should have an initial value', () => { @@ -151,7 +152,30 @@ describe('field api', () => { expect(subfield.getValue()).toBe('one') }) - it('should run validation onChange', async () => { + it('should not throw errors when no meta info is stored on a field and a form re-renders', async () => { + const form = new FormApi({ + defaultValues: { + name: 'test', + }, + }) + + const field = new FieldApi({ + form, + name: 'name', + }) + + field.mount() + + expect(() => + form.update({ + defaultValues: { + name: 'other', + }, + }), + ).not.toThrow() + }) + + it('should run validation onChange', () => { const form = new FormApi({ defaultValues: { name: 'test', @@ -162,10 +186,33 @@ describe('field api', () => { form, name: 'name', onChange: (value) => { - if (value === 'other') { - return 'Please enter a different value' - } + if (value === 'other') return 'Please enter a different value' + return + }, + }) + + field.mount() + + expect(field.getMeta().error).toBeUndefined() + field.setValue('other', { touch: true }) + expect(field.getMeta().error).toBe('Please enter a different value') + }) + + it('should run async validation onChange', async () => { + vi.useFakeTimers() + const form = new FormApi({ + defaultValues: { + name: 'test', + }, + }) + + const field = new FieldApi({ + form, + name: 'name', + onChangeAsync: async (value) => { + await sleep(1000) + if (value === 'other') return 'Please enter a different value' return }, }) @@ -174,10 +221,14 @@ describe('field api', () => { expect(field.getMeta().error).toBeUndefined() field.setValue('other', { touch: true }) + await vi.runAllTimersAsync() expect(field.getMeta().error).toBe('Please enter a different value') }) - it('should not throw errors when no meta info is stored on a field and a form re-renders', async () => { + it('should run async validation onChange with debounce', async () => { + vi.useFakeTimers() + const sleepMock = vi.fn().mockImplementation(sleep) + const form = new FormApi({ defaultValues: { name: 'test', @@ -187,16 +238,199 @@ describe('field api', () => { const field = new FieldApi({ form, name: 'name', + onChangeAsyncDebounceMs: 1000, + onChangeAsync: async (value) => { + await sleepMock(1000) + if (value === 'other') return 'Please enter a different value' + return + }, }) field.mount() - expect(() => - form.update({ - defaultValues: { - name: 'other', - }, - }), - ).not.toThrow() + expect(field.getMeta().error).toBeUndefined() + field.setValue('other', { touch: true }) + field.setValue('other') + await vi.runAllTimersAsync() + // sleepMock will have been called 2 times without onChangeAsyncDebounceMs + expect(sleepMock).toHaveBeenCalledTimes(1) + expect(field.getMeta().error).toBe('Please enter a different value') + }) + + it('should run async validation onChange with asyncDebounceMs', async () => { + vi.useFakeTimers() + const sleepMock = vi.fn().mockImplementation(sleep) + + const form = new FormApi({ + defaultValues: { + name: 'test', + }, + }) + + const field = new FieldApi({ + form, + name: 'name', + asyncDebounceMs: 1000, + onChangeAsync: async (value) => { + await sleepMock(1000) + if (value === 'other') return 'Please enter a different value' + return + }, + }) + + field.mount() + + expect(field.getMeta().error).toBeUndefined() + field.setValue('other', { touch: true }) + field.setValue('other') + await vi.runAllTimersAsync() + // sleepMock will have been called 2 times without asyncDebounceMs + expect(sleepMock).toHaveBeenCalledTimes(1) + expect(field.getMeta().error).toBe('Please enter a different value') + }) + + it('should run validation onBlur', () => { + const form = new FormApi({ + defaultValues: { + name: 'other', + }, + }) + + const field = new FieldApi({ + form, + name: 'name', + onBlur: (value) => { + if (value === 'other') return 'Please enter a different value' + return + }, + }) + + field.mount() + + field.setValue('other', { touch: true }) + field.validate('blur') + expect(field.getMeta().error).toBe('Please enter a different value') + }) + + it('should run async validation onBlur', async () => { + vi.useFakeTimers() + + const form = new FormApi({ + defaultValues: { + name: 'test', + }, + }) + + const field = new FieldApi({ + form, + name: 'name', + onBlurAsync: async (value) => { + await sleep(1000) + if (value === 'other') return 'Please enter a different value' + return + }, + }) + + field.mount() + + expect(field.getMeta().error).toBeUndefined() + field.setValue('other', { touch: true }) + field.validate('blur') + await vi.runAllTimersAsync() + expect(field.getMeta().error).toBe('Please enter a different value') + }) + + it('should run async validation onBlur with debounce', async () => { + vi.useFakeTimers() + const sleepMock = vi.fn().mockImplementation(sleep) + + const form = new FormApi({ + defaultValues: { + name: 'test', + }, + }) + + const field = new FieldApi({ + form, + name: 'name', + onBlurAsyncDebounceMs: 1000, + onBlurAsync: async (value) => { + await sleepMock(10) + if (value === 'other') return 'Please enter a different value' + return + }, + }) + + field.mount() + + expect(field.getMeta().error).toBeUndefined() + field.setValue('other', { touch: true }) + field.validate('blur') + field.validate('blur') + await vi.runAllTimersAsync() + // sleepMock will have been called 2 times without onBlurAsyncDebounceMs + expect(sleepMock).toHaveBeenCalledTimes(1) + expect(field.getMeta().error).toBe('Please enter a different value') + }) + + it('should run async validation onBlur with asyncDebounceMs', async () => { + vi.useFakeTimers() + const sleepMock = vi.fn().mockImplementation(sleep) + + const form = new FormApi({ + defaultValues: { + name: 'test', + }, + }) + + const field = new FieldApi({ + form, + name: 'name', + asyncDebounceMs: 1000, + onBlurAsync: async (value) => { + await sleepMock(10) + if (value === 'other') return 'Please enter a different value' + return + }, + }) + + field.mount() + + expect(field.getMeta().error).toBeUndefined() + field.setValue('other', { touch: true }) + field.validate('blur') + field.validate('blur') + await vi.runAllTimersAsync() + // sleepMock will have been called 2 times without asyncDebounceMs + expect(sleepMock).toHaveBeenCalledTimes(1) + expect(field.getMeta().error).toBe('Please enter a different value') + }) + + it('should run async validation onSubmit', async () => { + vi.useFakeTimers() + + const form = new FormApi({ + defaultValues: { + name: 'test', + }, + }) + + const field = new FieldApi({ + form, + name: 'name', + onSubmitAsync: async (value) => { + await sleep(1000) + if (value === 'other') return 'Please enter a different value' + return + }, + }) + + field.mount() + + expect(field.getMeta().error).toBeUndefined() + field.setValue('other', { touch: true }) + field.validate('submit') + await vi.runAllTimersAsync() + expect(field.getMeta().error).toBe('Please enter a different value') }) }) diff --git a/packages/form-core/src/tests/utils.ts b/packages/form-core/src/tests/utils.ts new file mode 100644 index 000000000..1a3a619a2 --- /dev/null +++ b/packages/form-core/src/tests/utils.ts @@ -0,0 +1,5 @@ +export function sleep(timeout: number): Promise { + return new Promise((resolve, _reject) => { + setTimeout(resolve, timeout) + }) +}