Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: chain of GenericValidateFunction in useField (#3725) #3726

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/vee-validate/src/useField.ts
Expand Up @@ -57,6 +57,7 @@ export type RuleExpression<TValue> =
| string
| Record<string, unknown>
| GenericValidateFunction
| GenericValidateFunction[]
| YupValidator
| BaseSchema<TValue>
| undefined;
Expand Down Expand Up @@ -114,7 +115,7 @@ function _useField<TValue = unknown>(
rulesValue = extractRuleFromSchema<TValue>(schema, unref(name)) || rulesValue;
}

if (isYupValidator(rulesValue) || isCallable(rulesValue)) {
if (isYupValidator(rulesValue) || isCallable(rulesValue) || Array.isArray(rulesValue)) {
return rulesValue;
}

Expand Down Expand Up @@ -296,7 +297,7 @@ function _useField<TValue = unknown>(
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 {};
}

Expand Down
38 changes: 36 additions & 2 deletions packages/vee-validate/src/validate.ts
Expand Up @@ -10,7 +10,7 @@ import { isCallable, FieldValidationMetaInfo } from '../../shared';
*/
interface FieldValidationContext {
name: string;
rules: GenericValidateFunction | YupValidator | string | Record<string, unknown>;
rules: GenericValidateFunction | GenericValidateFunction[] | YupValidator | string | Record<string, unknown>;
bails: boolean;
formData: Record<string, unknown>;
}
Expand All @@ -26,7 +26,12 @@ interface ValidationOptions {
*/
export async function validate(
value: unknown,
rules: string | Record<string, unknown | unknown[]> | GenericValidateFunction | YupValidator,
rules:
| string
| Record<string, unknown | unknown[]>
| GenericValidateFunction
| GenericValidateFunction[]
| YupValidator,
options: ValidationOptions = {}
): Promise<ValidationResult> {
const shouldBail = options?.bails;
Expand Down Expand Up @@ -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<typeof _generateFieldError>[] = [];
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),
Expand Down
219 changes: 219 additions & 0 deletions packages/vee-validate/tests/useField.spec.ts
Expand Up @@ -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: `
<input id="input1" name="field" v-model="value1" />
<span id="meta1">{{ meta1.valid ? 'valid' : 'invalid' }}</span>
<span id="errors1">{{ errors1.length }}</span>
<span v-for="(e, idx) in errors1" :id="'errormessage1' + idx">{{ e }}</span>
<button id="r1" @click="reset1()">Reset</button>
<input id="input2" name="field" v-model="value2" />
<span id="meta2">{{ meta2.valid ? 'valid' : 'invalid' }}</span>
<span id="errors2">{{ errors2.length }}</span>
<span v-for="(e, idx) in errors2" :id="'errormessage2' + idx">{{ e }}</span>
<button id="r2" @click="reset2()">Reset</button>
`,
});

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: `
<input id="input1" name="field" v-model="value1" />
<span id="meta1">{{ meta1.valid ? 'valid' : 'invalid' }}</span>
<span id="errors1">{{ errors1.length }}</span>
<span v-for="(e, idx) in errors1" :id="'errormessage1' + idx">{{ e }}</span>
<button id="r1" @click="reset1()">Reset</button>
<input id="input2" name="field" v-model="value2" />
<span id="meta2">{{ meta2.valid ? 'valid' : 'invalid' }}</span>
<span id="errors2">{{ errors2.length }}</span>
<span v-for="(e, idx) in errors2" :id="'errormessage2' + idx">{{ e }}</span>
<button id="r2" @click="reset2()">Reset</button>
`,
});

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');
});
});
});