Skip to content

Commit 0802512

Browse files
committed
feat: support yup validation schemas on field-level
1 parent f16abb2 commit 0802512

3 files changed

Lines changed: 67 additions & 2 deletions

File tree

packages/core/src/utils/rules.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ export function normalizeRules(rules: any): GenericValidateFunction | Record<str
1919
return acc;
2020
}
2121

22-
// If its a single validate function, leave as is.
23-
if (isCallable(rules)) {
22+
// If its a single validate function or a yup fn, leave as is.
23+
if (isCallable(rules) || isCallable(rules && rules.validate)) {
2424
return rules;
2525
}
2626

packages/core/src/validate.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ async function _validate(field: FieldValidationContext, value: any) {
7474
};
7575
}
7676

77+
if (isCallable(field.rules?.validate)) {
78+
return validateWithYup(field, value);
79+
}
80+
7781
const errors: ReturnType<typeof _generateFieldError>[] = [];
7882
const rules = Object.keys(field.rules);
7983
const length = rules.length;
@@ -101,6 +105,32 @@ async function _validate(field: FieldValidationContext, value: any) {
101105
};
102106
}
103107

108+
/**
109+
* Handles yup validation
110+
*/
111+
async function validateWithYup(field: FieldValidationContext, value: any) {
112+
const result = await field.rules
113+
.validate(value)
114+
.then(() => true)
115+
.catch((err: Error) => {
116+
// Yup errors have a name prop one them.
117+
// https://github.com/jquense/yup#validationerrorerrors-string--arraystring-value-any-path-string
118+
if (err.name === 'ValidationError') {
119+
return err.message;
120+
}
121+
122+
// re-throw the error so we don't hide it
123+
throw err;
124+
});
125+
126+
const isValid = typeof result !== 'string' && result;
127+
128+
return {
129+
valid: isValid,
130+
errors: !isValid ? [{ msg: result, rule: '' }] : [],
131+
};
132+
}
133+
104134
/**
105135
* Tests a single input value against a rule.
106136
*/
@@ -117,6 +147,7 @@ async function _test(field: FieldValidationContext, value: any, rule: { name: st
117147
form: field.crossTable,
118148
rule,
119149
};
150+
120151
const result = await validator(value, params, ctx);
121152

122153
if (typeof result === 'string') {

packages/core/tests/Field.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import flushPromises from 'flush-promises';
22
import { defineRule } from '@vee-validate/core';
33
import { mountWithHoc, setValue, dispatchEvent } from './helpers';
4+
import * as yup from 'yup';
45

56
jest.useFakeTimers();
67

@@ -355,6 +356,39 @@ describe('<Field />', () => {
355356
expect(error.textContent).toBe(REQUIRED_MESSAGE);
356357
});
357358

359+
test('yup rules can be used', async () => {
360+
const wrapper = mountWithHoc({
361+
setup() {
362+
const rules = yup.string().required().min(8);
363+
364+
return {
365+
rules,
366+
};
367+
},
368+
template: `
369+
<div>
370+
<Field name="field" :rules="rules" v-slot="{ field, errors }">
371+
<input v-bind="field" type="text">
372+
<p>{{ errors[0] }}</p>
373+
</Field>
374+
</div>
375+
`,
376+
});
377+
378+
const input = wrapper.$el.querySelector('input');
379+
const error = wrapper.$el.querySelector('p');
380+
381+
setValue(input, '');
382+
await flushPromises();
383+
expect(error.textContent).toBe('this is a required field');
384+
setValue(input, '12');
385+
await flushPromises();
386+
expect(error.textContent).toBe('this must be at least 8 characters');
387+
setValue(input, '12345678');
388+
await flushPromises();
389+
expect(error.textContent).toBe('');
390+
});
391+
358392
test('avoids race conditions between successive validations', async () => {
359393
// A decreasing timeout (the most recent validation will finish before new ones).
360394
defineRule('longRunning', value => {

0 commit comments

Comments
 (0)