diff --git a/.changeset/slimy-laws-drive.md b/.changeset/slimy-laws-drive.md new file mode 100644 index 000000000..01b6ec1d7 --- /dev/null +++ b/.changeset/slimy-laws-drive.md @@ -0,0 +1,5 @@ +--- +'vee-validate': patch +--- + +feat: allow multiple messages in a validator fn closes #4322 #4318 diff --git a/packages/vee-validate/src/types/common.ts b/packages/vee-validate/src/types/common.ts index dce145b0f..48ea5a470 100644 --- a/packages/vee-validate/src/types/common.ts +++ b/packages/vee-validate/src/types/common.ts @@ -6,6 +6,8 @@ export type GenericObject = Record; export type MaybeArray = T | T[]; +export type MaybePromise = T | Promise; + export type MapValuesPathsToRefs< TValues extends GenericObject, TPaths extends readonly [...MaybeRef>[]] diff --git a/packages/vee-validate/src/types/forms.ts b/packages/vee-validate/src/types/forms.ts index ae9ec2429..8b628b33f 100644 --- a/packages/vee-validate/src/types/forms.ts +++ b/packages/vee-validate/src/types/forms.ts @@ -1,5 +1,5 @@ import { ComputedRef, DeepReadonly, Ref, MaybeRef, MaybeRefOrGetter } from 'vue'; -import { MapValuesPathsToRefs, GenericObject } from './common'; +import { MapValuesPathsToRefs, GenericObject, MaybeArray, MaybePromise } from './common'; import { FieldValidationMetaInfo } from '../../../shared'; import { Path, PathValue } from './paths'; import { PartialDeep } from 'type-fest'; @@ -149,7 +149,7 @@ export type FieldContext = Omit, ' export type GenericValidateFunction = ( value: TValue, ctx: FieldValidationMetaInfo -) => boolean | string | Promise; +) => MaybePromise>; export interface FormState { values: PartialDeep; diff --git a/packages/vee-validate/src/validate.ts b/packages/vee-validate/src/validate.ts index 46f3208a7..ee0de3fb7 100644 --- a/packages/vee-validate/src/validate.ts +++ b/packages/vee-validate/src/validate.ts @@ -93,13 +93,17 @@ async function _validate(field: FieldValidationContext for (let i = 0; i < length; i++) { const rule = pipeline[i]; const result = await rule(value, ctx); - const isValid = typeof result !== 'string' && result; + const isValid = typeof result !== 'string' && !Array.isArray(result) && result; if (isValid) { continue; } - const message = typeof result === 'string' ? result : _generateFieldError(ctx); - errors.push(message); + if (Array.isArray(result)) { + errors.push(...result); + } else { + const message = typeof result === 'string' ? result : _generateFieldError(ctx); + errors.push(message); + } if (field.bails) { return { diff --git a/packages/vee-validate/tests/useField.spec.ts b/packages/vee-validate/tests/useField.spec.ts index ea4becf9e..d0d16769f 100644 --- a/packages/vee-validate/tests/useField.spec.ts +++ b/packages/vee-validate/tests/useField.spec.ts @@ -893,4 +893,24 @@ describe('useField()', () => { await flushPromises(); expect(field.value.value).toBe(123); }); + + test('a validator can return multiple messages', async () => { + let field!: FieldContext; + const validator = vi.fn(val => (val ? true : [REQUIRED_MESSAGE, 'second'])); + + mountWithHoc({ + setup() { + field = useField('field', validator); + + return {}; + }, + }); + + await flushPromises(); + expect(field.errors.value).toHaveLength(0); + await field.validate(); + await flushPromises(); + expect(field.errors.value).toHaveLength(2); + expect(field.errors.value).toEqual([REQUIRED_MESSAGE, 'second']); + }); });