From e710f9186c5f81c6be93b239b918fc6e7e1ed7f6 Mon Sep 17 00:00:00 2001 From: Kamil Kusy Date: Sat, 17 Jan 2026 17:32:30 +0100 Subject: [PATCH] fix(form-core): Validate fields without instances --- .changeset/happy-pillows-hear.md | 5 ++++ packages/form-core/src/FormApi.ts | 13 +++++++++- .../tests/standardSchemaValidator.spec.ts | 25 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 .changeset/happy-pillows-hear.md diff --git a/.changeset/happy-pillows-hear.md b/.changeset/happy-pillows-hear.md new file mode 100644 index 000000000..5c370c526 --- /dev/null +++ b/.changeset/happy-pillows-hear.md @@ -0,0 +1,5 @@ +--- +'@tanstack/form-core': patch +--- + +run validation for fields without instances diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index ff52bbe74..aa03262d0 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1599,7 +1599,18 @@ export class FormApi< ) => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const fieldInstance = this.fieldInfo[field]?.instance - if (!fieldInstance) return [] + + if (!fieldInstance) { + const { hasErrored } = this.validateSync(cause) + + if (hasErrored && !this.options.asyncAlways) { + return this.getFieldMeta(field)?.errors ?? [] + } + + return this.validateAsync(cause).then(() => { + return this.getFieldMeta(field)?.errors ?? [] + }) + } // If the field is not touched (same logic as in validateAllFields) if (!fieldInstance.state.meta.isTouched) { diff --git a/packages/form-core/tests/standardSchemaValidator.spec.ts b/packages/form-core/tests/standardSchemaValidator.spec.ts index ca0a7c78f..463dac173 100644 --- a/packages/form-core/tests/standardSchemaValidator.spec.ts +++ b/packages/form-core/tests/standardSchemaValidator.spec.ts @@ -108,6 +108,31 @@ describe('standard schema validator', () => { ]) }) + it('should handle form-level field errors for fields without a mounted FieldApi instance', () => { + const form = new FormApi({ + defaultValues: { + email: '', + }, + validators: { + onChange: z.object({ + email: z.string().email('email must be an email address'), + }), + }, + }) + + form.mount() + + form.setFieldValue('email', 'not-an-email') + + expect(form.state.errors).toMatchObject([ + { email: [{ message: 'email must be an email address' }] }, + ]) + + form.setFieldValue('email', 'test@example.com') + + expect(form.state.errors).toEqual([]) + }) + it('should support standard schema async validation with zod', async () => { vi.useFakeTimers()