React Hook Forms + Material UI Checkboxes - reactjs

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>

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>
);
};

react hook form material ui checkbox array

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'
/>
}
/>

How to use MUI Select with react-hook-form?

I've built a form in React using MUI and React Hook Form. I'm trying to create a custom TextField element that works as a Select Input. I would like it to be an uncontrolled component with a Ref prop. I've tried to pass the inputRef prop as the MUI and React Hook Form docs recommend but with no success.
<TextField
id="id"
name="name"
select
native="true"
className={classes.textField}
label="label"
margin="normal"
variant="outlined"
inputRef={register({ required: "Choose one option" })}
error={!!errors.name}
>
<MenuItem value="">Choose one option</MenuItem>
<MenuItem value="3">03</MenuItem>
<MenuItem value="6">06</MenuItem>
<MenuItem value="9">09</MenuItem>
<MenuItem value="12">12</MenuItem>
<MenuItem value="16">16</MenuItem>
<MenuItem value="18">18</MenuItem>
</TextField>
One thing that I've found is that if I use the native select with ref, it works just fine.
Besides, I tried to change the inputRef prop to a SelectProps one but it didn't work too.
Using Select component from Material-ui with react hook form need you to implement custom logic with a Controller https://react-hook-form.com/api#Controller
Here is a reusable component that will hopefully simplify the code to use that Select component in your app:
import FormControl from "#material-ui/core/FormControl";
import InputLabel from "#material-ui/core/InputLabel";
import Select from "#material-ui/core/Select";
import { Controller } from "react-hook-form";
const ReactHookFormSelect = ({
name,
label,
control,
defaultValue,
children,
...props
}) => {
const labelId = `${name}-label`;
return (
<FormControl {...props}>
<InputLabel id={labelId}>{label}</InputLabel>
<Controller
as={
<Select labelId={labelId} label={label}>
{children}
</Select>
}
name={name}
control={control}
defaultValue={defaultValue}
/>
</FormControl>
);
};
export default ReactHookFormSelect;
You can use it in your app like this:
<ReactHookFormSelect
id="numero_prestacao"
name="numero_prestacao"
className={classes.textField}
label="Em quantas parcelas?"
control={control}
defaultValue={numero_prestacao || ""}
variant="outlined"
margin="normal"
>
<MenuItem value="">Escolha uma opção</MenuItem>
<MenuItem value="3">03 parcelas</MenuItem>
<MenuItem value="6">06 parcelas</MenuItem>
<MenuItem value="9">09 parcelas</MenuItem>
<MenuItem value="12">12 parcelas</MenuItem>
<MenuItem value="16">16 parcelas</MenuItem>
<MenuItem value="18">18 parcelas</MenuItem>
</ReactHookFormSelect>
Here is your codeSandBox updated with this component for the selects in the Information form:
https://codesandbox.io/s/unit-multi-step-form-kgic4?file=/src/Register/Information.jsx:4406-5238
RHF v7 update
Below is a minimal code example of MUI Select in a RHF form:
const { formState, getValues, watch, register, handleSubmit } = useForm();
const { errors } = formState;
<TextField
select
fullWidth
label="Select"
defaultValue=''
inputProps={register('currency', {
required: 'Please enter currency',
})}
error={errors.currency}
helperText={errors.currency?.message}
>
{currencies.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
Accepted version is correct but outdated.
At least in the version that I'm using: "react-hook-form": "^7.30.0" you should use the render parameter.
Here is the "updated" version that perfectly works for me:
<FormControl>
<InputLabel id="level-label">Level</InputLabel>
<Controller
name="level"
id="level"
defaultValue={level}
control={control}
render={({ field }) => (
<Select labelId="level-label" {...field}>
<MenuItem value={0}>0</MenuItem>
<MenuItem value={1}>1</MenuItem>
</Select>
)}
/>
<FormHelperText error={true}>{errors.level?.message}</FormHelperText>
</FormControl>
The important here is to propagate the field properties down to the child element (Select in our case)
PS. I don't think you need a separate component for it, it is pretty straight forward.
[Updated]
Here is a full code of one of my dialog. As per request from Deshan.
import {
Box, Chip, FormControl, Input, Stack,
} from '#mui/material';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import debounce from '../#utils/debounce';
import useRawParams from '../#utils/useRawParams';
import { useBrandsSearchQuery } from '../data/products';
import { SearchRoute } from '../SBRoutes';
import LoadingDiv from './LoadingDiv';
import SBDialog from './SBDialog';
import { useSearchBarContext } from '../contexts/SearchBarContext';
const context = { suspense: false };
/**
* Show the modal dialog with the list of brands, and search box for it
* Eeach brand will be as a link, for the SEO purposes
*/
export default function AllBrandsDialog({ open, setOpen }) {
const [t] = useTranslation();
const [query, setQuery] = useState('');
const [brands, setBrands] = useState([]);
const params = useRawParams(true);
const paramsBrands = params.brands?.split(',') || [];
const { setFilterActive } = useSearchBarContext();
const variables = useMemo(() => (query.length ? {
filterText: query,
} : null), [query]);
const [{ data, fetching: loading }] = useBrandsSearchQuery({ variables, pause: Boolean(!variables), context });
const debounceSetQuery = useCallback(debounce(200, (text) => {
setQuery(text);
}));
useEffect(() => {
if (!data || !open) return;
setBrands(data.brands || []);
}, [data, open]);
return (
<SBDialog open={open} setOpen={setOpen} title={t('Search and select a brand')}>
<Stack direction="column" spacing={2}>
<FormControl>
<Input
id="tagSearch"
placeholder={t('Start typing to see the brands')}
onChange={(e) => debounceSetQuery(e.target.value)}
autoFocus={true}
/>
</FormControl>
<Box display="grid" width={220} height={300} overflow="auto" gap={1} position="relative">
{brands?.map((brand) => (
<Chip
component={Link}
key={brand.id}
disabled={paramsBrands.indexOf(brand.url) > -1}
to={SearchRoute.generatePath({
...params,
brands: [...paramsBrands, brand.url],
page: undefined,
})}
size="small"
label={brand.nicename}
variant="outlined"
onClick={() => {
setOpen(false);
setFilterActive(false);
}}
clickable={true}
/>
))}
{loading && <LoadingDiv modal={true} />}
</Box>
</Stack>
</SBDialog>
);
}
AllBrandsDialog.propTypes = {
open: PropTypes.bool.isRequired,
setOpen: PropTypes.func.isRequired,
};
Here my code that working, hope it can help, need to use setValue
<TextField
fullWidth
inputRef={register({
name: 'name',
})}
select
onChange={e => setValue('name', e.target.value, true)}
label={label}
defaultValue={defaultValue}
>
{options.map((option) => (
<MenuItem key={option.label} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
Here using native select, do not need setValue, but value alway string
<TextField
fullWidth
select
SelectProps={{
native: true,
inputProps: { ref: register, name: 'name' }
}}
label={label}
defaultValue={defaultValue}
>
{options.map((option) => (
<option key={option.label} value={option.value}>
{option.label}
</option>
))}
</TextField>
This is an example that uses Material-UI with React hook form. You need to add the validation in 'inputRef' prop of TextField. Also you need to add 'onChange' function to keep the state updated. 'shouldValidate' will trigger the validation.
<TextField
select
name='city'
inputRef={register({ required: true })}
onChange={e => setValue('city', e.target.value, { shouldValidate: true })}
label="City"
defaultValue="">
{cityList.map((option, index) => (
<MenuItem key={index} value={option}>
{option}
</MenuItem>
))}
</TextField>
{errors.city && <ErrorText>City is required</ErrorText>}
✔ I came across this same issue, and this is how i solved mine:
<Select ... onChange={e => register({ name: 'academicLevel', value: e.target.value })}/>
more info
When you using react-hook-form with material UI, you don´t need to use onChange and setState. Only use inputRef and all works!
Just need to pass the register to the Input Ref
<Select
variant="outlined"
name="reason"
inputRef={register({ required: true })}
>
just use mui-react-hook-form-plus
Here is an example:
import { HookSelect, useHookForm } from 'mui-react-hook-form-plus';
const defaultValues = {
person: {
firstName: 'Atif',
lastName: 'Aslam',
sex: '',
},
};
const App = () => {
const { registerState, handleSubmit } = useHookForm({
defaultValues,
});
const onSubmit = (_data: typeof defaultValues) => {
alert(jsonStringify(_data));
};
return (
<HookSelect
{...registerState('person.sex')}
label='SEX'
items={[
{ label: 'MALE', value: 'male' },
{ label: 'FEMALE', value: 'female' },
{ label: 'OTHERS', value: 'others' },
]}
/>
)
}
Repo: https://github.com/adiathasan/mui-react-hook-form-plus
Demo: https://mui-react-hook-form-plus.vercel.app/?path=/docs/

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