isValid returns false with no errors when using skipWhen
martinglover opened this issue · comments
vest@4.0.0-next-6abc96
skipWhen
is causing isValid
to return false
when there are no errors.
Example based on a toggle input, different fields are required the the definition below will return no errors as expected, but the result from isValid()
is false
I would expect the rules for a skipWhen to provide the same results for errors and isValid
import { create, test, enforce, skipWhen } from 'vest';
const validator = create((data, field) => {
skipWhen(data.toggleBoolean, () => {
test('requiredField', 'Required', () => {
enforce(data.requiredField).isNotEmpty();
});
});
skipWhen(!data.toggleBoolean, () => {
test('otherRequiredField', 'Required', () => {
enforce(data.otherRequiredField).isNotEmpty();
});
});
});
const validated = validator({
toggleBoolean: true,
requiredField: null,
otherRequiredField: 'content',
});
console.log(validated.isValid()); // false
console.log(validated.getErrors()); // {}
const validated = validator({
toggleBoolean: true,
requiredField: 'content',
otherRequiredField: 'content',
});
console.log(validated.isValid()); // false
console.log(validated.getErrors()); // {}
I have to define optional
against the opposite skipWhen states to get isValid
to return the matching results of errors
const validator = create((data, field) => {
skipWhen(data.toggleBoolean, () => {
optional('otherRequiredField')
test('requiredField', 'Required', () => {
enforce(data.requiredField).isNotEmpty();
});
});
skipWhen(!data.toggleBoolean, () => {
optional('requiredField')
test('otherRequiredField', 'Required', () => {
enforce(data.otherRequiredField).isNotEmpty();
});
});
});
const validated = validator({
toggleBoolean: true,
requiredField: null,
otherRequiredField: 'content',
});
console.log(validated.isValid()); // true
console.log(validated.getErrors()); // {}
Hey,
Briefly looking - it seems like isValid is working as expected. It is important to understand, isValid is not the negation of isInvalid.
This means that isValid -> false
does not necessarily mean that the form is invalid, but instead, in your case, it means that it might not be valid-yet.
Vest assumes that all fields in the suite are required by default, unless specifically set as optional. As long as not all fields are passing, the suite won't be considered as valid.
The main distinction between the two:
isValid
-> all required fields are passing
isInValid
-> the suite has errors, AND/OR some app specific business logic that Vest is not aware of.
Because Vest cannot be aware of feature specific business logic, it doesn't provide an isInvalid
flag, but it does give you all the information about the suite to infer that from the suite state.
I believe that your case is the exact scenario for which optional
was created. I would try making the following changes.
- Remove
skipWhen
- at least from your example, it doesn't look like it does much. Maybe in your actual app it serves a purpose, but if the field can't be interacted with, and is optional to begin with, you can possibly go without skipWhen. - Use conditional
optional
: Since you're using vest@next, you get more flexibility in howoptional
works.
https://vest.vercel.app/docs/writing_your_suite/optional_fields#advanced-usage---supplying-custom-omission-function
Here's something that I came up with that might be useful in your scenario. Not sure if I got all the details correct, but it looks like it could work.
import { create, test, enforce, optional } from "vest";
const suite = create((data) => {
optional({
otherRequiredField: () => !data.toggleBoolean,
requiredField: () => data.toggleBoolean,
});
test("requiredField", "Required", () => {
enforce(data.requiredField).isNotEmpty();
});
test("otherRequiredField", "Required", () => {
enforce(data.otherRequiredField).isNotEmpty();
});
});
const validated = suite({
toggleBoolean: true,
requiredField: null,
otherRequiredField: "content",
});
Apart from that - I noticed that in your example, you used optional
within skipWhen. At the moment optional and skipWhen do not interact - meaning, putting it inside skipWhen doesn't do anything. Would you expect that it does? I designed skipWhen to only filter out tests, and that everything else (only, skip, optional) would be declared at the top of the suite. Do you think even with my suggestion applied, you would still find it more useful to have a nested optional?
My fear was that someone might set a field as optional after its tests were declared - but maybe your approach is more convenient?
Either way - I hope that helps. If not, let's try to find another approach.
Thank you for getting back quickly.
I think I may have provided an over simplified reproducing example and confused the issue, below is a closer to real world example
I'm was using skipWhen
to skip multiple test depending on a checkbox state
When useNewAddress
is true
I want selectedAddress
and primaryAddress
as required fields and the definitions within the skipWhen
ignored when useNewAddress
is false
When useNewAddress
is false
I want addressLine1
, addressTown
and addressPostCode
as required fields and the definitions within the skipWhen
ignored when useNewAddress
is true
But what I'm seeing from isValid
does not match what I'm seeing form errors, isValid
is processing the validation rules defined within the skipWhen
not skipping them
My intention is not to use optional
at all in these cases, this was a work around example to get isValid
to return the expected result. I was expecting the field definitions within the skipWhen
to not be processed so optional
would not be required to be specified for the opposite state
I hope this provides a clearer picture of the issue
If defining optional is just the way things work it's a small price for the simplicity vest provides for validation
import { create, test, enforce, skipWhen, only } from 'vest';
const testData = {
useNewAddress: true, // define if new address or existing address
selectedAddress: null, // required only when useNewAddress is false
primaryAddress: null, // required only when useNewAddress is false
addressLine1: 'line 1', // required only when useNewAddress is true
addressTown: 'town', // required only when useNewAddress is true
addressPostCode: 'postcode', // required only when useNewAddress is true
};
const validator = create((data, field = null) => {
only(field);
skipWhen(data.useNewAddress, () => {
test('selectedAddress', 'Required', () => {
enforce(data.selectedAddress).isNotEmpty();
});
test('selectedAddress', 'Must be a number', () => {
enforce(data.selectedAddress).isNumber();
});
test('primaryAddress', 'Required', () => {
enforce(data.primaryAddress).isBoolean();
});
});
skipWhen(!data.useNewAddress, () => {
test('addressLine1', 'Required', () => {
enforce(data.addressLine1).isNotEmpty();
});
test('addressLine1', 'Required', () => {
enforce(data.addressLine1).isString();
});
test('addressTown', 'Required', () => {
enforce(data.addressTown).isNotEmpty();
});
test('addressTown', 'Must be a string', () => {
enforce(data.addressTown).isString();
});
test('addressPostCode', 'Required', () => {
enforce(data.addressPostCode).isNotEmpty();
});
test('addressPostCode', 'Must be a string', () => {
enforce(data.addressPostCode).isString();
});
});
});
const validated = validator(testData);
console.log(validated.isValid()); // false
console.log(validated.getErrors()); // {}
I see. Yes, this adds some clarity. At the moment, optional is the best way to achieve what you're after. There are a few alternatives, but they might put some more burden n you during development. For example, if you can guarantee that for the lifetime of the suite data.useNewAddress
will remain unchanged, you can wrap it with an if/else block, and have Vest "not see" the ignored fields - but since it is being controlled by the user, it means that you would have to re create()
the suite every time the checkbox is toggled. Not ideal.
Alternatively - I raised an idea in #737 that might be helpful in solving this. I'm going to fiddle with it today, and hopefully release a testing version that we can play with these ideas. Would you be willing to help out by testing it?
@martinglover I just released vest@4.0.0-dev-fc2efe
for testing
You can npm i vest@4.0.0-dev-fc2efe
to install it.
It might be useful in your situation. It is a slight change, but you can see it as skipWhen-meets-optional. The way functional-optional works is by omitting tests that returns true
in the optional function. This testing version does the same for all tests within skipWhen
. I think this tiny change can be the solution for your scenario as well.
If you're interested in what changed, here's the diff: #738 (files)
Let me know if that works for you. If it does, I'll try to find a way to introduce it to Vest. It's a good thing that V4 is not released yet, so we can test out these changes before it reaches mass audience.
Thank you for your feedback and helping make Vest better 🙏🏻
Sorry for the delayed response, I've tested the latest next
release with the omitWhen
and it performs exactly as needed for my scenario
Thank you for the quick implementation of this update
Amazing! Glad that I was able to make that work out for you.
Thanks for all the feedback and making the scenario clear. It really helped me shape this feature 🙏