Yup validation on array of checkbox - reactjs

I want to change the state of the checkbox through useForm and I have an array of checkbox. the number of checkbox I get from the server. See my code please i have selectionAll and when it is true then all other checkbox should be true . And when it is false, then all the rest should be false. But it doesn’t work for me Look at what the problem is And what I wrote wrong.
but when i refresh the page the checkbox state is always undefined
type OperationSelect = {
selections: boolean;
};
export type OperationsData = {
selectionAll: boolean;
selection: OperationSelect[];
};
const defaultValues = {
selectionAll: false,
selections: [false],
};
export const DrawerEditFinancial: React.FC = () => {
const {
handleSubmit,
control,
reset,
watch,
setValue,
getValues,
formState: { errors, isValid },
} = useForm<OperationsData>({ defaultValues, resolver: yupResolver(schema), mode: 'onSubmit' });
const money = watch('sum');
const selectionAll = watch('selectionAll');
const selections = watch('selection');
I wrote the conditions here when selectionAll is true then the rest of the checkbox is true
useEffect(() => {
if (selectionAll) {
selections?.map((selection, index) => {
setValue(`selections.${index}`, true);
});
}
}, [selections, selectionAll]);
return (
<form>
<Grid item xs={0.3}>
<Controller
control={control}
name={'selectionAll'}
render={({ field: { value, onChange } }) => (
<FormControlLabel
value={value}
onChange={(e, checked) => onChange(checked)}
css={labelTextCSS}
control={<Checkbox />}
label=""
/>
)}
/>
</Grid>
{operationsView?.map((operation, index) => (
<Accordion
key={operation.expenseId}
expanded={expanded === `panel_${operation.expenseId}`}
onChange={handleChangeExpanded(`panel_${operation.expenseId}`)}
>
<AccordionSummary expandIcon={<SvgArrowFinancial />}>
<Grid css={pl(12)} container justifyContent="flex-start" alignItems="center">
<Grid item xs={0.3}>
<Controller
control={control}
name={`selections.${index}`}
render={({ field: { value, onChange } }) => (
<FormControlLabel
value={value}
onChange={(e, checked) => onChange(checked)}
css={labelTextCSS}
control={<Checkbox />}
label=""
/>
)}
/>
</Grid>
</Accordion
))}
</form>
)

Related

How to reset MUI Autocomplete form on submit with React Hook Form?

How can I reset MUI Autocomplete form with React Hook Form on submit? Just reset() doesn't work with Autocomplete.
Codesandbox: https://codesandbox.io/s/custom-autocomplete-8dh17l?file=/src/App.tsx
export default function App() {
const {
handleSubmit,
control,
reset,
formState: { isDirty, isValid }
} = useForm<FormValues>({
mode: "onChange",
defaultValues: {
name: "",
dataset: []
}
});
const onSubmit = (data: any) => {
console.log("data:", data);
reset();
};
return (
<div>
<Stack onSubmit={handleSubmit(onSubmit)}>
<Controller
render={({ field: { onChange } }) => {
return (
<CustomAutocomplete
multiple
getOptionLabel={(option: Option) => option.name}
options={DATA}
label={"name"}
onChange={(e, options: Option[]) => onChange(options)}
/>
);
}}
name="dataset"
control={control}
rules={{
required: "Field is required"
}}
/>
<Button
type={"submit"}
disabled={!isDirty || !isValid}
>
Submit
</Button>
</Stack>
</div>
);
}
Try adding value to the CustomAutocomplete component. This passes the value to autocomplete field every time the value in controller gets updated
<Controller
render={({ field: { onChange, value } }) => {
return (
<CustomAutocomplete
multiple
getOptionLabel={(option: Option) => option.name}
options={DATA}
label={"name"}
value={value}
onChange={(e: FocusEvent, options: Option[]) =>
onChange(options)
}
/>
);
}}
name="dataset"
control={control}
rules={{
required: "Field is required"
}}
/>

React Hook Form autofocus on error not working when used in HOC

I have MUI TextField component(TextField2) inside of HOC, which is not getting auto focused on form submission.
If i dont wrap it in HOC it works(TextField1).
Any idea what i'm doing wrong?
Stackblitz: https://stackblitz.com/edit/react-ts-1yn56f?file=App.tsx
TestHoc.tsx :
export default function withTestHoc() {
function DP({ control }: any) {
return (
<div>
<Controller
render={(props1: any) => {
let { field } = props1;
let { ref, ...rest } = field;
return <TextField label="TextField 2" {...rest} inputRef={ref} />;
}}
name="TextField2"
defaultValue=""
rules={{ required: true }}
control={control}
/>
</div>
);
}
return DP;
}
App.tsx :
export default function App() {
const {
register,
handleSubmit,
watch,
control,
formState: { errors },
} = useForm();
const onSubmit = (data: any) => console.log('data', data);
const formRef: any = useRef(null);
const TextField2Comp = withTestHoc();
const submitForm = () => {
formRef.current.dispatchEvent(
new Event('submit', { cancelable: true, bubbles: true })
);
console.log('errors', errors);
};
return (
<div>
<form ref={formRef} onSubmit={handleSubmit(onSubmit)}>
<Controller
render={(props1: any) => {
let { field } = props1;
let { ref: ref1, ...rest } = field;
return <TextField label="TextField 1" {...rest} inputRef={ref1} />;
}}
name="TextField1"
defaultValue=""
rules={{ required: true }}
control={control}
/>
<TextField2Comp control={control} />
</form>
<Button onClick={submitForm}>Submit</Button>
</div>
);
}

Using Material UI's Autocomplete using Formik to display different value in dropdown but set different value in formik state

I am trying to use Material UI's Autocomplete with Formik. Here is a custom Autocomplete component I wrote to use with Formik.
import React from "react";
import Autocomplete from "#material-ui/lab/Autocomplete";
import TextField from "#material-ui/core/TextField";
import { fieldToTextField } from "formik-material-ui";
const FormikAutocomplete = ({ textFieldProps, ...props }) => {
const {
form: { setTouched, setFieldValue },
} = props;
const { error, helperText, ...field } = fieldToTextField(props);
const { name } = field;
return (
<Autocomplete
{...props}
{...field}
onChange={(_, value) =>
setFieldValue(name, value)
}
onBlur={() => setTouched({ [name]: true })}
renderInput={(props) => (
<TextField
{...props}
{...textFieldProps}
helperText={helperText}
error={error}
/>
)}
/>
);
};
export default FormikAutocomplete;
Here is how components callled
<Field
name="title"
component={FormikAutocomplete}
options={gender}
getOptionLabel={(option) => option.title_name_long}
textFieldProps={{
label: "Title",
required: true,
variant: "outlined",
margin: "dense",
}}
/>
Now what I intend to do is: If I have a object like
gender=[{gender_name_short:"F",gender_name_long:"Female},{gender_name_short:"M",gender_name_long:"Male}]
I want the Autocomplete dropdown to show options male,female.
But I want the formik state to save M,F respectively once selected from dropdown. Currently the entire object gets saved.
How can this be done?
In FormikAutocomplete component,
use setFieldValue in the onChange of autocomplete
use gender_name_long in getOptionLabel to display Male , Female
use gender_name_short in getOptionSelected to use M or F
With that, finally, when you submit, you will get to see M/F not Male/Female
Working demo
const gender = [
{ gender_name_short: "F", gender_name_long: "Female" },
{ gender_name_short: "M", gender_name_long: "Male" }
];
const validationSchema = object().shape({
// genderObj: array().required("At least one gender is required")
});
const initialValues = {
username: "abc",
country: "",
gender: "M",
birthdate: null
};
const FormikAutocomplete = ({ textFieldProps, ...props }) => {
const {
form: { setTouched, setFieldValue }
} = props;
const { error, helperText, ...field } = fieldToTextField(props);
const { name } = field;
return (
<Autocomplete
{...field}
{...props}
onChange={(_, data) => {
setFieldValue("gender", data.gender_name_short);
}}
onBlur={() => setTouched({ [name]: true })}
// getOptionLabel={item => item.gender_name_long} //<----see here
getOptionLabel={item => {
// console.log( '====>' , typeof item === "string" ? props.options.find(i => i.gender_name_short === item).gender_name_long : item.gender_name_long)
return typeof item === "string"
? props.options.find(i => i.gender_name_short === item)
.gender_name_long
: item.gender_name_long;
}}
// getOptionLabel={item => typeof item === 'string' ? props.options.find(i => i.gender_name_short === item).gender_name_long : item.gender_name_long}
getOptionSelected={(item, current) => {
return item.gender_name_short === current;
}}
// defaultValue={'hi'}
renderInput={props => (
<TextField
{...props}
{...textFieldProps}
helperText={helperText}
error={error}
/>
)}
/>
);
};
const SimpleFormExample = () => (
<div>
<h1>Simple Form Example</h1>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
validateOnBlur={false}
validateOnChange
onSubmit={(values, { resetForm, setSubmitting }) => {
console.log(values);
resetForm();
// setSubmitting(false);
}}
>
{formik => (
<Form>
<Field
name="gender"
component={FormikAutocomplete}
label="gender"
options={gender}
textFieldProps={{
fullWidth: true,
margin: "normal",
variant: "outlined"
}}
// multiple
/>
<button type="submit">Submit</button>
</Form>
)}
</Formik>
</div>
);
export default SimpleFormExample;

React Hook Forms + Material UI Checkboxes

I am having an error when submiting a form build using React Hook Form and material-ui checkboxes components. The number of checkboxes are build from a list from my api:
<Grid item xs={12}>
<FormControl
required
error={errors.project?.stack ? true : false}
component='fieldset'>
<FormLabel component='legend'>Tech Stack</FormLabel>
<FormGroup>
<Grid container spacing={1}>
{techs.map((option, i) => (
<Grid item xs={4} key={i}>
<FormControlLabel
control={
<Checkbox
id={`stack${i}`}
name='project.stack'
value={option.id}
inputRef={register({required: 'Select project Tech Stack'})}
/>
}
label={option.name}
/>
</Grid>
))}
</Grid>
</FormGroup>
<FormHelperText>{errors.project?.stack}</FormHelperText>
</FormControl>
</Grid>
When the form is submited I got the following error ( several times , 1 for each checkbox rendered ) :
Uncaught (in promise) Error: Objects are not valid as a React child
(found: object with keys {type, message, ref}). If you meant to render
a collection of children, use an array instead.
I don't understand this error. The message apparently says it is a rendering issue, but the component renders fine. The problems happens on submit. Any advices ?
Thank you
UPDATE: if you are using RHF >= 7, you should use props.field to call props.field.value and props.field.onChange.
You can use the default Checkbox Controller:
<FormControlLabel
control={
<Controller
name={name}
control={control}
render={({ field: props }) => (
<Checkbox
{...props}
checked={props.value}
onChange={(e) => props.onChange(e.target.checked)}
/>
)}
/>
}
label={label}
/>
I used the controller example from RHF but had to add checked={props.value}:
https://github.com/react-hook-form/react-hook-form/blob/master/app/src/controller.tsx
I managed to make it work without using Controller.
The props should be inside the FormControlLabel and not inside Checkbox
<Grid item xs={4} key={i}>
<FormControlLabel
value={option.id}
control={<Checkbox />}
label={option.name}
name={`techStack[${option.id}]`}
inputRef={register}
/>
</Grid>
))}
If someone struggle to achieve multiselect checkbox with React material-ui and react-hook-form you can check my codesandbox example
Also, there is a code example provided by react-hook-form in their documentation under useController chapter (don't forget to switch to the checkboxes tab).
Any of that examples works, I´m using this one:
const checboxArray = [{
name: '1h',
label: '1 hora'
},
{
name: '12h',
label: '12 horas'
},
{
name: '24h',
label: '24 horas'
},
{
name: '3d',
label: '3 dias'
},
];
//This inside render function:
{
checboxArray.map((checboxItem) => (
<Controller name = {
checboxItem.name
}
control = {
control
}
key = {
checboxItem.name
}
rules = {
{
required: true
}
}
render = {
({
field: {
onChange,
value
}
}) =>
<
FormControlLabel
control = { <Checkbox
checked = {!!value
}
onChange = {
(event, item) => {
onChange(item);
}
}
name = {
checboxItem.name
}
color = "primary" /
>
}
label = {
checboxItem.label
}
/>
}
/>
))
}
Material UI + React Hook Form + Yup .
Example page:
https://moiseshp.github.io/landing-forms/
Without extra dependences
Show and hide error messages
// import { yupResolver } from '#hookform/resolvers/yup'
// import * as yup from 'yup'
const allTopics = [
'React JS',
'Vue JS',
'Angular'
]
const defaultValues = {
topics: []
}
const validationScheme = yup.object({
topics: yup.array().min(1),
})
const MyForm = () => {
const resolver = yupResolver(validationScheme)
const {
control,
formState: { errors },
handleSubmit
} = useForm({
mode: 'onChange',
defaultValues,
resolver
})
const customSubmit = (data) => alert(JSON.stringify(data))
return (
<form onSubmit={handleSubmit(customSubmit)}>
<FormControl component="fieldset" error={!!errors?.topics}>
<FormLabel component="legend">Topics</FormLabel>
<FormGroup row>
<Controller
name="topics"
control={control}
render={({ field }) => (
allTopics.map(item => (
<FormControlLabel
{...field}
key={item}
label={item}
control={(
<Checkbox
onChange={() => {
if (!field.value.includes(item)) {
field.onChange([...field.value, item])
return
}
const newTopics = field.value.filter(topic => topic !== item)
field.onChange(newTopics)
}}
/>
)}
/>
))
)}
/>
</FormGroup>
<FormHelperText>{errors?.topics?.message}</FormHelperText>
</FormControl>
</form>
)
}
export default MyForm
Here is the simplest way to do it using Controller
<Box>
<Controller
control={control}
name={`${dimension.id}-${dimension.name}`}
defaultValue={false}
render={({ field: { onChange, value } }) => (
<FormControlLabel
control={
<Checkbox checked={value} onChange={onChange} />
}
/>
)}
/>
</Box>

Downshift autocomplete onBlur resetting value with Formik

I have a form with a field that needs to show suggestions via an api call. The user should be allowed to select one of those options or not and that value that they type in gets used to submit with the form, but this field is required. I am using Formik to handle the form, Yup for form validation to check if this field is required, downshift for the autocomplete, and Material-UI for the field.
The issue comes in when a user decides not to use one of the suggested options and the onBlur triggers. The onBlur always resets the field and I believe this is Downshift causing this behavior but the solutions to this problem suggest controlling the state of Downshift and when I try that it doesn't work well with Formik and Yup and there are some issues that I can't really understand since these components control the inputValue of this field.
Heres what I have so far:
const AddBuildingForm = props => {
const [suggestions, setSuggestions] = useState([]);
const { values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
modalLoading,
setFieldValue,
setFieldTouched,
classes } = props;
const loadOptions = (inputValue) => {
if(inputValue && inputValue.length >= 3 && inputValue.trim() !== "")
{
console.log('send api request', inputValue)
LocationsAPI.autoComplete(inputValue).then(response => {
let options = response.data.map(erlTO => {
return {
label: erlTO.address.normalizedAddress,
value: erlTO.address.normalizedAddress
}
});
setSuggestions(options);
});
}
setFieldValue('address', inputValue); // update formik address value
}
const handleSelectChange = (selectedItem) => {
setFieldValue('address', selectedItem.value); // update formik address value
}
const handleOnBlur = (e) => {
e.preventDefault();
setFieldValue('address', e.target.value);
setFieldTouched('address', true, true);
}
const handleStateChange = changes => {
if (changes.hasOwnProperty('selectedItem')) {
setFieldValue('address', changes.selectedItem)
} else if (changes.hasOwnProperty('inputValue')) {
setFieldValue('address', changes.inputValue);
}
}
return (
<form onSubmit={handleSubmit} autoComplete="off">
{modalLoading && <LinearProgress/>}
<TextField
id="name"
label="*Name"
margin="normal"
name="name"
type="name"
onChange={handleChange}
value={values.name}
onBlur={handleBlur}
disabled={modalLoading}
fullWidth={true}
error={touched.name && Boolean(errors.name)}
helperText={touched.name ? errors.name : ""}/>
<br/>
<Downshift id="address-autocomplete"
onInputValueChange={loadOptions}
onChange={handleSelectChange}
itemToString={item => item ? item.value : '' }
onStateChange={handleStateChange}
>
{({
getInputProps,
getItemProps,
getMenuProps,
highlightedIndex,
inputValue,
isOpen,
}) => (
<div>
<TextField
id="address"
label="*Address"
name="address"
type="address"
className={classes.autoCompleteOptions}
{...getInputProps( {onBlur: handleOnBlur})}
disabled={modalLoading}
error={touched.address && Boolean(errors.address)}
helperText={touched.address ? errors.address : ""}/>
<div {...getMenuProps()}>
{isOpen ? (
<Paper className={classes.paper} square>
{suggestions.map((suggestion, index) =>
<MenuItem {...getItemProps({item:suggestion, index, key:suggestion.label})} component="div" >
{suggestion.value}
</MenuItem>
)}
</Paper>
) : null}
</div>
</div>
)}
</Downshift>
<Grid container direction="column" justify="center" alignItems="center">
<Button id="saveBtn"
type="submit"
disabled={modalLoading}
className = {classes.btn}
color="primary"
variant="contained">Save</Button>
</Grid>
</form>
);
}
const AddBuildingModal = props => {
const { modalLoading, classes, autoComplete, autoCompleteOptions } = props;
return(
<Formik
initialValues={{
name: '',
address: '',
}}
validationSchema={validationSchema}
onSubmit = {
(values) => {
values.parentId = props.companyId;
props.submitAddBuildingForm(values);
}
}
render={formikProps => <AddBuildingForm
autoCompleteOptions={autoCompleteOptions}
autoComplete={autoComplete}
classes={classes}
modalLoading={modalLoading}
{...formikProps} />}
/>
);
}
Got it to work. Needed to use handleOuterClick and set the Downshift state with the Formik value:
const handleOuterClick = (state) => {
// Set downshift state to the updated formik value from handleOnBlur
state.setState({
inputValue: values.address
})
}
Now the value stays in the input field whenever I click out.

Resources