Using Yup to validate radiobox or checkbox value - reactjs

I am using two library with is Formik and Yup. And trying to validate by the value stored on the radio box(check box as well) but seems like I cant find anything on google where I can get the actual value to check. I can use the name value to validate but is there a way to check validation by looking at the value? I will provide the example below.
const initialValues = {
A: "",
B: "",
C: [],
};
const onSubmit = (values) => {
console.log(values);
};
const validationSchema = Yup.object({
A: Yup.string().required("Required"),
B: Yup.string().required("Required"),
C: Yup.array().min(1, "Must be selected"),
});
and here is the return for react
return (
<>
<Formik
enableReinitialize={true}
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={onSubmit}
render={(props) => <TreatmentView {...props} data={data} />}
/>
</>
);
And this is the rendered view below
I want to store that logic on the validationSchema. So again, I want to add a validation where checkbox with name "C" with value "4" can only be checked when radio button of name "B" with value "3" is selected. I did looked at the doc for the YUP and tried using the .value but seems like its not working..
Thank You!

Related

Formik material ui with autocomplete not works as expected

I'm using formik with material-ui to build custom autocomplete component.
my link to code
I add validation using yup for autocomplete to be required (user need to select one option):
validationSchema: Yup.object().shape({
skill: Yup.object().shape({
value: Yup.string().required(),
label: Yup.string().required()
})
}),
when user select any option I want it to save the option as {label,value} and not just the value because then I need to send it as object to the server.
I get some errors:
when I press submit I get error
Objects are not valid as a React child (found: object with keys {value, label}). If you meant to render a collection of children, use an array instead.
I get those warnings:
MUI: The value provided to Autocomplete is invalid. None of the options match with `{"label":"","value":""}`. You can use the `isOptionEqualToValue` prop to customize the equality test.
Warning: Failed prop type: Invalid prop `helperText` supplied to `ForwardRef(TextField)`, expected a ReactNode.
I'm trying to build reusable autocomplete component to be able to get the selected value as {label,value} without any error and working as expected.
I'm not sure if my validation is the right way. I want just the user to select an option.
in addition, I would like to ask, can I store object details into the value not as string? if no, what the best way to store value of object as option?
is my yup schema is the right way? should I store id of object as value and then filter it? how to pass value as string to autocomplete and show it selected in autocomplete?
How can I do it without skill schema as object, just as string like:
validationSchema: Yup.object().shape({
skill: Yup.string().required()
}),
This is because initial value as {label:'' , value :''} is not in skills list. so in isOptionEqualToValue it will return false
simply remove isOptionEqualToValue prop and in value check if there is option with that value and if not set null
const getValueFromOptions = (value: string) => {
return (
options.find((option: any) => option.value === value) ?? null
);
};
and in auto complete :
<Autocomplete
value={getValueFromOptions(field.value)}
...
or check if field.value has value otherwise null
<Autocomplete
value={Boolean(field.value?.value) ? field.value : null}
...
and another error was from show error message because meta.error is object that contains value and label error so you need to show one of them, I changed it to this :
renderInput={(params) => {
const errorMsg = meta.error as { value?:string }
return (
<TextField
{...params}
label={label}
name={name}
error={showError}
helperText={meta.touched && errorMsg?.value}
/>
);
}}
and another error was from show error message, it was displaying 'skill.value is a required field' so I simply add new message in yup validation
validationSchema: Yup.object().shape({
skill: Yup.object().shape({
value: Yup.string().required('skill is a required field'),
label: Yup.string()
})
}),
you see pure code here : https://codesandbox.io/s/blissful-nova-nqvxq2?file=/src/components/AutocompleteField.tsx

How do I override the defaultValues in useForm and maintain the isDirty function?

I have a situation where I need to call my DB to obtain some default values. But in the off-chance the data does not exist, I will set some default values in my useForm. Basically, this means that the defaultValues declared in useForm is a fallback value if I fail to obtain the default values from the DB.
From what Im understanding, according to the documentation with regards to useForm,
The defaultValues for inputs are used as the initial value when a component is first rendered, before a user interacts with it.
Or basically, the useForm is one of the first things defined when the page is loaded.
So, unless I can call my DB before useForm is loaded, Im a little stuck on this.
I've read that each Controller field can have something called defaultValue which sounds like the solution, but the documentation mentioned a caveat of
If both defaultValue and defaultValues are set, the value from defaultValues will be used.
I considered setValues but I want to use the isDirty function, which allows field validation and the value used to check whether the form is dirty is based on the useForm defaultValues. Thus, if I were to use setValues, the form would be declared dirty, which is something I do not want.
TL;DR this is what I want:
This is my initial value(the fallback value, result A).
const { formState: { isDirty }, } = useForm(
{defaultValues:{
userID: "",
userName: "",
userClass: "administrator",
}}
);
What I want to do is to make a DB call and replace the data, so that it would now look something like this(result B) if the call is successful(if fail, it will remain as result A).
const { formState: { isDirty }, } = useForm(
{defaultValues:{
userID: "1",
userName: "user",
userClass: "administrator",
}}
);
Please note that the DB call will replace only the userID and userName default values, the default value for userClass will be maintained.
So, the flow is as such:
Case 1: render form -> result A -> DB call -> success -> result B
Case 2: render form -> result A -> DB call -> fail/no data -> result A
So, unless I actually key in an input that is different from the default values of either results depending on the case, both Case 1 and Case 2 should return isDirty==false when I check it.
For react-hook-form#7.22.0 and newer
I think you want to use reset here in combination with useEffect to trigger it when your DB call has finished. You can additionally set some config as the second argument to reset, for example affecting the isDirty state in your case.
Although your answer works there is no need to use getValues here, as reset will only override the fields which you are passing to reset. Your linked answer is for an older version of RHF, where this was necessary.
Also if you're not adding a field dynamically on runtime then you can just pass the whole object you get from your DB call to reset and set the shouldUnregister to true in your useForm config. This way props from your result object which haven't got a corresponding form field will get ignored.
export default function Form() {
const { register, handleSubmit, reset, formState } = useForm({
defaultValues: {
userID: "",
userName: "",
userClass: "administrator"
},
shouldUnregister: true
});
console.log(formState.isDirty);
const onSubmit = (data) => {
console.log(data);
};
const onReset = async () => {
const result = await Promise.resolve({
userID: "123",
userName: "Brian Wilson",
otherField: "bla"
});
reset(result);
};
useEffect(() => {
onReset();
}, []);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>User ID</label>
<input type="text" {...register("userID")} />
<label>User Name</label>
<input type="text" {...register("userName")} />
<label>User Class</label>
<input type="text" {...register("userClass")} />
<input type="submit" />
</form>
);
}
Here is a Sandbox demonstrating the explanation above:
For older versions
Just merge your defaultValues or field values via getValues with the result of your DB call.
export default function Form() {
const {
register,
handleSubmit,
reset,
formState,
getValues,
control
} = useForm({
defaultValues: {
userID: "",
userName: "",
userClass: "administrator"
},
shouldUnregister: true
});
console.log(formState.isDirty);
const onSubmit = (data, e) => {
console.log(data);
};
const onReset = async () => {
const result = await delay();
reset({ ...getValues(), ...result });
};
useEffect(() => {
onReset();
}, []);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>User ID</label>
<input type="text" {...register("userID")} />
<label>User Name</label>
<input type="text" {...register("userName")} />
<label>User Class</label>
<Controller
name="userClass"
control={control}
render={({ field }) => <input type="text" {...field} />}
/>
<input type="submit" />
</form>
);
}
I went and further googled the issues I had with #knoefel's original answer, and I came across this
Thus, the solution I came up with, based on a combination with #knoefel's answer and the answer in the link:
useEffect(async () => {
let dataArray={};
let result= fetch('my-db-call');
if(result) {
dataArray['userID']=result.userID
dataArray['userName']=result.userName
}
reset({...getValues(), ...dataArray})
}, [])
Apparently, what happens is that the the reset function will first set the values, result A, using ...getValues() and any subsequent data after will replace the previously set values only if it exist. (eg. if my dataArray object lacks the userID key, it will not replace the userID default with the new default. ).
Only after both getValues and dataArray is set then will it reset the default values.
As far as I can tell, this is more or less incline with what I need and should give me result B.

Formik initialValues and onSubmit issues

I am having a couple of issues with my Formik Form.
If I use defaultValue={location.fname} on the <TextField> I can type in the field,
but on submit the newly typed values do not appear in the alert.
If I use value={location.fname} on the <TextField> I can't type in the field.
What am I doing wrong? I would like to be able to type into the field to update the value AND get the new value onSubmit.
let formik = useFormik({
initialValues: {
fname: person.fname,
lname: person.lname,
address: person.address,
city: person.city,
},
enableReinitialize:true,
validateOnChange: false,
validateOnBlur: false,
validationSchema: validationSchema,
onSubmit: values => {
alert(JSON.stringify(values, null, 2));
},
});
...
<TextField
id="fname"
name="fname"
variant="outlined"
defaultValue={location.fname}
onChange={formik.setFieldValue}
error={formik.touched.fname && Boolean(formik.errors.fname)}
helperText={formik.touched.fname && formik.errors.fname} />
Change the defaultValue from location.fname to formik.values.fname as you are already passing the values to formik using initialValues. Also change the onChange from formik.setFieldValue to formik.handleChange and let formik handle the form state through the input name, finally add onBlur={formik.handleBlur} so that formik can know when your input field is touched.

Formik + Yup: How to immediately validate form before submit?

I want to show field errors when form mounted. Not after submit.
Yup:
const validation = Yup.object().shape({
field: Yup.string().required('Required')
});
Formik:
<Formik
initialValues={initialValues}
validationSchema={validation}
validateOnMount
>
...
</Formik>
Thanks for help!
The simple answer is
Pass initialTouched to Formik which will be an object with key of the fields you want to show the error message and the value true for those keys.
e.g.
<Formik
initialValues={initialValues}
initialTouched={{
field: true,
}}
validationSchema={validation}
validateOnMount
>
...
</Formik>
But there is alot of ways to do this, but you can create an component that call validateForm on the initial render
const ValidateOnMount = () => {
const { validateForm } = useFormikContext()
React.useEffect(() => {
validateForm()
}, [])
// the return doesn't matter, it can return anything you want
return <></>
}
You can also do the same using validateOnMount and setting initialTouched to true on all fields you want to display the error message (maybe you only want to show certain field's error message on the initial mount).
<Formik
validateOnMount
initialValues={initialValues}
validationSchema={validation}
initialTouched={{
field: true
}}
{...rest}
>
</Formik>
Where initialTouched should be an object that have keys to all fields that you want to validate that are in initialValues but with the value true, which means you already touched that field.
Another way to do this is only passing validateOnMount to Formik and display any error message without checking for touched[name].
If you use ErrorMessage from formik, it won't work because it checks for touched[name] === true to display the message, so you need to create your own way of displaying the error, but only checking for errors[name].
you can use errors and touched props, like this
<Formik
initialValues={initialValues}
validationSchema={validation}
validateOnMount
>
{props => {
const { errors, touched } = props
return (
<Form>
...
{errors.field && touched.field ? <Error>{errors.field}</Error> : null}
...
</Form>
)
}
</Formik>
There's a prop called isInitialValid.
Setting it to false solved my problem :)

Automatically trim white spaces with Yup and Formik

I am using a Formik React Form and Yup validation defined on a schema:
export const Contact = yup.object<IContact>().shape({
contactName: yup
.string()
.trim('The contact name cannot include leading and trailing spaces')
.strict(true)
.min(1, 'The contact name needs to be at least 1 char')
.max(512, 'The contact name cannot exceed 512 char')
.required('The contact Name is required'),
});
Is there a way to have Yup trim white spaces without showing a message? So automatically trimming the spaces when a form is submitted?
Is there a way to have Yup trim white spaces without showing a message
Not in a single transform. The yup transform used by formik is only for validation.
You can create a seperate transform to use before passing the data, but its simpler to just valueToUse = userValue.trim() yourself.
You can do:
onSubmit={(values) => {
const castValues = Contact.cast(values)
})
reference:
https://github.com/jaredpalmer/formik/issues/473#issuecomment-499476056
I was able to achieve automatic removal of white spaces in an email field by overriding the onChange method from formik object. This is not the best solution to this but it works fine, especially where spaces are not allowed anywhere in the input field.
const { errors, touched, getFieldProps } = formik;
const { onChange } = getFieldProps('email');
const emailFieldProps = {
...getFieldProps('email'),
onChange: (e) => {
e.target.value = e.target.value.trim();
onChange(e);
},
};
return (
...
<TextField
{...emailFieldProps}
error={Boolean(touched.email && errors.email)}
helperText={touched.email && errors.email}
/>
...
)
You can also trim entered text on the go to completely limit user from using spaces at the end (example uses react native components):
const {handleSubmit, values, errors} =
useFormik({
validationSchema: EmailSchema(),
initialValues: {email: ''},
onSubmit: values => tryLogin(values.email)
})
...
<TextInput
value={values.email}
onChangeText={text => handleChange('email')(text.trim())}
error={errors.email}
/>
<Button title='SUBMIT' onPress={handleSubmit} />

Resources