Returning true in Yup test function is not clearing the previous error message in react-hook-form formState - checkbox

Repo to duplicate the problem: https://codesandbox.io/s/yup-react-hook-form-material-checkbox-5dqbm
I am working on a form that I created with react-hook-form and yup. I have a checkbox group and I am trying to require at least one checkbox to be selected before submitting. I created a checkbox group with Material UI and handled validation with yup test function.
Here are my default values showing the structure.
const defaultValues = {
companyName: "",
singleCheckbox: false,
multiCheckbox: { option1: false, option2: false }
};
And this is my yup validation. Basically, I am checking if one of the options of multiCheckbox is true, return true otherwise return false.
const yupSchema = yup.object().shape({
companyName: yup.string().required("Company name is required"),
singleCheckbox: yup.boolean().test("singleCheckbox", "Required", (val) => {
console.log(val, "yup singleCheckbox result");
return val;
}),
multiCheckbox: yup
.object()
.shape({
option1: yup.boolean(),
option2: yup.boolean()
})
.test(
"multiCheckbox",
"At least one of the checkbox is required",
(options) => {
console.log(
options.option1 || options.option2,
"yup multiCheckbox result"
);
return options.option1 || options.option2;
}
)
});
Problem: When I hit 'Submit' button without filling out any field, I see all the error messages that I should. And error messages are disappearing when I start filling the input form and clicking on the single checkbox but error message for multiCheckbox is not going away.
First I thought something is wrong with the test function, that's why I implemented the same logic to singleCheckbox as well. singleCheckbox is working as expected but multiCheckbox is not, even though test function of yup returns true.
What I have tried: I tried to change revalidation value of useForm to onBlur and onChange but it didn't work out for me.
Observation: Clicking on Submit button brings all the error messages. After selecting one of the options of multiCheckbox and hitting submit button again clears error message of multiCheckbox. So I am assuming there is a disconnection between yup validation and react-hook-form validation. But why same disconnection is not happening for single singleCheckbox?

I found a workaround. Maybe someone will use it.
Create ref of clearErrors function and pass it to function returning schema so you can clear error when test is passed.
In schema test you cant use arrow function because you need this value to get field path.
const getSchema = (clearErrors) => yup.object().shape({
companyName: yup.string().required("Company name is required"),
singleCheckbox: yup.boolean().test("singleCheckbox", "Required", (val) => {
console.log(val, "yup singleCheckbox result");
return val;
}),
multiCheckbox: yup
.object()
.shape({
option1: yup.boolean(),
option2: yup.boolean()
})
.test(
"multiCheckbox",
"At least one of the checkbox is required",
function (this, options) {
console.log(
options.option1 || options.option2,
"yup multiCheckbox result"
);
if (options.option1 || options.option2) {
clearErrors(this.path);
return true;
}
return false;
}
)
});
const copiedClearErrors = useRef();
const formMethods = useForm({
resolver: yupResolver(getSchema(copiedClearErrors.current))
});
useEffect(() => {
copiedClearErrors.current = formMethods.clearErrors;
}, [formMethods.clearErrors]);

Related

Transform Yup validation error into a useable object

Problem
I have a formik form which needs to have 2 different validation schemas depending on what button the user uses to submit. I have seen some people say use state to decide which one but I want to avoid using state as it feels wrong in this case.
I have viewed Yup's documentation and it seems you can just validate using a schema directly and passing the values. This seems to work as I have shown in my example however the validation errors it returns are useless and I need to transform them to be able to use the Formik setErrors helper.
Yup validation as per documentation
let validationErrors = null;
try {
// Validate the form data using a custom Schema
await createDraftContractorFormValidationSchema.validate(values, { abortEarly: false, strict: false });
}
catch (errors: any) {
console.log(errors);
// What I essentially need here is a way to transform these errors
// into an object with the keys being the field that has errors and the message
if (errors) {
formikRef.current.setErrors(errors);
}
}
What gets logged
ValidationError: 4 errors occurred
at finishTestRun (runTests.js:54:1)
at runTests.js:8:1
at finishTestRun (runTests.js:54:1)
at runTests.js:8:1
at finishTestRun (runTests.js:54:1)
at createValidation.js:60:1
What I ended up doing I found in some obscure forum but thought it might be useful on Stack for others in the future. Credit for answer https://lightrun.com/answers/jaredpalmer-formik-yup-schemavalidate-options-show-every-error-of-a-field-at-the-same-time.
Essentially I created a helper method that transformed Yup's validation error into a useful object which you can pass directly into Formik's setErrors method.
Helper
/**
* TransformYupErrorsIntoObject
*
* #description Transform the useless yup error into a useable validation object
* #param {ValidationError} errors Yup validation errors
* #returns {Record<string, string>} Validation errors
*/
export const transformYupErrorsIntoObject = (errors: ValidationError): Record<string, string> => {
const validationErrors: Record<string, string> = {};
errors.inner.forEach((error: any) => {
if (error.path !== undefined) {
validationErrors[error.path] = error.errors[0];
}
});
return validationErrors;
};
Implementation
try {
// You need to define this schema as a model of your form
// Abort early will ensure all the fields validate not just one
await createDraftContractorFormValidationSchema.validate(values, { abortEarly: false, strict: false });
}
catch (error: any) {
// Transform the validationErrors
const validationErrors = transformYupErrorsIntoObject(error);
// Set errors on the field showing the user the errors
formikRef.current.setErrors(validationErrors);
return;
}
const [practiceData, setPracticeData] = useState({
firstName: '',
lastName: '',
role: '',
email: '',
phoneNumber: '',
})
const [Error, setError] = useState({ isErr: false, message: '' })
const handleSubmit = (e) => {
e.preventDefault()
schema
.validate(practiceData, { abortEarly: false })
.then((val) => console.log(val))
//I've got all the errors just by writing err.errors in message.
.catch((err) => setError({ isErr: true, message: err.errors }))
}
//Output
 ['First Name is required!', 'Last Name is required!', 'Role is required!', 'Email is required!', 'phoneNumber must be a `number` type, but the final value was: `NaN` (cast from the value `""`).']

yup schema .compact() method manipulates form data submitted

I've got a form using react-hook-form that validates submission by checking against a schema defined in yup.
It works well, but if I apply .compact() to remove false values in order to count how many true values in the array with .min(1), the data that comes out the other end of the handleSubmit(onSubmit) function has been manipulated by the schema validation process, and false values no longer reach the onSubmit function.
const schema = yup
.object({
roles: yup
.array()
.compact()
.min(1, "A user must have at least one role assigned"),
})
.required();
const MyForm = () => {
const { handleSubmit } = useForm({resolver: yupResolver(schema)});
const roles = ["foo", "bar", "baz"];
const onSubmit = ({roles}) => {
console.log(roles); // [true, true]
const selectedRoles = roles.filter((_r, i) => roleData[i]);
console.log(selectedRoles); // ["foo", "bar"]; <— incorrect roles due to loss of false values
}
<form onSubmit={handleSubmit(onSubmit)}>
// ...form
// example submission data from my form: `{ roles: [true, false, true] }`
// ✅ foo
// ❌ bar
// ✅ baz
</form>
};
Is there a way to stop yup from manipulating the output of the form, and only being used for schema validation?
If not, is there a way to return the full values after .compact() has removed some?
Is there a different way to validate that an array has at least x true values that won't manipulate the output of the form submission data?
Any help much appreciated.
I don't know if there's a way to stop yup from manipulating the output of the data submitted at the end, but I've found a way to do the equivalent of .compact().min(1) using the .test(name: string, message: string | function | any, test: function) method:
const schema = yup
.object({
roles: yup
.array()
.test(
"min-1-true",
"A user must have at least one role assigned",
value => {
const trueValues = value?.filter(v => v === true).length;
return trueValues >= 1;
},
),
})
.required();

Typescript Yup validation together with useSetRecoilState

I want to use Yups custom messages to use my useSetRecoilState, but I don't know how.
const setSnackbarError = useSetRecoilState(snackbarConfig)
if (!ValidateEmail(email)) {
setSnackbarError({
message: ('Enter missing data'),
severity: 'error'
})
} else {
everything is alrighty...
}
My yup schema:
const emailSchema = yup.object({
email: yup.string().email("Not a valid email").required("Email is required"),
});
Validate email:
function ValidateEmail(email: string): boolean {
return emailSchema.isValidSync({ email: email });
}
How can I inside ValidateEmail use setSnackbarError and set either "Not a valid email" or "Email is required" directly from yup? I've googled like crazy and only found how to use it on useForm together with yup resolver. Sorry if can't show my real investigation with code suggestions, but I've honestly tried for a few hours.

react-hook-form custom validation message not showing

I'm using react-hook-form 6.8.2 with React 16.13.1 and normal validation works as expected, but not when inside of the "validate" key.
const { trigger, formState, watch, reset } = useFormContext({
mode: 'all',
});
--
ref={register({
required: 'This is required',
minLength: {
value: 3,
message: 'Length must be 3 or more',
},
validate: {
always: (value) => value === 1 || 'This should almost always trigger',
},
maxLength: {
value: 5,
message: 'max 5',
},
})}
required, minLength and maxLength all work, but always doesn't.
I have tried this:
always: (value) => value === 1 || console.log('This should almost always trigger') which logs the error in the console
and I have tried this: validate: (value) => value === 1 || 'This should almost always trigger' which does nothing either
Why isn't the validation message shown in the UI?
I made a CodeSandbox with your requirements using react-hook-form#6.8.2 and react#16.13.1 also using RHF's useFormContext hook. It's working fine, have a look:
One thing that isn't correct in your example code: you're passing the useForm config options to the useFormContext hook. That isn't working, you have to pass them instead to your useForm call. Check the CodeSandbox to see how to set the mode to 'all'.
Turns out that there was a custom error rendering class, hidden deep within our own library for input fields, that didn't account for custom error types.
:(
In: V7
Instead of passing the register function's result to a ref, just call the register function with a spread operator on the input field. After this, you need a unique name for this input, just like for every input. In my example, your config works fine. https://codesandbox.io/s/react-hook-form-js-forked-js0il?file=/src/App.js
Leave this answer for someone who looking for a similar solution.
The validate function checks for your condition. If it returns false then the message can be displayed using the error mechanism like this:
import React from "react";
import { useForm } from "react-hook-form";
const Example = () => {
const { handleSubmit, register, errors } = useForm();
const onSubmit = values => console.log(values);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register("username", {
validate: value => value !== "admin" || "Nice try!"
})}
/>
{errors.username && errors.username.message} //Here we display the error
<button type="submit">Submit</button>
</form>
);
};

Yup + useFormik - conditional required field based on some parameter

I have a validation schema object:
SCHEMA = object().shape({
...
someField: string().required(validation_keys.required),
...
});
I am using useFormik within my component:
const formik = useFormik({
initialValues: values,
onSubmit: ...,
validationSchema: SCHEMA,
});
I was looking for a way to pass an argument to my SCHEMA, so as someField would be required just when the argument is true... I read about context, also tried to do it with when(), but none of these worked for me...
My goal is to validate someField base on a component prop, not other field from formik. Something similar to the example below:
validationSchema={yup.object().shape({
someField: yup
.string()
.when("xxx", {
is: true,
then: yup.string().required("Required"),
otherwise: yup.string(),
})
})
}
xxx is supposed to be passed to validation schema in my React component where useFormik is used
You can do something like this.
const validationSchema= Yup.object({
custom_field: Yup.string().test(
"match",
"Custom validation enabled",
function () {
if (!shouldFormValidate) return true;
return false;
}
)
});
Here the working codesandbox.
If you want to validate a field based on a React component prop you can use withMutation:
schema = object({
...,
someField: string(),
...
});
if(validation_keys.required) {
schema.fields.someField.withMutation(schema => {
schema.required();
});
}
Example
While I'm not sure you can achieve this solely in the schema that you pass to Formik, there is an options param that you could use in your Yup Schema.validate. From the Yup docs:
Schema.validate accepts and returns the following:
Schema.validate(value: any, options?: object): Promise<InferType<Schema>, ValidationError>
In which options is:
interface Options {
// when true, parsing is skipped an the input is validated "as-is"
strict: boolean = false;
// Throw on the first error or collect and return all
abortEarly: boolean = true;
// Remove unspecified keys from objects
stripUnknown: boolean = false;
// when `false` validations will be performed shallowly
recursive: boolean = true;
// External values that can be provided to validations and conditionals
context?: object;
}
The last key, context is going to help you out here, you can use it to pass values that later can be accessed during validation.
Example from the docs:
let schema = object({
isBig: boolean(),
count: number()
.when('isBig', {
is: true, // alternatively: (val) => val == true
then: (schema) => schema.min(5),
otherwise: (schema) => schema.min(0),
})
.when('$other', ([other], schema) =>
other === 4 ? schema.max(6) : schema,
),
});
await schema.validate(value, { context: { other: 4 } });
This solution obviously means you'll have to manually call schema.validate and can't rely on Formik doing that for you, but there are ways to achieve that as well, so if you really need your additional params to be available during validation, you can technically do it.

Resources