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/whole-views-wear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/form-core': patch
---

Ensure fieldName is passed to custom validation logic functions
4 changes: 4 additions & 0 deletions packages/form-core/src/FieldApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1676,6 +1676,7 @@ export class FieldApi<
const validates = getSyncValidatorArray(cause, {
...this.options,
form: this.form,
fieldName: this.name,
validationLogic:
this.form.options.validationLogic || defaultValidationLogic,
})
Expand All @@ -1686,6 +1687,7 @@ export class FieldApi<
const fieldValidates = getSyncValidatorArray(cause, {
...field.options,
form: field.form,
fieldName: field.name,
validationLogic:
field.form.options.validationLogic || defaultValidationLogic,
})
Expand Down Expand Up @@ -1812,6 +1814,7 @@ export class FieldApi<
const validates = getAsyncValidatorArray(cause, {
...this.options,
form: this.form,
fieldName: this.name,
validationLogic:
this.form.options.validationLogic || defaultValidationLogic,
})
Expand All @@ -1825,6 +1828,7 @@ export class FieldApi<
const fieldValidates = getAsyncValidatorArray(cause, {
...field.options,
form: field.form,
fieldName: field.name,
validationLogic:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
field.form.options.validationLogic || defaultValidationLogic,
})
Expand Down
2 changes: 1 addition & 1 deletion packages/form-core/src/ValidationLogic.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { AnyFormApi, FormValidators } from './FormApi'

interface ValidationLogicValidatorsFn {
export interface ValidationLogicValidatorsFn {
// TODO: Type this properly
fn: FormValidators<
any,
Expand Down
6 changes: 4 additions & 2 deletions packages/form-core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ export function getSyncValidatorArray<T>(
options: SyncValidatorArrayPartialOptions<T> & {
validationLogic?: any
form?: any
fieldName?: string
},
): T extends FieldValidators<
any,
Expand Down Expand Up @@ -300,7 +301,7 @@ export function getSyncValidatorArray<T>(
return options.validationLogic({
form: options.form,
validators: options.validators,
event: { type: cause, async: false },
event: { type: cause, fieldName: options.fieldName, async: false },
runValidation,
})
}
Expand All @@ -313,6 +314,7 @@ export function getAsyncValidatorArray<T>(
options: AsyncValidatorArrayPartialOptions<T> & {
validationLogic?: any
form?: any
fieldName?: string
},
): T extends FieldValidators<
any,
Expand Down Expand Up @@ -410,7 +412,7 @@ export function getAsyncValidatorArray<T>(
return options.validationLogic({
form: options.form,
validators: options.validators,
event: { type: cause, async: true },
event: { type: cause, fieldName: options.fieldName, async: true },
runValidation,
})
}
Expand Down
198 changes: 198 additions & 0 deletions packages/form-core/tests/DynamicValidation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import { describe, expect, it, vi } from 'vitest'
import { z } from 'zod'
import { FieldApi, FormApi } from '../src/index'
import { defaultValidationLogic, revalidateLogic } from '../src/ValidationLogic'
import { sleep } from './utils'
import type {
ValidationLogicFn,
ValidationLogicProps,
ValidationLogicValidatorsFn,
} from '../src/ValidationLogic'

describe('custom validation', () => {
it('should handle default validation logic', async () => {
Expand Down Expand Up @@ -233,4 +239,196 @@ describe('custom validation', () => {

expect(field.state.meta.errorMap.onDynamic).toBe(undefined)
})

describe('customised field-level validation logic', () => {
const validationLogic: ValidationLogicFn = (props) => {
const validatorNames = Object.keys(props.validators ?? {})
if (validatorNames.length === 0) {
// No validators is a valid case, just return
return props.runValidation({
validators: [],
form: props.form,
})
}

let validators: ValidationLogicValidatorsFn[] = []
defaultValidationLogic({
...props,
runValidation: (vProps) => {
validators =
vProps.validators.slice() as ValidationLogicValidatorsFn[]
},
})

let addDynamicValidator = props.event.type === 'submit'
if (!addDynamicValidator) {
const hasFormSubmitted = props.form.state.submissionAttempts > 0
const modesToWatch: ValidationLogicProps['event']['type'][] =
hasFormSubmitted
? ['change']
: props.event.fieldName
? props.form.state.fieldMeta[props.event.fieldName]?.isBlurred
? ['change', 'blur']
: ['blur']
: ['blur']
addDynamicValidator = modesToWatch.includes(props.event.type)
}
if (addDynamicValidator) {
validators.push({
fn: props.event.async
? props.validators!['onDynamicAsync']
: props.validators!['onDynamic'],
cause: 'dynamic',
})
}

return props.runValidation({
validators,
form: props.form,
})
}

it('should support sync validation', async () => {
const form = new FormApi({
defaultValues: {
firstName: '',
lastName: '',
},
validationLogic,
})
form.mount()

const fieldFirstName = new FieldApi({
form,
name: 'firstName',
validators: {
onDynamic: ({ value }) =>
value.length >= 3
? undefined
: 'First name must be at least 3 characters long',
},
})
fieldFirstName.mount()

const fieldLastName = new FieldApi({
form,
name: 'lastName',
validators: {
onDynamic: ({ value }) =>
value.length >= 3
? undefined
: 'Last name must be at least 3 characters long',
},
})
fieldLastName.mount()

expect(fieldFirstName.state.value).toBe('')
expect(fieldFirstName.state.meta.errorMap.onDynamic).toBe(undefined)

// Should not validate on change initially
fieldFirstName.handleChange('Jo')
expect(fieldFirstName.state.meta.errorMap.onDynamic).toBe(undefined)

// But validation should occur immediately on blur
fieldFirstName.handleBlur()
expect(fieldFirstName.state.meta.errorMap.onDynamic).toBe(
'First name must be at least 3 characters long',
)

// And after that point, validation should occur on change
fieldFirstName.handleChange('Matt')
expect(fieldFirstName.state.meta.errorMap.onDynamic).toBe(undefined)

// This field hasn't been touched, so it shouldn't have an error
expect(fieldLastName.state.value).toBe('')
expect(fieldLastName.state.meta.errorMap.onDynamic).toBe(undefined)

// But after form submission, it should immediately have an error
await form.handleSubmit()
expect(fieldLastName.state.meta.errorMap.onDynamic).toBe(
'Last name must be at least 3 characters long',
)

// And it should immediately validate on change now
fieldLastName.handleChange('Smith')
expect(fieldLastName.state.meta.errorMap.onDynamic).toBe(undefined)
})

it('should support async validation', async () => {
vi.useFakeTimers()

const form = new FormApi({
defaultValues: {
firstName: '',
lastName: '',
},
validationLogic,
})
form.mount()

const fieldFirstName = new FieldApi({
form,
name: 'firstName',
validators: {
onDynamicAsync: async ({ value }) => {
await sleep(100)
return value.length >= 3
? undefined
: 'First name must be at least 3 characters long'
},
},
})
fieldFirstName.mount()

const fieldLastName = new FieldApi({
form,
name: 'lastName',
validators: {
onDynamicAsync: async ({ value }) => {
await sleep(100)
return value.length >= 3
? undefined
: 'Last name must be at least 3 characters long'
},
},
})
fieldLastName.mount()

expect(fieldFirstName.state.value).toBe('')
expect(fieldFirstName.state.meta.errorMap.onDynamic).toBe(undefined)

// Should not validate on change initially
fieldFirstName.handleChange('Jo')
expect(fieldFirstName.state.meta.errorMap.onDynamic).toBe(undefined)

// But validation should occur immediately on blur
fieldFirstName.handleBlur()
await vi.runAllTimersAsync()
expect(fieldFirstName.state.meta.errorMap.onDynamic).toBe(
'First name must be at least 3 characters long',
)

// And after that point, validation should occur on change
fieldFirstName.handleChange('Matt')
await vi.runAllTimersAsync()
expect(fieldFirstName.state.meta.errorMap.onDynamic).toBe(undefined)

// This field hasn't been touched, so it shouldn't have an error
expect(fieldLastName.state.value).toBe('')
expect(fieldLastName.state.meta.errorMap.onDynamic).toBe(undefined)

// But after form submission, it should immediately have an error
const submitPromise = form.handleSubmit()
await vi.runAllTimersAsync()
await submitPromise
expect(fieldLastName.state.meta.errorMap.onDynamic).toBe(
'Last name must be at least 3 characters long',
)

// And it should immediately validate on change now
fieldLastName.handleChange('Smith')
await vi.runAllTimersAsync()
expect(fieldLastName.state.meta.errorMap.onDynamic).toBe(undefined)
})
})
})
Loading