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
5 changes: 5 additions & 0 deletions .changeset/complete-unmounted-field-meta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/form-core': patch
---

Fix `form.setFieldValue()` creating incomplete metadata for a field that is not mounted yet. A `FormGroup` validator that later reports an error for that field no longer throws `TypeError: Cannot read properties of undefined (reading 'onChange')`, and the error is assigned to the field.
5 changes: 2 additions & 3 deletions packages/form-core/src/FormApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2659,13 +2659,12 @@ export class FormApi<

batch(() => {
if (!dontUpdateMeta) {
this.setFieldMeta(field, (prev) => ({
this.setFieldMeta(field, (prev = defaultFieldMeta) => ({
...prev,
isTouched: true,
isDirty: true,
errorMap: {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
...prev?.errorMap,
...prev.errorMap,
onMount: undefined,
},
}))
Expand Down
57 changes: 57 additions & 0 deletions packages/form-core/tests/FormApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,63 @@ describe('form api', () => {
expect(form.state.isPristine).toBe(false)
})

it('should create complete field meta when setting the value of an unmounted field', () => {
const form = new FormApi({
defaultValues: {
name: 'test',
},
})
form.mount()

form.setFieldValue('name', 'other')

expect(form.getFieldMeta('name')).toMatchObject({
isTouched: true,
isDirty: true,
isBlurred: false,
isValidating: false,
errorMap: { onMount: undefined },
errorSourceMap: {},
errors: [],
})
})

it('should preserve existing field meta when setting a field value', () => {
const form = new FormApi({
defaultValues: {
name: 'test',
},
})
form.mount()

const field = new FieldApi({
form,
name: 'name',
validators: {
onMount: () => 'mount error',
},
})
field.mount()

form.setFieldMeta('name', (prev) => ({
...prev,
isBlurred: true,
errorMap: { ...prev.errorMap, onBlur: 'blur error' },
errorSourceMap: { ...prev.errorSourceMap, onBlur: 'field' },
}))

form.setFieldValue('name', 'other')

const meta = form.getFieldMeta('name')
expect(meta?.isBlurred).toBe(true)
expect(meta?.errorMap.onMount).toBeUndefined()
expect(meta?.errorMap.onBlur).toBe('blur error')
expect(meta?.errorSourceMap).toMatchObject({
onMount: 'field',
onBlur: 'field',
})
})

it('should be clean again after being reset from a dirty state', () => {
const form = new FormApi({
defaultValues: {
Expand Down
55 changes: 55 additions & 0 deletions packages/form-core/tests/FormGroupApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,61 @@ describe('form group api', () => {
)
})

it('Should distribute group field errors to an unmounted field previously written with setFieldValue', () => {
const form = new FormApi({
defaultValues: {
launch: {
type: 'manual',
price: undefined as number | undefined,
},
},
})
form.mount()

const group = new FormGroupApi({
form,
name: 'launch',
validators: {
onChange: ({ value }) =>
value.type === 'exclusive' && value.price === undefined
? { fields: { price: 'Price is required' } }
: undefined,
},
})
group.mount()

const typeField = new FieldApi({
form,
name: 'launch.type',
})
typeField.mount()

form.setFieldValue('launch.price', undefined)

expect(() => typeField.handleChange('exclusive')).not.toThrow()

expect(form.getFieldMeta('launch.price')?.errorMap.onChange).toBe(
'Price is required',
)
expect(form.getFieldMeta('launch.price')?.errorSourceMap.onChange).toBe(
'form',
)
expect(group.state.meta.isFieldsValid).toBe(false)

const priceField = new FieldApi({
form,
name: 'launch.price',
})
priceField.mount()

expect(priceField.state.meta.errors).toEqual(['Price is required'])

priceField.handleChange(10)

expect(priceField.state.meta.errorMap.onChange).toBeUndefined()
expect(group.state.meta.isFieldsValid).toBe(true)
})

it('Should propagate manual function field errors from group validators to child fields', async () => {
const onSubmit = vi.fn()

Expand Down