I am trying to build a form which accommodates multiple 'grouped' checkboxes using Material UI.
The checkboxes are created async from a HTTP Request.
I want to provide an array of the objects IDs as the default values:
defaultValues: {
boat_ids: trip?.boats.map(boat => boat.id.toString()) || []
},
Also, when I select or deselect a checkbox, I want to add/remove the ID of the object to the values
of react-hook-form.
How can I achieve that?
Here is a sample that I am trying to reproduce the issue:
https://codesandbox.io/s/smoosh-dream-zmszs?file=/src/App.js
Right now with what I have tried, I get this:
Bonus point, validation of minimum selected checkboxes using Yup
boat_ids: Yup.array()
.min(2, ""),
Thanks in advance!
can you update your example with Controller
instead react-hook-form-input
?
Done, not sure how is the correct way though, since that would answer my question :)
This is probably a more relevant question for MUI's checkbox, but i just quicky did it with my own understanding.
https://codesandbox.io/s/serverless-dream-jm134?file=/src/App.js
@bluebill1049 I am not sure if it's MUI checkbox question.
I want to handle integers (the objects IDs), not true/false when checking/unchecking.
I have to check the doc for MUI:
https://material-ui.com/components/checkboxes/
I am trying to do something like this:
const handleCheck = (event, isInputChecked) => {
let checked = event[1];
if (checked) {
return value;
} else {
return null;
}
// onChange(event, isInputChecked, this.props.category);
};
<FormControlLabel
value={value}
control={
<Controller
as={<Checkbox />}
name={name}
type="checkbox"
value={value}
onChange={handleCheck}
register={register}
control={control}
/>
}
label={`Boat ${value}`}
/>
But no luck,
With chakra ui and formik that would be like this:
https://codesandbox.io/s/reverent-goldstine-1bvq9?file=/src/App.js
maybe you should try to use chakra with RHF.
Here is a working version:
import React from "react";
import { useForm, Controller } from "react-hook-form";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Checkbox from "@material-ui/core/Checkbox";
export default function CheckboxesGroup() {
const { control, handleSubmit } = useForm({
defaultValues: {
bill: "bill",
luo: ""
}
});
return (
<form onSubmit={handleSubmit(e => console.log(e))}>
{["bill", "luo"].map(name => (
<Controller
key={name}
name={name}
as={
<FormControlLabel
control={<Checkbox value={name} />}
label={name}
/>
}
valueName="checked"
type="checkbox"
onChange={([e]) => {
return e.target.checked ? e.target.value : "";
}}
control={control}
/>
))}
<button>Submit</button>
</form>
);
}
codesandbox link: https://codesandbox.io/s/material-demo-65rjy?file=/demo.js:0-932
However, I do not recommend doing so, because Checkbox in material UI probably should return checked (boolean) instead of (value).
I'd like to post another solution as well:
import React, { useState } from "react";
import { useForm, Controller } from "react-hook-form";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Checkbox from "@material-ui/core/Checkbox";
export default function CheckboxesGroup() {
const defaultNames = ["bill", "Manos"];
const { control, handleSubmit } = useForm({
defaultValues: { names: defaultNames }
});
const [checkedValues, setCheckedValues] = useState(defaultNames);
function handleSelect(checkedName) {
const newNames = checkedValues?.includes(checkedName)
? checkedValues?.filter(name => name !== checkedName)
: [...(checkedValues ?? []), checkedName];
setCheckedValues(newNames);
return newNames;
}
return (
<form onSubmit={handleSubmit(e => console.log(e))}>
{["bill", "luo", "Manos", "user120242"].map(name => (
<FormControlLabel
control={
<Controller
as={<Checkbox />}
control={control}
checked={checkedValues.includes(name)}
name="names"
onChange={() => handleSelect(name)}
/>
}
key={name}
label={name}
/>
))}
<button>Submit</button>
</form>
);
}
Codesandbox link: https://codesandbox.io/s/material-demo-639rq?file=/demo.js
i love solutions! thanks very much @4ortytwo
@bluebill1049
It seems that there is a problem with 5.6.1
.
The initial states of the checkboxes break on the latest version. They are checked but their ui state is not updated.
Try to switch from 5.6.1 and 5.6.0 and refresh on that example:
@mKontakis can you raise this as an issue?
wait didn't i supply you with a working version? (above)
You did mate, but I preferred to use that solution https://stackoverflow.com/a/61541837/2200675 to transform the values using Yup instead of modifying the returned value of Material's component CheckBox.
Although 5.6.1 breaking that solution may be a bug that should be fixed? I checked the changes between 5.6.1 and 5.6.0 I didn't manage to find something relevant
right, please create a separate issue I will do some investigation tonight.
Hey I was able to get almost the result I was looking for with @4ortytwo solution. Everything is working except for the reset()
. I am using this field on a form that can edited or not and when cancelling the edit I want to reset back to the original value. Which happens just not visually.
The only way I can get this working is to stop using Material ui Checkbox and use a basic `. Just need to match styling now :disappointed:
Are there any ideas on how to get the reset functionality to work with @4ortytwo solution?
Here is a modified codesandbox which simulates what my setup is doing when fetching data with Apollo and reset in the useEffect
https://codesandbox.io/s/material-demo-0m6x5?file=/demo.js
Also note I am using this code to get the initial list value as the data comes from Apollo client
const initialList = R.path(['defaultValuesRef', 'current', name])(control)
useEffect(() => {
if (initialList) {
setCheckedList(initialList)
}
}, [initialList])
And here is the full component for reference, note the reset
is called in the parent component which handles the entire form
const CourseAvailableTo = (props) => {
const {
control,
name,
error,
className,
darkDisabled,
required,
disabled,
checkboxes,
label,
headerLabel,
register,
} = props
const classes = useStyles()
const hasError = !!error
const [checkedList, setCheckedList] = useState([])
const initialList = R.path(['defaultValuesRef', 'current', name])(control)
useEffect(() => {
if (initialList) {
setCheckedList(initialList)
}
}, [initialList])
const handleChange = useCallback(
(value) => {
const onList = checkedList.includes(value)
const oldList = [...checkedList]
let newList = [...oldList]
if (onList) {
// remove from list
newList = R.reject(R.equals(value))(oldList)
} else {
// add to list
newList = R.append(value)(oldList)
}
setCheckedList(newList)
// control.setValue(name, newList)
return newList
},
[checkedList]
)
return (
<div className={clsx(classes.wrapper, className)}>
<Typography variant={headerLabel} gutterBottom>
{`${label}:${required ? ' *' : ''}`}
</Typography>
<FormControl component="fieldset" className={classes.checkboxFormControl}>
<FormGroup error={hasError} className={classes.checkboxFormGroup}>
{checkboxes.map((checkbox, index) => (
<FormControlLabel
className={clsx(
classes.checkbox,
darkDisabled && classes.darkDisabledCheckBoxLabel
)}
key={checkbox.id}
control={
<Controller
as={<Checkbox />}
className={clsx(darkDisabled && classes.darkDisabledCheckBox)}
disabled={disabled}
value={checkbox.value}
onChange={(e) => handleChange(checkbox.value)}
checked={checkedList.includes(checkbox.value)}
name={`${name}`}
control={control}
/>
// <input
// type="checkbox"
// className={clsx(darkDisabled && classes.darkDisabledCheckBox)}
// disabled={disabled}
// name={name}
// value={checkbox.value}
// ref={register}
// />
}
label={checkbox.label}
/>
))}
</FormGroup>
<FormHelperText error={hasError} className={classes.errorMessage}>
{hasError && error}
</FormHelperText>
</FormControl>
<Button
type="button"
className={classes.selectAllButton}
variant="outlined"
color="primary"
disabled={disabled}
onClick={() => {
if (R.length(checkedList) <= R.length(checkboxes) - 1) {
control.setValue(name, R.pluck('value')(checkboxes))
return setCheckedList(R.pluck('value')(checkboxes))
}
control.setValue(name, [])
return setCheckedList([])
}}
>
{`${
R.length(checkedList) <= R.length(checkboxes) - 1
? 'Select All'
: 'Deselect All'
}`}
</Button>
</div>
)
}
@natac13 , your checked
state depends on the checkedValues
array and it wasn't getting updated when you reset the form.
I added this:
const handleReset = () => {
setCheckedValues(defaultNames);
reset();
};
and called it here
<button onClick={handleReset}>Reset</button>
See the codesandbox, it is working there: https://codesandbox.io/s/material-demo-0bwsi?file=/demo.js
That works! Thanks @4ortytwo. However I would have to move the setCheckedValues
function out of my custom field component to the form component, then pass in the state and state update function. This then causes the form component to re-render!! :scream: If I lose Material ui and use the example found here almost no re-render happens! Which is the goal of rewriting all my forms from Formik to react-hook-forms.
Again thank you for the solution which works great! However I think I may just make a custom checkbox component which using <input type="checkbox"/>
and use the checkbox hack
Also this library is amazing! No re-renders on inputs! Just hard to use Material ui. At least that is what I am finding moving from Formik.
That works! Thanks @4ortytwo. However I would have to move the
setCheckedValues
function out of my custom field component to the form component, then pass in the state and state update function. This then causes the form component to re-render!! 😱 If I lose Material ui and use the example found here almost no re-render happens! Which is the goal of rewriting all my forms from Formik to react-hook-forms.Again thank you for the solution which works great! However I think I may just make a custom checkbox component which using
<input type="checkbox"/>
and use the checkbox hackAlso this library is amazing! No re-renders on inputs! Just hard to use Material ui. At least that is what I am finding moving from Formik.
V6 will make this exp easier, with render props (Controller).
For anyone looking at this with v6.x.x, the workarounds here do not work at all in v6.01 currently
For anyone looking at this with v6.x.x, the workarounds here do not work at all in v6.01 currently
use render
prop it should be much easier now.
This solution works with v6 now.
Thanks for sharing @4ortytwo
Again, issues with this use-case on v6.0.8.
Let's recap:
Let's say that I have an array of objects as such:
const items = [
{
id: 0,
name: "Object 0"
},
{
id: 1,
name: "Object 1"
},
{
id: 2,
name: "Object 2"
},
{
id: 3,
name: "Object 3"
},
{
id: 4,
name: "Object 4"
}
];
Each object corresponds to one checkbox. The user should be able to select multiple and get a result in the form of:
item_ids: [1, 3, 4]
, with the IDs in the array being the selected checkboxes.
The group of the checkboxes should have initial states.
I am trying to use the render
method, but it looks like something is wrong with the value
. When i select checkboxes i get strings instead of booleans.
I tried some workarounds but nothing works, the array is not populated/removed properly.
Here is the codesandbox:
https://codesandbox.io/s/material-demo-2wvuq?file=/demo.js
@bluebill1049 Maybe you have an opinion on that?
@mKontakis, I'm not Bill but came up with a couple of solutions.
With and without useState
.
Adjusted your Yup resolver as well, it filters out falsy fields now but would transform your values into an array of indices otherwise.
WITHOUT useState
:
https://codesandbox.io/s/material-demo-bzj4i?file=/demo.js
WITH useState
:
https://codesandbox.io/s/material-demo-ofk6d?file=/demo.js
With useState
is probably redundant now that it can be done without it, just wanted to show that it's possible, too.
Thanks @4ortytwo, @mKontakis i will take a look at this one. maybe consider staying at the old version (if works for you) until i get you a solution.
wow @4ortytwo your example works great, i think it would probably easier to build a controlled component which wrapt those checkbox and a single Controller just to collect value
instead map around Conrtroller
Thanks @bluebill1049, adjusted both examples and wrapped the checkboxes with a Controller
.
Interested to know whether you meant it this way.
Without useState
https://codesandbox.io/s/material-demo-bzj4i?file=/demo.js
With useState
https://codesandbox.io/s/material-demo-ofk6d?file=/demo.js
nice @4ortytwo yeah! personally I think it's cleaner with a single Controller
. Thanks for making those CSBs. ❤️
It is cleaner, indeed. My pleasure, @bluebill1049! Thanks for the great library! :)
Thanks @bluebill1049, adjusted both examples and wrapped the checkboxes with a
Controller
.Interested to know whether you meant it this way.
Without
useState
https://codesandbox.io/s/material-demo-bzj4i?file=/demo.jsWith
useState
https://codesandbox.io/s/material-demo-ofk6d?file=/demo.js
@4ortytwo @bluebill1049 Hello, Thanks for the solutions with MuiCheckboxes without useState. How can we do validation without yup library by just providing rules from RHF ?
Thanks @bluebill1049, adjusted both examples and wrapped the checkboxes with a
Controller
.
Interested to know whether you meant it this way.
WithoutuseState
https://codesandbox.io/s/material-demo-bzj4i?file=/demo.js
WithuseState
https://codesandbox.io/s/material-demo-ofk6d?file=/demo.js@4ortytwo @bluebill1049 Hello, Thanks for the solutions with MuiCheckboxes without useState. How can we do validation without yup library by just providing rules from RHF ?
you can use the validate function right?
Thanks @bluebill1049, adjusted both examples and wrapped the checkboxes with a
Controller
.
Interested to know whether you meant it this way.
WithoutuseState
https://codesandbox.io/s/material-demo-bzj4i?file=/demo.js
WithuseState
https://codesandbox.io/s/material-demo-ofk6d?file=/demo.js@4ortytwo @bluebill1049 Hello, Thanks for the solutions with MuiCheckboxes without useState. How can we do validation without yup library by just providing rules from RHF ?
you can use the validate function right?
@bluebill1049 Yes. We can add validate function but how can we attach custom message to that ?
what do you mean custom message @sandeepkumar-vedam-by? can't you produce different messages based on the selection from validate
function?
@bluebill1049 Here is the validate function and when value length is 0
i want to display error message like 'Select atleast one item'
rules: {
validate: (value: any) => {
return value.length > 0 ? true: false;
},
}
Generally, we can add message to rules like below
rules: {
required: {
value: true,
message: 'Email is Required'
},
pattern: {
value: /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/,
message: "Invalid email address"
},
minLength: {
value: 5,
message: 'First Name must be atleast 2 characters'
}
}
you can have a switch statement inside right? based on your array value return specific error message.
you can have a switch statement inside right? based on your array value return specific error message.
@bluebill1049 Yes. It is working. I didn't think that we can directly return message in validate function. Thanks :)
If not asking too much, could you do your examples with Typescript @4ortytwo ? It's almost there, the render function complains here about this:
Type '(props: { onChange: (...event: any[]) => void; onBlur: () => void; value: any; name: string; }) => Element[]' is not assignable to type '(data: { onChange: (...event: any[]) => void; onBlur: () => void; value: any; name: string; }) => ReactElement<any, string | ((props: any) => ReactElement<any, any> | null) | (new (props: any) => Component<any, any, any>)>'.
Type 'Element[]' is missing the following properties from type 'ReactElement<any, string | ((props: any) => ReactElement<any, any> | null) | (new (props: any) => Component<any, any, any>)>': type, props, keyts(2322)
controller.d.ts(6, 5): The expected type comes from property 'render' which is declared here on type '(IntrinsicAttributes & { as: "input" | "select" | "textarea" | FunctionComponent<any> | ComponentClass<any, any> | ReactElement<...>; render?: undefined; } & { ...; } & Pick<...>) | (IntrinsicAttributes & ... 2 more ... & Pick<...>)'
I'm unsure how to fix it. It looks like the return from render is expected to be something else than an array so when you put the array there, the type won't validate.
EDIT: I do think it's a problem with the render
prop, could anyone confirm?
If not asking too much, could you do your examples with Typescript @4ortytwo ? It's almost there, the render function complains here about this:
Type '(props: { onChange: (...event: any[]) => void; onBlur: () => void; value: any; name: string; }) => Element[]' is not assignable to type '(data: { onChange: (...event: any[]) => void; onBlur: () => void; value: any; name: string; }) => ReactElement<any, string | ((props: any) => ReactElement<any, any> | null) | (new (props: any) => Component<any, any, any>)>'. Type 'Element[]' is missing the following properties from type 'ReactElement<any, string | ((props: any) => ReactElement<any, any> | null) | (new (props: any) => Component<any, any, any>)>': type, props, keyts(2322) controller.d.ts(6, 5): The expected type comes from property 'render' which is declared here on type '(IntrinsicAttributes & { as: "input" | "select" | "textarea" | FunctionComponent<any> | ComponentClass<any, any> | ReactElement<...>; render?: undefined; } & { ...; } & Pick<...>) | (IntrinsicAttributes & ... 2 more ... & Pick<...>)'
I'm unsure how to fix it. It looks like the return from render is expected to be something else than an array so when you put the array there, the type won't validate.
EDIT: I do think it's a problem with the
render
prop, could anyone confirm?
I also got this same error, this is because the render prop does not allow an array of JSX elements. So I just wrapped my mapped JSX with a fragment. This managed to "fool" typescript
I just threw a @ts-ignore
in that line because I intentionally want to go back to this issue later (and it is a hack).
If you could please update the render method to also accept JSX.Element[]
of whatever is accepting now.
Nice fix too @ahmedrafayat
This is my final element:
<Controller
name="ids"
control={control}
// @ts-ignore
// <Controller /> render doesnt expect an array of elements (JSX.Element[])
// BUT it works regardless. So I'm ignoring TS typecheck here until it's fixed
render={() =>
arrayOfElements.map((element) => (
<FormControlLabel
control={
<Checkbox
onChange={() => handleCheck(element.id)}
checked={checkedValues.includes(element.id)}
/>
}
key={element.id}
label={element.action}
/>
))
}
/>
Most helpful comment
Thanks @bluebill1049, adjusted both examples and wrapped the checkboxes with a
Controller
.Interested to know whether you meant it this way.
Without
useState
https://codesandbox.io/s/material-demo-bzj4i?file=/demo.js
With
useState
https://codesandbox.io/s/material-demo-ofk6d?file=/demo.js