Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
37d659c
add 'found' property to getBy
LeCarbonator Sep 13, 2025
fabf0ec
WIP: for testing
LeCarbonator Sep 13, 2025
fb1a06f
ci: apply automated fixes and generate docs
autofix-ci[bot] Sep 13, 2025
3b07e51
refactor metaHelper for clarity
LeCarbonator Sep 18, 2025
9c2f9b4
Merge branch 'undefined-default-value' of github.com:LeCarbonator/tan…
LeCarbonator Sep 24, 2025
ee4b55c
Merge branch 'main' of github.com:LeCarbonator/tanstack-form into und…
LeCarbonator Sep 24, 2025
60e4c68
add baseStore to fieldApi
LeCarbonator Sep 26, 2025
04519ef
chore: remove dead code from metaHelper
LeCarbonator Sep 26, 2025
ba0d121
chore: let's see what sticks
LeCarbonator Sep 26, 2025
bc16c7a
chore: remove unneeded additional store
LeCarbonator Sep 26, 2025
fc0358d
fix undefined errors being passed to fieldMeta
LeCarbonator Oct 2, 2025
97a9877
remove unused import
LeCarbonator Oct 2, 2025
7f115ca
chore: add tests for vue
LeCarbonator Oct 8, 2025
f4cdba3
Merge branch 'main' of github.com:TanStack/form into undefined-defaul…
LeCarbonator Oct 15, 2025
0d1b7d6
chore: clean up previous drafts
LeCarbonator Oct 17, 2025
53380ba
remove unrelated unit test diff
LeCarbonator Oct 17, 2025
6d82079
fix: prevent defaultValue regression
LeCarbonator Oct 30, 2025
91ced86
chore: adjust unit tests's target value
LeCarbonator Oct 30, 2025
840ef61
fix: adhere to rules of react for useField
LeCarbonator Nov 18, 2025
0559065
chore: Merge from upstream main
LeCarbonator Nov 18, 2025
d280770
chore: add solidjs unit test
LeCarbonator Nov 24, 2025
4a489fa
Merge branch 'main' into undefined-default-value
lachlancollins Nov 24, 2025
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
54 changes: 27 additions & 27 deletions packages/form-core/src/FieldApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
import { defaultFieldMeta } from './metaHelper'
import {
determineFieldLevelErrorSourceAndValue,
evaluate,
getAsyncValidatorArray,
getBy,
getSyncValidatorArray,
Expand Down Expand Up @@ -1052,7 +1053,7 @@ export class FieldApi<
/**
* The field name.
*/
name!: DeepKeys<TParentData>
name: TName
/**
* The field options.
*/
Expand Down Expand Up @@ -1151,8 +1152,10 @@ export class FieldApi<
TParentSubmitMeta
>,
) {
this.form = opts.form as never
this.name = opts.name as never
this.form = opts.form
this.name = opts.name
this.options = opts

this.timeoutIds = {
validations: {} as Record<ValidationCause, never>,
listeners: {} as Record<ListenerCause, never>,
Expand All @@ -1162,12 +1165,21 @@ export class FieldApi<
this.store = new Derived({
deps: [this.form.store],
fn: () => {
const value = this.form.getFieldValue(this.name)
const meta = this.form.getFieldMeta(this.name) ?? {
...defaultFieldMeta,
...opts.defaultMeta,
}

let value = this.form.getFieldValue(this.name)
if (
!meta.isTouched &&
(value as unknown) === undefined &&
this.options.defaultValue !== undefined &&
!evaluate(value, this.options.defaultValue)
) {
value = this.options.defaultValue
}

return {
value,
meta,
Expand Down Expand Up @@ -1196,8 +1208,6 @@ export class FieldApi<
>
},
})

this.options = opts as never
}

/**
Expand Down Expand Up @@ -1232,8 +1242,8 @@ export class FieldApi<
mount = () => {
const cleanup = this.store.mount()

if ((this.options.defaultValue as unknown) !== undefined) {
this.form.setFieldValue(this.name, this.options.defaultValue as never, {
if (this.options.defaultValue !== undefined && !this.getMeta().isTouched) {
this.form.setFieldValue(this.name, this.options.defaultValue, {
dontUpdateMeta: true,
})
}
Expand Down Expand Up @@ -1309,33 +1319,23 @@ export class FieldApi<
TParentSubmitMeta
>,
) => {
this.options = opts as never

const nameHasChanged = this.name !== opts.name
this.options = opts
this.name = opts.name

// Default Value
if ((this.state.value as unknown) === undefined) {
const formDefault = getBy(opts.form.options.defaultValues, opts.name)

const defaultValue = (opts.defaultValue as unknown) ?? formDefault

// The name is dynamic in array fields. It changes when the user performs operations like removing or reordering.
// In this case, we don't want to force a default value if the store managed to find an existing value.
if (nameHasChanged) {
this.setValue((val) => (val as unknown) || defaultValue, {
dontUpdateMeta: true,
})
} else if (defaultValue !== undefined) {
this.setValue(defaultValue as never, {
if (!this.state.meta.isTouched && this.options.defaultValue !== undefined) {
const formField = this.form.getFieldValue(this.name)
if (!evaluate(formField, opts.defaultValue)) {
this.form.setFieldValue(this.name, opts.defaultValue as never, {
dontUpdateMeta: true,
dontValidate: true,
dontRunListeners: true,
})
}
}

// Default Meta
if (this.form.getFieldMeta(this.name) === undefined) {
this.setMeta(this.state.meta)
if (!this.form.getFieldMeta(this.name)) {
this.form.setFieldMeta(this.name, this.state.meta)
}
}

Expand Down
20 changes: 9 additions & 11 deletions packages/form-core/src/FormApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1071,20 +1071,18 @@ export class FormApi<
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
fieldErrors = Object.values(currBaseMeta.errorMap ?? {}).filter(
(val) => val !== undefined,
) as never
)

// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const fieldInstance = this.getFieldInfo(fieldName)?.instance

if (fieldInstance && !fieldInstance.options.disableErrorFlat) {
fieldErrors = (fieldErrors as undefined | string[])?.flat(
1,
) as never
fieldErrors = fieldErrors.flat(1)
}
}

// As primitives, we don't need to aggressively persist the same referential value for performance reasons
const isFieldValid = !isNonEmptyArray(fieldErrors ?? [])
const isFieldValid = !isNonEmptyArray(fieldErrors)
const isFieldPristine = !currBaseMeta.isDirty
const isDefaultValue =
evaluate(
Expand Down Expand Up @@ -1112,11 +1110,11 @@ export class FormApi<

fieldMeta[fieldName] = {
...currBaseMeta,
errors: fieldErrors,
errors: fieldErrors ?? [],
isPristine: isFieldPristine,
isValid: isFieldValid,
isDefaultValue: isDefaultValue,
} as AnyFieldMeta
} satisfies AnyFieldMeta as AnyFieldMeta
}

if (!Object.keys(currBaseStore.fieldMetaBase).length) return fieldMeta
Expand Down Expand Up @@ -2347,7 +2345,7 @@ export class FormApi<
}

// Shift down all meta after validating to make sure the new field has been mounted
metaHelper(this).handleArrayFieldMetaShift(field, index, 'insert')
metaHelper(this).handleArrayInsert(field, index)

if (!dontValidate) {
await this.validateArrayFieldsStartingFrom(field, index, 'change')
Expand Down Expand Up @@ -2408,7 +2406,7 @@ export class FormApi<
)

// Shift up all meta
metaHelper(this).handleArrayFieldMetaShift(field, index, 'remove')
metaHelper(this).handleArrayRemove(field, index)

if (lastIndex !== null) {
const start = `${field}[${lastIndex}]`
Expand Down Expand Up @@ -2443,7 +2441,7 @@ export class FormApi<
)

// Swap meta
metaHelper(this).handleArrayFieldMetaShift(field, index1, 'swap', index2)
metaHelper(this).handleArraySwap(field, index1, index2)

const dontValidate = options?.dontValidate ?? false
if (!dontValidate) {
Expand Down Expand Up @@ -2475,7 +2473,7 @@ export class FormApi<
)

// Move meta between index1 and index2
metaHelper(this).handleArrayFieldMetaShift(field, index1, 'move', index2)
metaHelper(this).handleArrayMove(field, index1, index2)

const dontValidate = options?.dontValidate ?? false
if (!dontValidate) {
Expand Down
Loading
Loading