diff --git a/packages/form-core/src/FieldApi.ts b/packages/form-core/src/FieldApi.ts index a343bda81..7317d70a1 100644 --- a/packages/form-core/src/FieldApi.ts +++ b/packages/form-core/src/FieldApi.ts @@ -9,6 +9,7 @@ import { getAsyncValidatorArray, getBy, getSyncValidatorArray, + mergeOpts, } from './utils' import { defaultValidationLogic } from './ValidationLogic' import type { DeepKeys, DeepValue, UnwrapOneLevelOfArray } from './util-types' @@ -1350,11 +1351,19 @@ export class FieldApi< * Sets the field value and run the `change` validator. */ setValue = (updater: Updater, options?: UpdateMetaOptions) => { - this.form.setFieldValue(this.name, updater as never, options) + this.form.setFieldValue( + this.name, + updater as never, + mergeOpts(options, { dontRunListeners: true, dontValidate: true }), + ) - this.triggerOnChangeListener() + if (!options?.dontRunListeners) { + this.triggerOnChangeListener() + } - this.validate('change') + if (!options?.dontValidate) { + this.validate('change') + } } getMeta = () => this.store.state.meta @@ -1400,11 +1409,17 @@ export class FieldApi< */ pushValue = ( value: TData extends any[] ? TData[number] : never, - opts?: UpdateMetaOptions, + options?: UpdateMetaOptions, ) => { - this.form.pushFieldValue(this.name, value as any, opts) + this.form.pushFieldValue( + this.name, + value as any, + mergeOpts(options, { dontRunListeners: true }), + ) - this.triggerOnChangeListener() + if (!options?.dontRunListeners) { + this.triggerOnChangeListener() + } } /** @@ -1413,11 +1428,18 @@ export class FieldApi< insertValue = ( index: number, value: TData extends any[] ? TData[number] : never, - opts?: UpdateMetaOptions, + options?: UpdateMetaOptions, ) => { - this.form.insertFieldValue(this.name, index, value as any, opts) + this.form.insertFieldValue( + this.name, + index, + value as any, + mergeOpts(options, { dontRunListeners: true }), + ) - this.triggerOnChangeListener() + if (!options?.dontRunListeners) { + this.triggerOnChangeListener() + } } /** @@ -1426,47 +1448,83 @@ export class FieldApi< replaceValue = ( index: number, value: TData extends any[] ? TData[number] : never, - opts?: UpdateMetaOptions, + options?: UpdateMetaOptions, ) => { - this.form.replaceFieldValue(this.name, index, value as any, opts) + this.form.replaceFieldValue( + this.name, + index, + value as any, + mergeOpts(options, { dontRunListeners: true }), + ) - this.triggerOnChangeListener() + if (!options?.dontRunListeners) { + this.triggerOnChangeListener() + } } /** * Removes a value at the specified index. */ - removeValue = (index: number, opts?: UpdateMetaOptions) => { - this.form.removeFieldValue(this.name, index, opts) + removeValue = (index: number, options?: UpdateMetaOptions) => { + this.form.removeFieldValue( + this.name, + index, + mergeOpts(options, { dontRunListeners: true }), + ) - this.triggerOnChangeListener() + if (!options?.dontRunListeners) { + this.triggerOnChangeListener() + } } /** * Swaps the values at the specified indices. */ - swapValues = (aIndex: number, bIndex: number, opts?: UpdateMetaOptions) => { - this.form.swapFieldValues(this.name, aIndex, bIndex, opts) + swapValues = ( + aIndex: number, + bIndex: number, + options?: UpdateMetaOptions, + ) => { + this.form.swapFieldValues( + this.name, + aIndex, + bIndex, + mergeOpts(options, { dontRunListeners: true }), + ) - this.triggerOnChangeListener() + if (!options?.dontRunListeners) { + this.triggerOnChangeListener() + } } /** * Moves the value at the first specified index to the second specified index. */ - moveValue = (aIndex: number, bIndex: number, opts?: UpdateMetaOptions) => { - this.form.moveFieldValues(this.name, aIndex, bIndex, opts) + moveValue = (aIndex: number, bIndex: number, options?: UpdateMetaOptions) => { + this.form.moveFieldValues( + this.name, + aIndex, + bIndex, + mergeOpts(options, { dontRunListeners: true }), + ) - this.triggerOnChangeListener() + if (!options?.dontRunListeners) { + this.triggerOnChangeListener() + } } /** * Clear all values from the array. */ - clearValues = (opts?: UpdateMetaOptions) => { - this.form.clearFieldValues(this.name, opts) + clearValues = (options?: UpdateMetaOptions) => { + this.form.clearFieldValues( + this.name, + mergeOpts(options, { dontRunListeners: true }), + ) - this.triggerOnChangeListener() + if (!options?.dontRunListeners) { + this.triggerOnChangeListener() + } } /** @@ -1936,7 +1994,10 @@ export class FieldApi< } } - private triggerOnChangeListener() { + /** + * @private + */ + triggerOnChangeListener() { const formDebounceMs = this.form.options.listeners?.onChangeDebounceMs if (formDebounceMs && formDebounceMs > 0) { if (this.timeoutIds.formListeners.change) { diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 0c62c5cd6..ee654e598 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -9,6 +9,7 @@ import { getSyncValidatorArray, isGlobalFormValidationError, isNonEmptyArray, + mergeOpts, setBy, } from './utils' import { defaultValidationLogic } from './ValidationLogic' @@ -35,6 +36,7 @@ import type { FieldManipulator, FormValidationError, FormValidationErrorMap, + ListenerCause, UpdateMetaOptions, ValidationCause, ValidationError, @@ -930,6 +932,15 @@ export class FormApi< */ prevTransformArray: unknown[] = [] + /** + * + */ + timeoutIds: { + validations: Record | null> + listeners: Record | null> + formListeners: Record | null> + } + /** * Constructs a new `FormApi` instance with the given form options. */ @@ -949,6 +960,12 @@ export class FormApi< TSubmitMeta >, ) { + this.timeoutIds = { + validations: {} as Record, + listeners: {} as Record, + formListeners: {} as Record, + } + this.baseStore = new Store( getDefaultFormState({ ...(opts?.defaultState as any), @@ -2056,6 +2073,8 @@ export class FormApi< opts?: UpdateMetaOptions, ) => { const dontUpdateMeta = opts?.dontUpdateMeta ?? false + const dontRunListeners = opts?.dontRunListeners ?? false + const dontValidate = opts?.dontValidate ?? false batch(() => { if (!dontUpdateMeta) { @@ -2078,6 +2097,14 @@ export class FormApi< } }) }) + + if (!dontRunListeners) { + this.getFieldInfo(field).instance?.triggerOnChangeListener() + } + + if (!dontValidate) { + this.validateField(field, 'change') + } } deleteField = >(field: TField) => { @@ -2109,14 +2136,13 @@ export class FormApi< value: DeepValue extends any[] ? DeepValue[number] : never, - opts?: UpdateMetaOptions, + options?: UpdateMetaOptions, ) => { this.setFieldValue( field, (prev) => [...(Array.isArray(prev) ? prev : []), value] as any, - opts, + options, ) - this.validateField(field, 'change') } insertFieldValue = async >( @@ -2125,7 +2151,7 @@ export class FormApi< value: DeepValue extends any[] ? DeepValue[number] : never, - opts?: UpdateMetaOptions, + options?: UpdateMetaOptions, ) => { this.setFieldValue( field, @@ -2136,7 +2162,7 @@ export class FormApi< ...(prev as DeepValue[]).slice(index), ] as any }, - opts, + mergeOpts(options, { dontValidate: true }), ) // Validate the whole array + all fields that have shifted @@ -2157,7 +2183,7 @@ export class FormApi< value: DeepValue extends any[] ? DeepValue[number] : never, - opts?: UpdateMetaOptions, + options?: UpdateMetaOptions, ) => { this.setFieldValue( field, @@ -2166,7 +2192,7 @@ export class FormApi< i === index ? value : d, ) as any }, - opts, + mergeOpts(options, { dontValidate: true }), ) // Validate the whole array + all fields that have shifted @@ -2180,7 +2206,7 @@ export class FormApi< removeFieldValue = async >( field: TField, index: number, - opts?: UpdateMetaOptions, + options?: UpdateMetaOptions, ) => { const fieldValue = this.getFieldValue(field) @@ -2195,7 +2221,7 @@ export class FormApi< (_d, i) => i !== index, ) as any }, - opts, + mergeOpts(options, { dontValidate: true }), ) // Shift up all meta @@ -2218,7 +2244,7 @@ export class FormApi< field: TField, index1: number, index2: number, - opts?: UpdateMetaOptions, + options?: UpdateMetaOptions, ) => { this.setFieldValue( field, @@ -2227,7 +2253,7 @@ export class FormApi< const prev2 = prev[index2]! return setBy(setBy(prev, `${index1}`, prev2), `${index2}`, prev1) }, - opts, + mergeOpts(options, { dontValidate: true }), ) // Swap meta @@ -2247,7 +2273,7 @@ export class FormApi< field: TField, index1: number, index2: number, - opts?: UpdateMetaOptions, + options?: UpdateMetaOptions, ) => { this.setFieldValue( field, @@ -2256,7 +2282,7 @@ export class FormApi< next.splice(index2, 0, next.splice(index1, 1)[0]) return next }, - opts, + mergeOpts(options, { dontValidate: true }), ) // Move meta between index1 and index2 @@ -2274,7 +2300,7 @@ export class FormApi< */ clearFieldValues = >( field: TField, - opts?: UpdateMetaOptions, + options?: UpdateMetaOptions, ) => { const fieldValue = this.getFieldValue(field) @@ -2282,7 +2308,11 @@ export class FormApi< ? Math.max((fieldValue as unknown[]).length - 1, 0) : null - this.setFieldValue(field, [] as any, opts) + this.setFieldValue( + field, + [] as any, + mergeOpts(options, { dontValidate: true }), + ) if (lastIndex !== null) { for (let i = 0; i <= lastIndex; i++) { diff --git a/packages/form-core/src/types.ts b/packages/form-core/src/types.ts index ff8b3e5db..ff5824283 100644 --- a/packages/form-core/src/types.ts +++ b/packages/form-core/src/types.ts @@ -134,6 +134,14 @@ export interface UpdateMetaOptions { * @default false */ dontUpdateMeta?: boolean + /** + * @default false + */ + dontValidate?: boolean + /** + * @default false + */ + dontRunListeners?: boolean } /** diff --git a/packages/form-core/src/utils.ts b/packages/form-core/src/utils.ts index 3b0186e35..9acf9e4fd 100644 --- a/packages/form-core/src/utils.ts +++ b/packages/form-core/src/utils.ts @@ -539,3 +539,18 @@ export function createFieldMap(values: Readonly): { [K in keyof T]: K } { return output } + +/** + * Merge the first parameter with the given overrides. + * @private + */ +export function mergeOpts( + originalOpts: T | undefined | null, + overrides: T, +): T { + if (originalOpts === undefined || originalOpts === null) { + return overrides + } + + return { ...originalOpts, ...overrides } +} diff --git a/packages/form-core/tests/utils.spec.ts b/packages/form-core/tests/utils.spec.ts index 08966f330..6c8fbba22 100644 --- a/packages/form-core/tests/utils.spec.ts +++ b/packages/form-core/tests/utils.spec.ts @@ -8,6 +8,7 @@ import { evaluate, getBy, makePathArray, + mergeOpts, setBy, } from '../src/index' @@ -725,3 +726,28 @@ describe('createFieldMap', () => { expect(input).toEqual(copy) }) }) + +describe('mergeOpts', () => { + type SomeOpts = { + foo?: string + bar?: boolean + } + + it('should return the overrides if original object is undefined', () => { + expect(mergeOpts(undefined, { foo: 'test' })).toEqual({ + foo: 'test', + }) + expect(mergeOpts(null, { foo: 'test' })).toEqual({ foo: 'test' }) + }) + + it('should preserve properties that were not overwritten', () => { + const original: SomeOpts = { + foo: 'test', + } + expect(mergeOpts(original, { bar: true })).toEqual({ + foo: 'test', + bar: true, + }) + expect(mergeOpts(original, {})).toEqual({ foo: 'test' }) + }) +})