Skip to content

Commit

Permalink
feat: spread valibot 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 5fdfa85 commit 198c08a
Show file tree
Hide file tree
Showing 2 changed files with 193 additions and 26 deletions.
80 changes: 54 additions & 26 deletions packages/valibot-form-adapter/src/validator.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,91 @@
import { getDotPath, safeParse, safeParseAsync } from 'valibot'
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 {
BaseIssue,
GenericIssue,
GenericSchema,
GenericSchemaAsync,
ValiError,
} from 'valibot'

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

export function prefixSchemaToErrors(errors: Array<BaseIssue<unknown>>) {
let schema = {} as object
for (const valibotError of errors) {
schema = setBy(schema, getDotPath(valibotError), () => valibotError.message)
}
return schema
}
export function prefixSchemaToErrors(
valiErrors: GenericIssue[],
transformErrors: TransformFn,
) {
const schema = new Map<string, GenericIssue[]>()

for (const valiError of valiErrors) {
if (!valiError.path) continue

export function defaultFormTransformer(errors: Array<BaseIssue<unknown>>) {
return {
form: mapIssuesToSingleString(errors),
fields: prefixSchemaToErrors(errors),
const path = valiError.path
.map(({ key: segment }) =>
typeof segment === 'number' ? `[${segment}]` : segment,
)
.join('.')
.replace(/\.\[/g, '[')
schema.set(path, (schema.get(path) ?? []).concat(valiError))
}

const transformedSchema = {} as Record<string, ValidationError>

schema.forEach((value, key) => {
transformedSchema[key] = transformErrors(value)
})

return transformedSchema
}

export const mapIssuesToSingleString = (errors: Array<BaseIssue<unknown>>) =>
errors.map((error) => error.message).join(', ')
export function defaultFormTransformer(transformErrors: TransformFn) {
return (zodErrors: GenericIssue[]) => ({
form: transformErrors(zodErrors),
fields: prefixSchemaToErrors(zodErrors, transformErrors),
})
}

export const valibotValidator =
(
params: Params = {},
): Validator<unknown, GenericSchema | GenericSchemaAsync> =>
() => {
const transformFieldErrors =
params.transformErrors ??
((issues: GenericIssue[]) =>
issues.map((issue) => issue.message).join(', '))

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

return {
validate({ value, validationSource }, fn) {
if (fn.async) return
const result = safeParse(fn, value, {
abortPipeEarly: false,
})
if (result.success) return
const transformErrors = params.transformErrors
? params.transformErrors
: validationSource === 'form'
? defaultFormTransformer
: mapIssuesToSingleString
return transformErrors(result.issues)

const transformer = getTransformStrategy(validationSource)

return transformer(result.issues)
},
async validateAsync({ value, validationSource }, fn) {
const result = await safeParseAsync(fn, value, {
abortPipeEarly: false,
})
if (result.success) return
const transformErrors = params.transformErrors
? params.transformErrors
: validationSource === 'form'
? defaultFormTransformer
: mapIssuesToSingleString
return transformErrors(result.issues)

const transformer = getTransformStrategy(validationSource)

return transformer(result.issues)
},
}
}
139 changes: 139 additions & 0 deletions packages/valibot-form-adapter/tests/FormApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,143 @@ describe('valibot 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: valibotValidator(),
validators: {
onChange: v.object({
name: v.pipe(
v.string(),
v.minLength(3, 'You must have a length of at least 3'),
v.endsWith('a', 'You must end with an "a"'),
),
surname: v.pipe(
v.string(),
v.minLength(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: valibotValidator({
transformErrors: (errors: any[]) => errors[0]?.message,
}),
validators: {
onChange: v.object({
name: v.pipe(
v.string(),
v.minLength(3, 'You must have a length of at least 3'),
v.endsWith('a', 'You must end with an "a"'),
),
foo: v.object({
bar: v.pipe(
v.string(),
v.minLength(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: valibotValidator(),
validators: {
onChange: v.object({
names: v.array(
v.pipe(
v.string(),
v.minLength(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([])
})
})

0 comments on commit 198c08a

Please sign in to comment.