react hook form material ui checkbox array - reactjs

i can't get values of checkbox array
const schema = yup.object().shape({
modules: yup.array(),
});
component
{role === "user" &&
divisions.map((division, i) => (
<Box key={division.name}>
<Typography variant='h6'>{division.name}</Typography>
{division.modules.map((m, j) => (
<Controller
key={m.name}
name={`modules[${i}][${j}]`}
control={control}
defaultValue={[division.name, m.name, false]}
render={({ field }) => (
<FormControlLabel
{...field}
label={m.name}
control={
<Checkbox
onChange={(e) => (e.target.value = "chen")}
color='primary'
/>
}
/>
)}
/>
))}
</Box>
))}
when i submit the form without checking anything i got a result like this
{//userinfo, modules: [
//array per division and a nested array for modules access
[ ["Admin-tools", "admin",false ], ["Admin-tools", "Backup",false ] ]
...other divisions and modules
] }
this is the result i expect when i check fields and submit the form
{//userinfo, modules: [
//array per division and a nested array for modules access
[ ["Admin-tools", "admin",true], ["Admin-tools", "Backup",true
] ]
...other divisions and modules
] }
but i got
{//userinfo, modules: [
//array per division and a nested array for modules access
[ [true], [true] ]
...other divisions and modules
] }

You need to pass checked and onChange to your checkbox and append/remove from the form array.
Steps:
Create FromCheckboxes that'll serve as a container to a checkbox array
Loop through your divisions and render a FromCheckbox for each division
Form.js
<form onSubmit={handleSubmit(onSubmit)}>
{divisions.map((division) => (
<Box key={division.name}>
<Typography variant="h6">{division.name}</Typography>
<FormCheckboxes
name="modules"
control={control}
parent={division.name}
options={division.modules}
/>
</Box>
))}
<Button type="submit">Submit</Button>
</form>
FormCheckboxes.js
import { useController } from "react-hook-form";
import { Checkbox, FormControlLabel } from "#material-ui/core";
const FormCheckBoxes = ({ options, ...rest }) => {
const { field } = useController(rest);
const { value, onChange } = field;
return options.map((option) => {
return (
<FormControlLabel
key={option.name}
value={option.name}
label={option.name}
control={
<Checkbox
checked={value.some((formOption) => formOption[1] === option.name)}
onChange={(e) => {
const valueCopy = [...value];
if (e.target.checked) {
valueCopy.push([rest.parent, option.name, true]); // append to array
} else {
const idx = valueCopy.findIndex(
(formOption) => formOption[1] === option.name
);
valueCopy.splice(idx, 1); // remove from array
}
onChange(valueCopy); // update form field with new array
}}
/>
}
/>
);
});
};
export default FormCheckBoxes;
Updated Sandbox
Note that you'll still need to figure out how to tie them all in one form field. Now it won't work with multi-divisions.

if you want the values of the checkbox in order to go to the form values and get it.
you have to give each check box a name property.
ex:
<FormControlLabel
{...field}
label={m.name}
control={
<Checkbox
onChange={(e) => (e.target.value = "chen")}
color='primary'
name='name-one'
/>
}
/>

Related

Multiple Checkbox select one at a time using React Hook Form

Hi guys I'm using react hook form for multiple checkbox, Currently all checkbox can be selected.
Any idea how can I select checkbox one at a time?
const formMethods = useForm<BookingSourcesForm>({
defaultValues: { bookingSources: [] },
});
const { fields, append } = useFieldArray<BookingSourcesForm>({
control: formMethods.control,
name: 'bookingSources',
});
{fields.map((field, index) => {
return (
<HStack align="center" justify="space-between" w="100%">
<Controller
name={`bookingSources.${index}.IsDefault` as const}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onChange={(e) => {
field.onChange(e.currentTarget.checked);
}}
/>
)}
/>
)
}
}
If only one box should be able to picked you should use a RadioGroup instead.
See: https://chakra-ui.com/docs/components/radio/usage
onChange={(value) => {
getValues(`bookingSources`)?.forEach((booking, idx) => {
setValue(`bookingSources.${idx}.IsDefault` as `bookingSources.0.IsDefault`, false);
});
field.onChange(value);
}}

Unable to dynamically set the initial state of a Select box in Material UI

I'm using Material UI 5.7.0. I've created a survey form that users can fill out and click a button to download a json file with their answers. I'd like to manage my questions in json object called questionsObject. It seemed to make sense to use this same object (or one created dynamically from it) to load the initial state of the form as well as updating it.
The below code is working BUT I've statically defined my default values in the defaultValues object. I'll need to update that every time I add a new question to the questions object. I really want it to read the default value from the value field in the questionsObject.
Working code
import React, {useEffect, useState} from "react";
import Grid from "#mui/material/Grid";
import FormControlLabel from "#mui/material/FormControlLabel";
import FormControl from "#mui/material/FormControl";
import FormLabel from "#mui/material/FormLabel";
import RadioGroup from "#mui/material/RadioGroup";
import Radio from "#mui/material/Radio";
import Button from "#mui/material/Button";
import Select from '#mui/material/Select';
import { saveAs } from 'file-saver';
import {InputLabel, MenuItem} from "#mui/material";
const questionsObject = {
q1: {
questionText: "Animals",
type: "BOTH",
displayType: "select",
menuItems: [
'',
"Dogs",
"Cats",
"Elephants"
],
value: ''
},
q2: {
questionText: "Favorite Pizza Topping",
type: "BOTH",
displayType: "select",
menuItems: [
'',
"Pepperoni",
"Mushrooms"
],
value: ''
},
q3: {
questionText: "Question text",
type: "BOTH",
displayType: "radioGroup",
value: "true"
},
q4: {
questionText: "Question text",
type: "BOTH",
displayType: "radioGroup",
value: "no answer"
},
q5: {
questionText: "Question text",
type: "BE",
displayType: "radioGroup",
value: "no answer"
}
}
const defaultValues = {
q1: '',
q2: '',
q3: "no answer",
q4: "no answer",
q5: "no answer"
}
const MyForm = () => {
const [formValues, setFormValues] = useState(defaultValues);
// useEffect(() => {
// const outputObj = {}
// Object.keys(questionsObject).map((question) =>
// outputObj[question.id] = question.value
// );
// setFormValues(outputObj)
// }, []);
const handleInputChange = (e) => {
const { name, value} = e.target;
setFormValues({
...formValues,
[name]: value
});
};
const handleSubmit = (event) => {
event.preventDefault();
const outputObj = {}
Object.keys(questionsObject).map((question) =>
outputObj[question.id] = {
questionText: question.questionText,
type: question.type,
answer: formValues[question.id]
}
);
console.log(outputObj);
const outputObjJSON = JSON.stringify(outputObj, null, 2);
const blob = new Blob([outputObjJSON], {type: "application/json"});
saveAs(blob, "answerfile.json");
};
const getQuestionElement = (questionId) => {
const question = questionsObject[questionId];
switch(question.displayType) {
case "radioGroup":
return (
<Grid item key={questionId}>
<FormControl>
<FormLabel>{question.questionText}</FormLabel>
<RadioGroup
questiontype = {question.type}
name={questionId}
value={formValues[questionId]}
onChange={handleInputChange}
row
>
<FormControlLabel
value="no answer"
control={<Radio size="small" />}
label="No Answer"
/>
<FormControlLabel
value="false"
control={<Radio size="small" />}
label="False"
/>
<FormControlLabel
value="true"
control={<Radio size="small" />}
label="True"
/>
<FormControlLabel
value="NA"
control={<Radio size="small" />}
label="Not Applicable"
/>
</RadioGroup>
</FormControl>
</Grid>
);
case "select":
return (
<Grid item key={questionId}>
<FormControl sx={{ m: 2, minWidth: 200 }}>
<InputLabel>{question.questionText}</InputLabel>
<Select
name={questionId}
value={formValues[questionId]}
label={question.questionText}
onChange={handleInputChange}
>
{question.menuItems.map((item, index) =>
<MenuItem key={index} value={item}>{item}</MenuItem>
)}
</Select>
</FormControl>
</Grid>
);
default:
}
}
return formValues ? (
<form onSubmit={handleSubmit}>
<Grid
container
spacing={0}
direction="column"
alignItems="left"
justifyContent="center">
{Object.keys(questionsObject).map((questionId =>
getQuestionElement(questionId)
))}
<Button variant="contained" color="primary" type="submit">
Download
</Button>
</Grid>
</form>
): null ;
};
export default MyForm;
Things I've tried
I tried starting with no default state like this:
const [formValues, setFormValues] = useState();
and uncommenting useEffect()
but I get this error:
MUI: You have provided an out-of-range value `undefined` for the select component.
Consider providing a value that matches one of the available options or ''.
The available values are ``, `Dogs`, `Cats`, `Elephants`.
on top of that my Radio Buttons have no default value set.
This lead me to attempting to use the defaultValue fields on both the RadioGroup and the Select component.
Attempting to set initial values in the defaultValue property like this:
defaultValue=""
or
defaultValue={question.value}
Give me this error:
MUI: A component is changing the uncontrolled value state of Select to be controlled.
Elements should not switch from uncontrolled to controlled (or vice versa).
Decide between using a controlled or uncontrolled Select element for the lifetime of the component.
The nature of the state is determined during the first render. It's considered controlled if the value is not `undefined`.
How can I remove the defaultValues object and use the value field inside of questionsObject for the initial state instead?
The problem is in the logic inside your useEffect:
useEffect(() => {
const outputObj = {}
Object.keys(questionsObject).map(
(question) => outputObj[question.id] = question.value
);
console.log(outputObj);
setFormValues(outputObj)
}, []);
/// OUTPUT of outputObj:
{undefined: undefined}
You should change it to:
useEffect(() => {
const outputObj = {};
Object.keys(questionsObject).map(
(question) => (outputObj[question] = questionsObject[question].value)
);
console.log(outputObj);
setFormValues(outputObj);
}, []);
/// OUTPUT of outputObj:
{q1: "", q2: "", q3: "true", q4: "no answer", q5: "no answer"}
Also, avoid the switch/case - mainly for not doing anything with default value. In this case prefer the conditional renderering with && operator (docs):
const getQuestionElement = (questionId) => {
const question = questionsObject[questionId];
return (
<React.Fragment key={questionId}>
{question.displayType === "radioGroup" && (
<Grid item>
<FormControl>
<FormLabel>{question.questionText}</FormLabel>
<RadioGroup
questiontype={question.type}
name={questionId}
value={formValues[questionId]}
onChange={handleInputChange}
row
>
<FormControlLabel
value="no answer"
control={<Radio size="small" />}
label="No Answer"
/>
<FormControlLabel
value="false"
control={<Radio size="small" />}
label="False"
/>
<FormControlLabel
value="true"
control={<Radio size="small" />}
label="True"
/>
<FormControlLabel
value="NA"
control={<Radio size="small" />}
label="Not Applicable"
/>
</RadioGroup>
</FormControl>
</Grid>
)}
{question.displayType === "select" && (
<Grid item>
<FormControl sx={{ m: 2, minWidth: 200 }}>
<InputLabel>{question.questionText}</InputLabel>
<Select
name={questionId}
value={formValues[questionId]}
label={question.questionText}
onChange={handleInputChange}
>
{question.menuItems.map((item, index) => (
<MenuItem key={index} value={item}>
{item}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
)}
</React.Fragment>
);
};

Set defaultValues to Controllers in useFieldArray

Misunderstanding react-hook-forms.
I have a form for editing some stuff. Form contains fieldArray.
I set initial formData in useForm hook using default values
const methods = useForm({ defaultValues: defaultValues });
where defaultValues is
const defaultValues = {
test: [
{
name: "useFieldArray1"
},
{
name: "useFieldArray2"
}
]
};
And fieldArray. Here I'm using Controller (it's simplified case - in fact Custom input Controller more complex)
<ul>
{fields.map((item, index) => {
return (
<li key={item.id}>
<Controller
name={`test[${index}].name`}
control={control}
render={({value, onChange}) =>
<input onChange={onChange} defaultValue={value} />}
/>
<button type="button" onClick={() => remove(index)}>
Delete
</button>
</li>
);
})}
</ul>
When form is rendered everything is fine. Default values are displayed in input fields. But when I delete all fields and click append - new fields are not empty ... Default values are displayed
again. And it happens only with Controller. Why it happens ? And how I can avoid it?
Please, here is CodeSandBox link. Delete inputs and press append to reproduce what I am saying.
https://codesandbox.io/s/react-hook-form-usefieldarray-nested-arrays-forked-7mzyw?file=/src/fieldArray.js
Thanks
<Controller
name={name}
rules={rules}
defaultValue={defaultValue ? defaultValue : ''}
render={({ field, fieldState }) => {
return (
<TextField
inputRef={field.ref}
{...props}
{...field}
label={label}
value={field.value ? field.value : ''}
onChange={(event) => {
field.onChange(event.target.value);
props.onChange && props.onChange(event);
}}
style={props.style || { width: '100%' }}
helperText={fieldState?.error && fieldState?.error?.message}
error={Boolean(fieldState?.error)}
size={props.size || 'small'}
variant={props.variant || 'outlined'}
fullWidth={props.fullWidth || true}
/>
);
}}
/>

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>

Putting a customised radio button component inside a Radio Group in Material UI

I want to have a list of radio buttons, with one option being a freestyle 'Other' text box that lets the user enter their own text.
Here I have a working sandbox of everything I want to do:
https://codesandbox.io/s/r4oo5q8q5o
handleChange = event => {
this.setState({
value: event.target.value
});
};
selectItem = item => {
this.setState({
selectedItem: item
});
};
handleOtherChange = event => {
this.setState({
otherText: event.target.value
});
this.selectItem(
//Todo put in right format
this.state.otherText
);
};
focusOther = () => {
this.setState({
value: "Other"
});
this.selectItem(this.state.otherText);
};
render() {
const { classes, items } = this.props;
const { value } = this.state;
return (
<div className={classes.root}>
<Typography>
{" "}
Selected item is: {JSON.stringify(this.state.selectedItem)}
</Typography>
<FormControl component="fieldset" fullWidth>
<RadioGroup value={this.state.value} onChange={this.handleChange}>
{items.map(v => (
<FormControlLabel
value={v.name}
control={<Radio />}
label={v.name}
key={v.name}
onChange={() => this.selectItem(v)}
/>
))}
<FormControlLabel
value="Other"
control={<Radio />}
label={
<TextField
placeholder="other"
onChange={this.handleOtherChange}
onFocus={this.focusOther}
/>
}
onChange={() => this.selectItem(this.state.otherText)}
/>
</RadioGroup>
</FormControl>
</div>
);
}
}
Now what I want to do is make the 'Other' text box its own component.
Here's my attempt:
https://codesandbox.io/s/ryomnpw1o
export default class OtherRadioButton extends React.Component {
constructor() {
super();
this.state = {
text: null
};
}
handleTextChange = event => {
this.setState({
text: event.target.value
});
this.props.onChange(this.state.text);
};
focusOther = () => {
this.props.onFocus(this.props.value);
this.props.onChange(this.state.text);
};
render() {
return (
<FormControlLabel
value={this.props.value}
control={<Radio />}
label={
<TextField
placeholder="other"
onChange={this.handleTextChange}
onFocus={this.focusOther}
/>
}
onChange={this.focusOther}
/>
);
}
}
Used with:
<OtherRadioButton
value="Other"
onFocus={v => this.setState({ value: v})}
onChange={v => this.selectItem(v)}
/>
As you can see - the value of the free text is propagating back fine - but the RadioGroup seems like it's not aware of the FormGroupLabel's value.
Why is this, and how would I solve this?
You can check the RadioGroup source code here.
And I have written my own code to better illustrate how it can be fixed. See here: https://codesandbox.io/s/mz1wn4n33j
RadioGroup creates some props to its FormControlLabel/RadioButton children. By creating your own customized radio button in a different component, these props are not passed to FormControlLabel/RadioButton.
You can fix these by passing the props to your FormControlLabel in your custom RadioButton.
<FormControlLabel
value={this.props.value} //Pass this
onChange={this.props.onChange} //Pass this one too
checked={this.props.checked} //Also this
control={<Radio name="gender" />}
label={
<TextField
id="standard-bare"
defaultValue={this.props.defaultValue}
margin="normal"
onChange={this.props.onTextChange}
/>
}
/>

Resources