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
111 changes: 86 additions & 25 deletions packages/form-core/src/FieldApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getAsyncValidatorArray,
getBy,
getSyncValidatorArray,
mergeOpts,
} from './utils'
import { defaultValidationLogic } from './ValidationLogic'
import type { DeepKeys, DeepValue, UnwrapOneLevelOfArray } from './util-types'
Expand Down Expand Up @@ -1350,11 +1351,19 @@ export class FieldApi<
* Sets the field value and run the `change` validator.
*/
setValue = (updater: Updater<TData>, 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 }),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so if options === undefined, the way mergeOpts works, is that it will return dontValidate === true. therefore, by default, calling setFieldValue does not validate?

Copy link
Contributor Author

@LeCarbonator LeCarbonator Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See lines 1360-1366. We are telling setFieldValue that we're taking care of it ourselves and it doesn't need to invest its work for that.

mergeOpts simply ensures that the overrides are present for this internal call

)

this.triggerOnChangeListener()
if (!options?.dontRunListeners) {
this.triggerOnChangeListener()
}

this.validate('change')
if (!options?.dontValidate) {
this.validate('change')
}
}

getMeta = () => this.store.state.meta
Expand Down Expand Up @@ -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()
}
}

/**
Expand All @@ -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()
}
}

/**
Expand All @@ -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()
}
}

/**
Expand Down Expand Up @@ -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) {
Expand Down
60 changes: 45 additions & 15 deletions packages/form-core/src/FormApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getSyncValidatorArray,
isGlobalFormValidationError,
isNonEmptyArray,
mergeOpts,
setBy,
} from './utils'
import { defaultValidationLogic } from './ValidationLogic'
Expand All @@ -35,6 +36,7 @@ import type {
FieldManipulator,
FormValidationError,
FormValidationErrorMap,
ListenerCause,
UpdateMetaOptions,
ValidationCause,
ValidationError,
Expand Down Expand Up @@ -930,6 +932,15 @@ export class FormApi<
*/
prevTransformArray: unknown[] = []

/**
*
*/
timeoutIds: {
validations: Record<ValidationCause, ReturnType<typeof setTimeout> | null>
listeners: Record<ListenerCause, ReturnType<typeof setTimeout> | null>
formListeners: Record<ListenerCause, ReturnType<typeof setTimeout> | null>
}

/**
* Constructs a new `FormApi` instance with the given form options.
*/
Expand All @@ -949,6 +960,12 @@ export class FormApi<
TSubmitMeta
>,
) {
this.timeoutIds = {
validations: {} as Record<ValidationCause, never>,
listeners: {} as Record<ListenerCause, never>,
formListeners: {} as Record<ListenerCause, never>,
}

this.baseStore = new Store(
getDefaultFormState({
...(opts?.defaultState as any),
Expand Down Expand Up @@ -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) {
Expand All @@ -2078,6 +2097,14 @@ export class FormApi<
}
})
})

if (!dontRunListeners) {
this.getFieldInfo(field).instance?.triggerOnChangeListener()
}

if (!dontValidate) {
this.validateField(field, 'change')
}
}

deleteField = <TField extends DeepKeys<TFormData>>(field: TField) => {
Expand Down Expand Up @@ -2109,14 +2136,13 @@ export class FormApi<
value: DeepValue<TFormData, TField> extends any[]
? DeepValue<TFormData, TField>[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 <TField extends DeepKeysOfType<TFormData, any[]>>(
Expand All @@ -2125,7 +2151,7 @@ export class FormApi<
value: DeepValue<TFormData, TField> extends any[]
? DeepValue<TFormData, TField>[number]
: never,
opts?: UpdateMetaOptions,
options?: UpdateMetaOptions,
) => {
this.setFieldValue(
field,
Expand All @@ -2136,7 +2162,7 @@ export class FormApi<
...(prev as DeepValue<TFormData, TField>[]).slice(index),
] as any
},
opts,
mergeOpts(options, { dontValidate: true }),
)

// Validate the whole array + all fields that have shifted
Expand All @@ -2157,7 +2183,7 @@ export class FormApi<
value: DeepValue<TFormData, TField> extends any[]
? DeepValue<TFormData, TField>[number]
: never,
opts?: UpdateMetaOptions,
options?: UpdateMetaOptions,
) => {
this.setFieldValue(
field,
Expand All @@ -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
Expand All @@ -2180,7 +2206,7 @@ export class FormApi<
removeFieldValue = async <TField extends DeepKeysOfType<TFormData, any[]>>(
field: TField,
index: number,
opts?: UpdateMetaOptions,
options?: UpdateMetaOptions,
) => {
const fieldValue = this.getFieldValue(field)

Expand All @@ -2195,7 +2221,7 @@ export class FormApi<
(_d, i) => i !== index,
) as any
},
opts,
mergeOpts(options, { dontValidate: true }),
)

// Shift up all meta
Expand All @@ -2218,7 +2244,7 @@ export class FormApi<
field: TField,
index1: number,
index2: number,
opts?: UpdateMetaOptions,
options?: UpdateMetaOptions,
) => {
this.setFieldValue(
field,
Expand All @@ -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
Expand All @@ -2247,7 +2273,7 @@ export class FormApi<
field: TField,
index1: number,
index2: number,
opts?: UpdateMetaOptions,
options?: UpdateMetaOptions,
) => {
this.setFieldValue(
field,
Expand All @@ -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
Expand All @@ -2274,15 +2300,19 @@ export class FormApi<
*/
clearFieldValues = <TField extends DeepKeysOfType<TFormData, any[]>>(
field: TField,
opts?: UpdateMetaOptions,
options?: UpdateMetaOptions,
) => {
const fieldValue = this.getFieldValue(field)

const lastIndex = Array.isArray(fieldValue)
? 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++) {
Expand Down
8 changes: 8 additions & 0 deletions packages/form-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ export interface UpdateMetaOptions {
* @default false
*/
dontUpdateMeta?: boolean
/**
* @default false
*/
dontValidate?: boolean
/**
* @default false
*/
dontRunListeners?: boolean
}

/**
Expand Down
Loading