Skip to content

Commit

Permalink
feat: spread yup errors into fields, nested and arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
Balastrong committed Sep 16, 2024
1 parent 606f542 commit 5fdfa85
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 37 deletions.
79 changes: 46 additions & 33 deletions packages/yup-form-adapter/src/validator.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,66 @@
import { setBy } from '@tanstack/form-core'
import type { Validator, ValidatorAdapterParams } from '@tanstack/form-core'
import type {
ValidationError,
Validator,
ValidatorAdapterParams,
} from '@tanstack/form-core'
import type { AnySchema, ValidationError as YupError } from 'yup'

type Params = ValidatorAdapterParams<string>
type Params = ValidatorAdapterParams<YupError>
type TransformFn = NonNullable<Params['transformErrors']>

export function prefixSchemaToErrors(error: YupError) {
let schema = {} as object
for (const yupError of error.inner) {
schema = setBy(schema, yupError.path, () => yupError.message)
}
return schema
}
export function prefixSchemaToErrors(
yupErrors: YupError[],
transformErrors: TransformFn,
) {
const schema = new Map<string, YupError[]>()

export function defaultFormTransformer(error: YupError) {
return {
form: mapIssuesToSingleString(error),
fields: prefixSchemaToErrors(error),
for (const yupError of yupErrors) {
if (!yupError.path) continue

const path = yupError.path
schema.set(path, (schema.get(path) ?? []).concat(yupError))
}
}

export const mapIssuesToSingleString = (error: YupError) =>
error.errors.join(', ')
const transformedSchema = {} as Record<string, ValidationError>

const executeParamsTransformErrors =
(transformErrors: NonNullable<Params['transformErrors']>) =>
(e: YupError) => {
return transformErrors(e.errors)
}
schema.forEach((value, key) => {
transformedSchema[key] = transformErrors(value)
})

return transformedSchema
}

export function defaultFormTransformer(transformErrors: TransformFn) {
return (zodErrors: YupError[]) => ({
form: transformErrors(zodErrors),
fields: prefixSchemaToErrors(zodErrors, transformErrors),
})
}

export const yupValidator =
(params: Params = {}): Validator<unknown, AnySchema> =>
() => {
const transformFieldErrors =
params.transformErrors ??
((errors: YupError[]) => errors.map((error) => error.message).join(', '))

const getTransformStrategy = (validationSource: 'form' | 'field') =>
validationSource === 'form'
? defaultFormTransformer(transformFieldErrors)
: transformFieldErrors

return {
validate({ value, validationSource }, fn) {
try {
fn.validateSync(value, { abortEarly: false })
return
} catch (_e) {
const e = _e as YupError
const transformErrors = params.transformErrors
? executeParamsTransformErrors(params.transformErrors)
: validationSource === 'form'
? defaultFormTransformer
: mapIssuesToSingleString

return transformErrors(e)
const transformer = getTransformStrategy(validationSource)

return transformer(e.inner)
}
},
async validateAsync({ value, validationSource }, fn) {
Expand All @@ -53,13 +69,10 @@ export const yupValidator =
return
} catch (_e) {
const e = _e as YupError
const transformErrors = params.transformErrors
? executeParamsTransformErrors(params.transformErrors)
: validationSource === 'form'
? defaultFormTransformer
: mapIssuesToSingleString

return transformErrors(e)
const transformer = getTransformStrategy(validationSource)

return transformer(e.inner)
}
},
}
Expand Down
4 changes: 2 additions & 2 deletions packages/yup-form-adapter/tests/FieldApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ describe('yup field api', () => {
const field = new FieldApi({
form,
validatorAdapter: yupValidator({
transformErrors: (errors) => errors[0],
transformErrors: (errors) => errors[0]?.message,
}),
name: 'name',
validators: {
Expand Down Expand Up @@ -166,7 +166,7 @@ describe('yup field api', () => {
const field = new FieldApi({
form,
validatorAdapter: yupValidator({
transformErrors: (errors) => errors[0],
transformErrors: (errors) => errors[0]?.message,
}),
name: 'name',
validators: {
Expand Down
130 changes: 129 additions & 1 deletion packages/yup-form-adapter/tests/FormApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import yup from 'yup'
import { yupValidator } from '../src/index'

describe('yup form api', () => {
it('should run an onChange with z.string validation', () => {
it('should run an onChange with yup.string validation', () => {
const form = new FormApi({
defaultValues: {
name: '',
Expand Down Expand Up @@ -56,4 +56,132 @@ describe('yup form api', () => {
field.setValue('asdf')
expect(field.getMeta().errors).toEqual([])
})

it("should set field errors with the form validator's onChange", () => {
const form = new FormApi({
defaultValues: {
name: '',
surname: '',
},
validatorAdapter: yupValidator(),
validators: {
onChange: yup.object({
name: yup
.string()
.min(3, 'You must have a length of at least 3')
.matches(/a$/, 'You must end with an "a"'),
surname: yup.string().min(3, 'You must have a length of at least 3'),
}),
},
})

const nameField = new FieldApi({
form,
name: 'name',
})

const surnameField = new FieldApi({
form,
name: 'surname',
})

nameField.mount()
surnameField.mount()

expect(nameField.getMeta().errors).toEqual([])
nameField.setValue('q')
expect(nameField.getMeta().errors).toEqual([
'You must have a length of at least 3, You must end with an "a"',
])
expect(surnameField.getMeta().errors).toEqual([
'You must have a length of at least 3',
])
nameField.setValue('qwer')
expect(nameField.getMeta().errors).toEqual(['You must end with an "a"'])
nameField.setValue('qwera')
expect(nameField.getMeta().errors).toEqual([])
})

it("should set field errors with the form validator's onChange and transform them", () => {
const form = new FormApi({
defaultValues: {
name: '',
foo: {
bar: '',
},
},
validatorAdapter: yupValidator({
transformErrors: (errors) => errors[0]?.message,
}),
validators: {
onChange: yup.object({
name: yup
.string()
.min(3, 'You must have a length of at least 3')
.matches(/a$/, 'You must end with an "a"'),
foo: yup.object({
bar: yup.string().min(4, 'You must have a length of at least 4'),
}),
}),
},
})

const nameField = new FieldApi({
form,
name: 'name',
})

const fooBarField = new FieldApi({
form,
name: 'foo.bar',
})

nameField.mount()
fooBarField.mount()

expect(nameField.getMeta().errors).toEqual([])
nameField.setValue('q')
expect(nameField.getMeta().errors).toEqual([
'You must have a length of at least 3',
])
expect(fooBarField.getMeta().errors).toEqual([
'You must have a length of at least 4',
])
nameField.setValue('qwer')
expect(nameField.getMeta().errors).toEqual(['You must end with an "a"'])
nameField.setValue('qwera')
expect(nameField.getMeta().errors).toEqual([])
})

it('should set field errors with the form validator for arrays', () => {
const form = new FormApi({
defaultValues: {
names: [''],
},
validatorAdapter: yupValidator(),
validators: {
onChange: yup.object({
names: yup.array(
yup.string().min(3, 'You must have a length of at least 3'),
),
}),
},
})

const name0Field = new FieldApi({
form,
name: 'names[0]',
})

name0Field.mount()

expect(name0Field.getMeta().errors).toEqual([])
name0Field.setValue('q')

expect(name0Field.getMeta().errors).toEqual([
'You must have a length of at least 3',
])
name0Field.setValue('qwer')
expect(name0Field.getMeta().errors).toEqual([])
})
})
1 change: 0 additions & 1 deletion packages/zod-form-adapter/src/validator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { setBy } from '@tanstack/form-core'
import type {
ValidationError,
Validator,
Expand Down

0 comments on commit 5fdfa85

Please sign in to comment.