Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-4868-required-if-resubmit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vee-validate/rules": patch
---

Fix required_if rule not re-evaluating on subsequent submissions (#4868)
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changeset summary says this fixes required_if not re-evaluating on subsequent submissions, but the PR primarily re-introduces the required_if rule into @vee-validate/rules. Please align the changeset text with the actual change so the published changelog is accurate (e.g. mention re-adding/restoring the rule).

Copilot uses AI. Check for mistakes.
3 changes: 3 additions & 0 deletions packages/rules/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@
import one_of from './one_of';
import regex from './regex';
import required from './required';
import required_if from './required_if';
import size from './size';
import url from './url';
import { toTypedSchema } from './toTypedSchema';
import { SimpleValidationRuleFunction } from '../../shared/types';

export const all: Record<string, SimpleValidationRuleFunction<any, any>> = {

Check warning on line 32 in packages/rules/src/index.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

Check warning on line 32 in packages/rules/src/index.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
alpha_dash,
alpha_num,
alpha_spaces,
Expand All @@ -54,6 +55,7 @@
one_of,
regex,
required,
required_if,
size,
url,
};
Expand Down Expand Up @@ -84,6 +86,7 @@
one_of,
regex,
required,
required_if,
size,
url,
toTypedSchema,
Expand Down
36 changes: 36 additions & 0 deletions packages/rules/src/required_if.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { isEmpty } from './utils';
import { isEmptyArray, isNullOrUndefined } from '../../shared';

const requiredIfValidator = (
value: unknown,
params: [unknown, ...unknown[]] | { target: unknown; values?: unknown[] },
) => {
let target: unknown;
let values: unknown[] | undefined;

if (Array.isArray(params)) {
target = params[0];
values = params.slice(1);
} else {
target = params.target;
values = params.values;
}

// Determine if the field should be required
let isRequired: boolean;

if (values && values.length) {
// eslint-disable-next-line eqeqeq
isRequired = values.some(val => val == target);
} else {
isRequired = !isNullOrUndefined(target) && !isEmptyArray(target) && !!String(target).trim().length;
Comment on lines +25 to +26
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "truthy target" branch doesn't treat false as falsy, so a boolean target of false will still make the field required (because String(false).trim().length is non-zero). Consider using the same presence check as the required rule for the target (e.g. required(target)), so false/empty values don't trigger the condition unexpectedly.

Suggested change
} else {
isRequired = !isNullOrUndefined(target) && !isEmptyArray(target) && !!String(target).trim().length;
} else if (typeof target === 'boolean') {
// For boolean targets, only `true` should make the field required.
isRequired = target;
} else {
isRequired =
!isNullOrUndefined(target) &&
!isEmptyArray(target) &&
!!String(target).trim().length;

Copilot uses AI. Check for mistakes.
}

if (!isRequired) {
return true;
}

return !isEmpty(value);
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the condition is met, this returns !isEmpty(value), which doesn't match the semantics of the existing required rule (e.g. ' ' and false will incorrectly be considered valid). To keep behavior consistent across rules, delegate to the required validator (or mirror its trim/boolean handling) when isRequired is true.

Copilot uses AI. Check for mistakes.
};

export default requiredIfValidator;
66 changes: 66 additions & 0 deletions packages/rules/tests/required_if.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import validate from '../src/required_if';

test('validates required_if with array params', () => {
// target has a matching value, field is empty -> invalid
expect(validate('', ['Online Banking', 'Online Banking'])).toBe(false);
expect(validate(null, ['Online Banking', 'Online Banking'])).toBe(false);
expect(validate(undefined, ['Online Banking', 'Online Banking'])).toBe(false);
expect(validate([], ['Online Banking', 'Online Banking'])).toBe(false);

// target has a matching value, field is filled -> valid
expect(validate('Nabil', ['Online Banking', 'Online Banking'])).toBe(true);

// target does not match any value, field is empty -> valid (not required)
expect(validate('', ['Cash', 'Online Banking'])).toBe(true);
expect(validate(null, ['Cash', 'Online Banking'])).toBe(true);
expect(validate(undefined, ['Cash', 'Online Banking'])).toBe(true);
});
Comment on lines +3 to +17
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new tests don't cover parity with the existing required rule once the field becomes required (e.g. whitespace-only strings and false should fail, and 0 should pass). Adding those cases would prevent regressions and ensure required_if matches required behavior for the validated value.

Copilot uses AI. Check for mistakes.

test('validates required_if with object params', () => {
// target matches, field is empty -> invalid
expect(validate('', { target: 'Online Banking', values: ['Online Banking'] })).toBe(false);

// target matches, field is filled -> valid
expect(validate('Nabil', { target: 'Online Banking', values: ['Online Banking'] })).toBe(true);

// target does not match, field is empty -> valid
expect(validate('', { target: 'Cash', values: ['Online Banking'] })).toBe(true);
});

test('validates required_if with multiple comparison values', () => {
// target matches one of the values
expect(validate('', ['Online Banking', 'Online Banking', 'Wire Transfer'])).toBe(false);
expect(validate('', ['Wire Transfer', 'Online Banking', 'Wire Transfer'])).toBe(false);
expect(validate('Nabil', ['Online Banking', 'Online Banking', 'Wire Transfer'])).toBe(true);

// target matches none
expect(validate('', ['Cash', 'Online Banking', 'Wire Transfer'])).toBe(true);
});

test('validates required_if without comparison values (truthy target)', () => {
// target is truthy, no comparison values -> field is required
expect(validate('', ['some value'])).toBe(false);
expect(validate('filled', ['some value'])).toBe(true);

// target is empty/falsy, no comparison values -> field is not required
expect(validate('', [''])).toBe(true);
expect(validate('', [null])).toBe(true);
expect(validate('', [undefined])).toBe(true);
});

test('validates required_if with object params and no values array', () => {
// target is truthy -> required
expect(validate('', { target: 'truthy' })).toBe(false);
expect(validate('filled', { target: 'truthy' })).toBe(true);

// target is falsy -> not required
expect(validate('', { target: '' })).toBe(true);
expect(validate('', { target: null })).toBe(true);
expect(validate('', { target: undefined })).toBe(true);
});

test('uses loose equality for value comparison', () => {
// '1' == 1 should be true with loose equality
expect(validate('', ['1', 1])).toBe(false);
expect(validate('', [1, '1'])).toBe(false);
});
Loading