From 8db407785c5611c10c221eabd747c3f31770145b Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 20 Mar 2022 16:56:01 +0100 Subject: [PATCH] feat: chain of GenericValidateFunction in useField (#3725) (#3726) --- packages/vee-validate/src/useField.ts | 5 +- packages/vee-validate/src/validate.ts | 38 +++- packages/vee-validate/tests/useField.spec.ts | 219 +++++++++++++++++++ 3 files changed, 258 insertions(+), 4 deletions(-) diff --git a/packages/vee-validate/src/useField.ts b/packages/vee-validate/src/useField.ts index 9cbe1991b..763d966e0 100644 --- a/packages/vee-validate/src/useField.ts +++ b/packages/vee-validate/src/useField.ts @@ -57,6 +57,7 @@ export type RuleExpression = | string | Record | GenericValidateFunction + | GenericValidateFunction[] | YupValidator | BaseSchema | undefined; @@ -114,7 +115,7 @@ function _useField( rulesValue = extractRuleFromSchema(schema, unref(name)) || rulesValue; } - if (isYupValidator(rulesValue) || isCallable(rulesValue)) { + if (isYupValidator(rulesValue) || isCallable(rulesValue) || Array.isArray(rulesValue)) { return rulesValue; } @@ -296,7 +297,7 @@ function _useField( const dependencies = computed(() => { const rulesVal = normalizedRules.value; // is falsy, a function schema or a yup schema - if (!rulesVal || isCallable(rulesVal) || isYupValidator(rulesVal)) { + if (!rulesVal || isCallable(rulesVal) || isYupValidator(rulesVal) || Array.isArray(rulesVal)) { return {}; } diff --git a/packages/vee-validate/src/validate.ts b/packages/vee-validate/src/validate.ts index 31459fbc9..3ee619563 100644 --- a/packages/vee-validate/src/validate.ts +++ b/packages/vee-validate/src/validate.ts @@ -10,7 +10,7 @@ import { isCallable, FieldValidationMetaInfo } from '../../shared'; */ interface FieldValidationContext { name: string; - rules: GenericValidateFunction | YupValidator | string | Record; + rules: GenericValidateFunction | GenericValidateFunction[] | YupValidator | string | Record; bails: boolean; formData: Record; } @@ -26,7 +26,12 @@ interface ValidationOptions { */ export async function validate( value: unknown, - rules: string | Record | GenericValidateFunction | YupValidator, + rules: + | string + | Record + | GenericValidateFunction + | GenericValidateFunction[] + | YupValidator, options: ValidationOptions = {} ): Promise { const shouldBail = options?.bails; @@ -71,6 +76,35 @@ async function _validate(field: FieldValidationContext, value: unknown) { }; } + // chain of generic functions + if (Array.isArray(field.rules)) { + const ctx = { + field: field.name, + form: field.formData, + value: value, + }; + const length = field.rules.length; + const errors: ReturnType[] = []; + for (let i = 0; i < length; i++) { + const rule = field.rules[i]; + const result = await rule(value, ctx); + const isValid = typeof result !== 'string' && result; + const message = typeof result === 'string' ? result : _generateFieldError(ctx); + if (!isValid) { + if (field.bails) { + return { + errors: [message], + }; + } else { + errors.push(message); + } + } + } + return { + errors, + }; + } + const normalizedContext = { ...field, rules: normalizeRules(field.rules), diff --git a/packages/vee-validate/tests/useField.spec.ts b/packages/vee-validate/tests/useField.spec.ts index f8323d3b2..0892a8b4b 100644 --- a/packages/vee-validate/tests/useField.spec.ts +++ b/packages/vee-validate/tests/useField.spec.ts @@ -267,4 +267,223 @@ describe('useField()', () => { expect(error?.textContent).toBe(REQUIRED_MESSAGE); }); }); + + describe('generic function chains', () => { + test('when bails is true', async () => { + mountWithHoc({ + setup() { + const { + value: value1, + meta: meta1, + errors: errors1, + resetField: reset1, + } = useField('field', [ + val => (val ? true : REQUIRED_MESSAGE), + val => ((val as string)?.length >= 3 ? true : MIN_MESSAGE), + ]); + const { + value: value2, + meta: meta2, + errors: errors2, + resetField: reset2, + } = useField('field', [ + val => ((val as string)?.length >= 3 ? true : MIN_MESSAGE), + val => (val ? true : REQUIRED_MESSAGE), + ]); + + return { + value1, + value2, + meta1, + meta2, + errors1, + errors2, + reset1, + reset2, + }; + }, + template: ` + + {{ meta1.valid ? 'valid' : 'invalid' }} + {{ errors1.length }} + {{ e }} + + + {{ meta2.valid ? 'valid' : 'invalid' }} + {{ errors2.length }} + {{ e }} + + `, + }); + + const input1 = document.querySelector('#input1') as HTMLInputElement; + const meta1 = document.querySelector('#meta1'); + const errors1 = document.querySelector('#errors1'); + const input2 = document.querySelector('#input2') as HTMLInputElement; + const meta2 = document.querySelector('#meta2'); + const errors2 = document.querySelector('#errors2'); + + await flushPromises(); + + expect(meta1?.textContent).toBe('invalid'); + expect(meta2?.textContent).toBe('invalid'); + setValue(input1, ''); + setValue(input2, ''); + + await flushPromises(); + + let errorMessage10 = document.querySelector('#errormessage10'); + let errorMessage20 = document.querySelector('#errormessage20'); + + expect(meta1?.textContent).toBe('invalid'); + expect(meta2?.textContent).toBe('invalid'); + expect(errors1?.textContent).toBe('1'); + expect(errors2?.textContent).toBe('1'); + expect(errorMessage10?.textContent).toBe(REQUIRED_MESSAGE); + expect(errorMessage20?.textContent).toBe(MIN_MESSAGE); + + setValue(input1, '12'); + setValue(input2, '12'); + + await flushPromises(); + + errorMessage10 = document.querySelector('#errormessage10'); + errorMessage20 = document.querySelector('#errormessage20'); + + expect(meta1?.textContent).toBe('invalid'); + expect(meta2?.textContent).toBe('invalid'); + expect(errors1?.textContent).toBe('1'); + expect(errors2?.textContent).toBe('1'); + expect(errorMessage10?.textContent).toBe(MIN_MESSAGE); + expect(errorMessage20?.textContent).toBe(MIN_MESSAGE); + + setValue(input1, '123'); + setValue(input2, '123'); + + await flushPromises(); + + expect(meta1?.textContent).toBe('valid'); + expect(meta2?.textContent).toBe('valid'); + expect(errors1?.textContent).toBe('0'); + expect(errors2?.textContent).toBe('0'); + + // trigger reset + (document.querySelector('#r1') as HTMLButtonElement).click(); + (document.querySelector('#r2') as HTMLButtonElement).click(); + await flushPromises(); + expect(meta1?.textContent).toBe('invalid'); + expect(meta2?.textContent).toBe('invalid'); + }); + test('when bails is false', async () => { + mountWithHoc({ + setup() { + const { + value: value1, + meta: meta1, + errors: errors1, + resetField: reset1, + } = useField( + 'field', + [val => (val ? true : REQUIRED_MESSAGE), val => ((val as string)?.length >= 3 ? true : MIN_MESSAGE)], + { bails: false } + ); + const { + value: value2, + meta: meta2, + errors: errors2, + resetField: reset2, + } = useField( + 'field', + [val => ((val as string)?.length >= 3 ? true : MIN_MESSAGE), val => (val ? true : REQUIRED_MESSAGE)], + { bails: false } + ); + + return { + value1, + value2, + meta1, + meta2, + errors1, + errors2, + reset1, + reset2, + }; + }, + template: ` + + {{ meta1.valid ? 'valid' : 'invalid' }} + {{ errors1.length }} + {{ e }} + + + {{ meta2.valid ? 'valid' : 'invalid' }} + {{ errors2.length }} + {{ e }} + + `, + }); + + const input1 = document.querySelector('#input1') as HTMLInputElement; + const meta1 = document.querySelector('#meta1'); + const errors1 = document.querySelector('#errors1'); + const input2 = document.querySelector('#input2') as HTMLInputElement; + const meta2 = document.querySelector('#meta2'); + const errors2 = document.querySelector('#errors2'); + + await flushPromises(); + + expect(meta1?.textContent).toBe('invalid'); + expect(meta2?.textContent).toBe('invalid'); + setValue(input1, ''); + setValue(input2, ''); + + await flushPromises(); + + let errorMessage10 = document.querySelector('#errormessage10'); + const errorMessage11 = document.querySelector('#errormessage11'); + let errorMessage20 = document.querySelector('#errormessage20'); + const errorMessage21 = document.querySelector('#errormessage21'); + + expect(meta1?.textContent).toBe('invalid'); + expect(meta2?.textContent).toBe('invalid'); + expect(errors1?.textContent).toBe('2'); + expect(errors2?.textContent).toBe('2'); + expect(errorMessage10?.textContent).toBe(REQUIRED_MESSAGE); + expect(errorMessage11?.textContent).toBe(MIN_MESSAGE); + expect(errorMessage20?.textContent).toBe(MIN_MESSAGE); + expect(errorMessage21?.textContent).toBe(REQUIRED_MESSAGE); + + setValue(input1, '12'); + setValue(input2, '12'); + + await flushPromises(); + + errorMessage10 = document.querySelector('#errormessage10'); + errorMessage20 = document.querySelector('#errormessage20'); + + expect(meta1?.textContent).toBe('invalid'); + expect(meta2?.textContent).toBe('invalid'); + expect(errors1?.textContent).toBe('1'); + expect(errors2?.textContent).toBe('1'); + expect(errorMessage10?.textContent).toBe(MIN_MESSAGE); + expect(errorMessage20?.textContent).toBe(MIN_MESSAGE); + + setValue(input1, '123'); + setValue(input2, '123'); + + await flushPromises(); + + expect(meta1?.textContent).toBe('valid'); + expect(meta2?.textContent).toBe('valid'); + expect(errors1?.textContent).toBe('0'); + expect(errors2?.textContent).toBe('0'); + + // trigger reset + (document.querySelector('#r1') as HTMLButtonElement).click(); + (document.querySelector('#r2') as HTMLButtonElement).click(); + await flushPromises(); + expect(meta1?.textContent).toBe('invalid'); + expect(meta2?.textContent).toBe('invalid'); + }); + }); });