Skip to content

Commit 0857677

Browse files
logaretmclaude
andcommitted
fix: global and local rules can be combined on same field (#5025)
When both global rules (via defineRule) and local/inline validator functions are used on the same field, they can now be combined using either the object syntax or the array syntax. Object syntax: { required: true, myLocalRule: myValidatorFn } Array syntax: ['required', myValidatorFn] Previously, the object syntax would throw "No such validator" because function values were treated as rule parameters rather than inline validators. The array syntax would throw "rule is not a function" because string entries were called directly instead of being resolved as global rules. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7d8cc52 commit 0857677

4 files changed

Lines changed: 177 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"vee-validate": patch
3+
---
4+
5+
Allow combining global and local validation rules on the same field (#5025)

packages/vee-validate/src/useField.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,15 @@ function _useField<TValue = unknown>(
119119
return rulesValue;
120120
}
121121

122+
// If the rules object contains function values (inline validators mixed with global rules),
123+
// pass it through as-is so that _validate can handle both types (#5025)
124+
if (rulesValue && typeof rulesValue === 'object') {
125+
const hasInlineFns = Object.values(rulesValue).some(v => isCallable(v));
126+
if (hasInlineFns) {
127+
return rulesValue;
128+
}
129+
}
130+
122131
return normalizeRules(rulesValue);
123132
});
124133

packages/vee-validate/src/validate.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,30 @@ async function _validate<TInput = unknown, TOutput = TInput>(
9696

9797
for (let i = 0; i < length; i++) {
9898
const rule = pipeline[i];
99+
100+
// Handle string rules in arrays by resolving them as global rules
101+
if (typeof rule === 'string') {
102+
const stringRules = normalizeRules(rule);
103+
const ruleNames = Object.keys(stringRules);
104+
for (let j = 0; j < ruleNames.length; j++) {
105+
const normalizedContext = {
106+
...field,
107+
rules: stringRules,
108+
};
109+
const testResult = await _test(normalizedContext, value, {
110+
name: ruleNames[j],
111+
params: stringRules[ruleNames[j]],
112+
});
113+
if (testResult.error) {
114+
errors.push(testResult.error);
115+
if (field.bails) {
116+
return { errors };
117+
}
118+
}
119+
}
120+
continue;
121+
}
122+
99123
const result = await rule(value, ctx);
100124
const isValid = typeof result !== 'string' && !Array.isArray(result) && result;
101125
if (isValid) {
@@ -121,6 +145,77 @@ async function _validate<TInput = unknown, TOutput = TInput>(
121145
};
122146
}
123147

148+
// If it's an object, separate function-valued rules from global rule references
149+
if (typeof rules === 'object') {
150+
// Check if the object contains any function values that should be treated as inline validators
151+
const inlineFns: GenericValidateFunction<TInput>[] = [];
152+
const globalRulesObj: Record<string, unknown> = {};
153+
for (const key of Object.keys(rules)) {
154+
if (isCallable(rules[key])) {
155+
inlineFns.push(rules[key] as GenericValidateFunction<TInput>);
156+
} else {
157+
globalRulesObj[key] = rules[key];
158+
}
159+
}
160+
161+
const errors: ReturnType<typeof _generateFieldError>[] = [];
162+
163+
// Run global rules first
164+
if (Object.keys(globalRulesObj).length) {
165+
const normalizedContext = {
166+
...field,
167+
rules: normalizeRules(globalRulesObj),
168+
};
169+
const rulesKeys = Object.keys(normalizedContext.rules);
170+
for (let i = 0; i < rulesKeys.length; i++) {
171+
const rule = rulesKeys[i];
172+
const result = await _test(normalizedContext, value, {
173+
name: rule,
174+
params: normalizedContext.rules[rule],
175+
});
176+
177+
if (result.error) {
178+
errors.push(result.error);
179+
if (field.bails) {
180+
return { errors };
181+
}
182+
}
183+
}
184+
}
185+
186+
// Run inline function validators
187+
if (inlineFns.length) {
188+
const ctx = {
189+
field: field.label || field.name,
190+
name: field.name,
191+
label: field.label,
192+
form: field.formData,
193+
value,
194+
};
195+
196+
for (let i = 0; i < inlineFns.length; i++) {
197+
const result = await inlineFns[i](value, ctx);
198+
const isValid = typeof result !== 'string' && !Array.isArray(result) && result;
199+
if (isValid) {
200+
continue;
201+
}
202+
203+
if (Array.isArray(result)) {
204+
errors.push(...result);
205+
} else {
206+
const message = typeof result === 'string' ? result : _generateFieldError(ctx);
207+
errors.push(message);
208+
}
209+
210+
if (field.bails) {
211+
return { errors };
212+
}
213+
}
214+
}
215+
216+
return { errors };
217+
}
218+
124219
const normalizedContext = {
125220
...field,
126221
rules: normalizeRules(rules),

packages/vee-validate/tests/validate.spec.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,71 @@ test('target params are filled in the params in message context', async () => {
4141
generateMessage: original,
4242
});
4343
});
44+
45+
// #5025
46+
test('global and local rules can be combined in object syntax', async () => {
47+
defineRule('required', (value: any) => {
48+
if (!value && value !== 0) {
49+
return 'This field is required';
50+
}
51+
return true;
52+
});
53+
54+
const myLocalRule = (value: any) => {
55+
if (typeof value === 'string' && value.length < 3) {
56+
return 'Must be at least 3 characters';
57+
}
58+
return true;
59+
};
60+
61+
// Both global and local rules should run - valid value
62+
let result = await validate('hello', { required: true, myLocalRule } as any);
63+
expect(result.valid).toBe(true);
64+
expect(result.errors).toHaveLength(0);
65+
66+
// Global rule fails
67+
result = await validate('', { required: true, myLocalRule } as any);
68+
expect(result.valid).toBe(false);
69+
expect(result.errors).toContain('This field is required');
70+
71+
// Local rule fails (bails: false to see both errors)
72+
result = await validate('ab', { required: true, myLocalRule } as any, { bails: false });
73+
expect(result.valid).toBe(false);
74+
expect(result.errors).toContain('Must be at least 3 characters');
75+
76+
// Both rules pass
77+
result = await validate('abc', { required: true, myLocalRule } as any);
78+
expect(result.valid).toBe(true);
79+
});
80+
81+
// #5025
82+
test('global and local rules can be combined in array syntax', async () => {
83+
defineRule('required', (value: any) => {
84+
if (!value && value !== 0) {
85+
return 'This field is required';
86+
}
87+
return true;
88+
});
89+
90+
const myLocalRule = (value: any) => {
91+
if (typeof value === 'string' && value.length < 3) {
92+
return 'Must be at least 3 characters';
93+
}
94+
return true;
95+
};
96+
97+
// Both string global rules and function local rules in an array
98+
let result = await validate('hello', ['required', myLocalRule] as any);
99+
expect(result.valid).toBe(true);
100+
expect(result.errors).toHaveLength(0);
101+
102+
// Global rule fails
103+
result = await validate('', ['required', myLocalRule] as any);
104+
expect(result.valid).toBe(false);
105+
expect(result.errors).toContain('This field is required');
106+
107+
// Local rule fails
108+
result = await validate('ab', ['required', myLocalRule] as any, { bails: false });
109+
expect(result.valid).toBe(false);
110+
expect(result.errors).toContain('Must be at least 3 characters');
111+
});

0 commit comments

Comments
 (0)