diff --git a/packages/vee-validate/src/validate.ts b/packages/vee-validate/src/validate.ts index 17a8ca275..7918bfab6 100644 --- a/packages/vee-validate/src/validate.ts +++ b/packages/vee-validate/src/validate.ts @@ -81,7 +81,7 @@ async function _validate(field: FieldValidationContext name: field.name, label: field.label, form: field.formData, - value: value, + value, }; // Normalize the pipeline @@ -141,6 +141,17 @@ async function _validate(field: FieldValidationContext }; } +interface YupError { + name: 'ValidationError'; + path?: string; + errors: string[]; + inner: { path?: string; errors: string[] }[]; +} + +function isYupError(err: unknown): err is YupError { + return !!err && (err as any).name === 'ValidationError'; +} + function yupToTypedSchema(yupSchema: YupSchema): TypedSchema { const schema: TypedSchema = { __type: 'VVTypedSchema', @@ -152,23 +163,24 @@ function yupToTypedSchema(yupSchema: YupSchema): TypedSchema { output, errors: [], }; - } catch (err: any) { + } catch (err: unknown) { // Yup errors have a name prop one them. // https://github.com/jquense/yup#validationerrorerrors-string--arraystring-value-any-path-string - if (err.name !== 'ValidationError') { + if (!isYupError(err)) { throw err; } - const errors: Record = err.inner.reduce((acc: any, curr: any) => { - if (!curr.path) { - return acc; - } + if (!err.inner?.length && err.errors.length) { + return { errors: [{ path: err.path, errors: err.errors }] }; + } - if (!acc[curr.path]) { - acc[curr.path] = { errors: [], path: curr.path }; + const errors: Record = err.inner.reduce((acc, curr) => { + const path = curr.path || ''; + if (!acc[path]) { + acc[path] = { errors: [], path }; } - acc[curr.path].errors.push(...curr.errors); + acc[path].errors.push(...curr.errors); return acc; }, {} as Record); diff --git a/packages/yup/src/index.ts b/packages/yup/src/index.ts index c8fd7d409..8931e6f8d 100644 --- a/packages/yup/src/index.ts +++ b/packages/yup/src/index.ts @@ -30,15 +30,12 @@ export function toTypedSchema = error.inner.reduce((acc, curr) => { - if (!curr.path) { - return acc; + const path = curr.path || ''; + if (!acc[path]) { + acc[path] = { errors: [], path }; } - if (!acc[curr.path]) { - acc[curr.path] = { errors: [], path: curr.path }; - } - - acc[curr.path].errors.push(...curr.errors); + acc[path].errors.push(...curr.errors); return acc; }, {} as Record); diff --git a/packages/zod/src/index.ts b/packages/zod/src/index.ts index 380b1bef4..bf95ab163 100644 --- a/packages/zod/src/index.ts +++ b/packages/zod/src/index.ts @@ -1,4 +1,4 @@ -import { ZodObject, input, output, ZodDefault, ZodSchema } from 'zod'; +import { ZodObject, input, output, ZodDefault, ZodSchema, ParseParams } from 'zod'; import { PartialDeep } from 'type-fest'; import type { TypedSchema, TypedSchemaError } from 'vee-validate'; import { isIndex, merge } from '../../shared'; @@ -10,11 +10,11 @@ export function toTypedSchema< TSchema extends ZodSchema, TOutput = output, TInput = PartialDeep> ->(zodSchema: TSchema): TypedSchema { +>(zodSchema: TSchema, opts?: Partial): TypedSchema { const schema: TypedSchema = { __type: 'VVTypedSchema', async validate(value) { - const result = await zodSchema.safeParseAsync(value); + const result = await zodSchema.safeParseAsync(value, opts); if (result.success) { return { value: result.data, @@ -22,12 +22,19 @@ export function toTypedSchema< }; } - const errors: TypedSchemaError[] = result.error.issues.map(issue => { - return { path: joinPath(issue.path), errors: [issue.message] }; - }); + const errors: Record = result.error.issues.reduce((acc, issue) => { + const path = joinPath(issue.path); + if (!acc[path]) { + acc[path] = { errors: [], path }; + } + + acc[path].errors.push(issue.message); + + return acc; + }, {} as Record); return { - errors, + errors: Object.values(errors), }; }, parse(values) { diff --git a/packages/zod/tests/zod.spec.ts b/packages/zod/tests/zod.spec.ts index db48a788f..1f7817237 100644 --- a/packages/zod/tests/zod.spec.ts +++ b/packages/zod/tests/zod.spec.ts @@ -1,6 +1,7 @@ import { useField, useForm } from '@/vee-validate'; import { toTypedSchema } from '@/zod'; import { mountWithHoc, flushPromises, setValue } from 'vee-validate/tests/helpers'; +import { Ref } from 'vue'; import * as zod from 'zod'; const REQUIRED_MSG = 'field is required'; @@ -40,6 +41,90 @@ test('validates typed field with yup', async () => { expect(error.textContent).toBe(''); }); +test('generates multiple errors for any given field', async () => { + let errors!: Ref; + const wrapper = mountWithHoc({ + setup() { + const rules = toTypedSchema(zod.string().min(1, REQUIRED_MSG).min(8, MIN_MSG)); + const { value, errors: fieldErrors } = useField('test', rules); + + errors = fieldErrors; + return { + value, + }; + }, + template: ` +
+ +
+ `, + }); + + const input = wrapper.$el.querySelector('input'); + + setValue(input, ''); + await flushPromises(); + expect(errors.value).toHaveLength(2); + expect(errors.value).toEqual([REQUIRED_MSG, MIN_MSG]); +}); + +test('shows multiple errors using error bag', async () => { + const wrapper = mountWithHoc({ + setup() { + const schema = toTypedSchema( + zod.object({ + email: zod.string().email(EMAIL_MSG).min(7, MIN_MSG), + password: zod.string().min(8, MIN_MSG), + }) + ); + + const { useFieldModel, errorBag } = useForm({ + validationSchema: schema, + validateOnMount: true, + }); + + const [email, password] = useFieldModel(['email', 'password']); + + return { + schema, + email, + password, + errorBag, + }; + }, + template: ` +
+ + {{ errorBag.email?.join(',') }} + + + {{ errorBag.password?.join(',') }} +
+ `, + }); + + const email = wrapper.$el.querySelector('#email'); + const password = wrapper.$el.querySelector('#password'); + const emailError = wrapper.$el.querySelector('#emailErr'); + const passwordError = wrapper.$el.querySelector('#passwordErr'); + + await flushPromises(); + + setValue(email, 'hello@'); + setValue(password, '1234'); + await flushPromises(); + + expect(emailError.textContent).toBe([EMAIL_MSG, MIN_MSG].join(',')); + expect(passwordError.textContent).toBe([MIN_MSG].join(',')); + + setValue(email, 'hello@email.com'); + setValue(password, '12346789'); + await flushPromises(); + + expect(emailError.textContent).toBe(''); + expect(passwordError.textContent).toBe(''); +}); + test('validates typed schema form with yup', async () => { const wrapper = mountWithHoc({ setup() {