Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Material UI + multiple checkboxes + default selected #1517

Closed
mKontakis opened this issue Apr 28, 2020 · 48 comments
Closed

Material UI + multiple checkboxes + default selected #1517

mKontakis opened this issue Apr 28, 2020 · 48 comments
Labels
question Further information is requested

Comments

@mKontakis
Copy link

mKontakis commented Apr 28, 2020

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:

Screenshot 2020-04-28 at 03 10 04

Bonus point, validation of minimum selected checkboxes using Yup

boat_ids: Yup.array() .min(2, ""),

Thanks in advance!

@bluebill1049
Copy link
Member

can you update your example with Controller instead react-hook-form-input?

@bluebill1049 bluebill1049 added the status: need more detail Please follow our issue template. label Apr 28, 2020
@mKontakis
Copy link
Author

Done, not sure how is the correct way though, since that would answer my question :)

@bluebill1049
Copy link
Member

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 bluebill1049 added question Further information is requested and removed status: need more detail Please follow our issue template. labels Apr 28, 2020
@mKontakis
Copy link
Author

@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.

@bluebill1049
Copy link
Member

I have to check the doc for MUI:
https://material-ui.com/components/checkboxes/

Screen Shot 2020-04-28 at 5 46 08 pm

@mKontakis
Copy link
Author

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,

Screenshot 2020-04-28 at 15 28 00

@mKontakis
Copy link
Author

With chakra ui and formik that would be like this:

https://codesandbox.io/s/reverent-goldstine-1bvq9?file=/src/App.js

@bluebill1049
Copy link
Member

maybe you should try to use chakra with RHF.

@bluebill1049
Copy link
Member

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).

@4ortytwo
Copy link

4ortytwo commented May 6, 2020

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

@bluebill1049
Copy link
Member

i love solutions! thanks very much @4ortytwo

@mKontakis
Copy link
Author

@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:

https://codesandbox.io/s/autumn-pond-8tfip?file=/src/App.js

@bluebill1049
Copy link
Member

@mKontakis can you raise this as an issue?

@bluebill1049
Copy link
Member

bluebill1049 commented May 6, 2020

wait didn't i supply you with a working version? (above)

@mKontakis
Copy link
Author

mKontakis commented May 6, 2020

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

@bluebill1049
Copy link
Member

right, please create a separate issue I will do some investigation tonight.

@natac13
Copy link

natac13 commented Jun 17, 2020

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 😞

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

@4ortytwo
Copy link

@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

@natac13
Copy link

natac13 commented Jun 17, 2020

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 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.

@bluebill1049
Copy link
Member

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 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.

V6 will make this exp easier, with render props (Controller).

@harrisrobin
Copy link

For anyone looking at this with v6.x.x, the workarounds here do not work at all in v6.01 currently

@bluebill1049
Copy link
Member

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.

@4ortytwo
Copy link

4ortytwo commented Jul 7, 2020

This solution works with v6 now.

https://codesandbox.io/s/material-demo-54nvi?file=/demo.js

@bluebill1049
Copy link
Member

Thanks for sharing @4ortytwo

@mKontakis
Copy link
Author

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?

@4ortytwo
Copy link

@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.

@bluebill1049
Copy link
Member

bluebill1049 commented Jul 21, 2020

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.

@bluebill1049
Copy link
Member

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

@4ortytwo
Copy link

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

@bluebill1049
Copy link
Member

nice @4ortytwo yeah! personally I think it's cleaner with a single Controller. Thanks for making those CSBs. ❤️

@4ortytwo
Copy link

It is cleaner, indeed. My pleasure, @bluebill1049! Thanks for the great library! :)

@sandeepkumar-vedam-by
Copy link

sandeepkumar-vedam-by commented Jul 24, 2020

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

@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 ?

@bluebill1049
Copy link
Member

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

@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?

@sandeepkumar-vedam-by
Copy link

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

@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 ?

@bluebill1049
Copy link
Member

bluebill1049 commented Jul 24, 2020

what do you mean custom message @sandeepkumar-vedam-by? can't you produce different messages based on the selection from validate function?

@sandeepkumar-vedam-by
Copy link

@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

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

@bluebill1049
Copy link
Member

you can have a switch statement inside right? based on your array value return specific error message.

@sandeepkumar-vedam-by
Copy link

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 :)

@bsides
Copy link

bsides commented Sep 21, 2020

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?

@ahmedrafayat
Copy link

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

@bsides
Copy link

bsides commented Sep 29, 2020

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

@chase2981
Copy link

chase2981 commented Nov 24, 2020

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

Could anybody tell me how to get the reset to work properly within @4ortytwo's latest "Without useState" example?

Or at least how to get the checkboxes to reflect the reset form state?

https://codesandbox.io/s/material-demo-forked-9ekb9?file=/demo.js:2171-2214

@4ortytwo
Copy link

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

Could anybody tell me how to get the reset to work properly within @4ortytwo's latest "Without useState" example?

Or at least how to get the checkboxes to reflect the reset form state?

https://codesandbox.io/s/material-demo-forked-9ekb9?file=/demo.js:2171-2214

Here is a solution I've come up with, @chase2981 : you'll need to swap defaultChecked for checked and, in this case, pass it this bit of logic that returns a boolean

 props.value.includes(item.id) 

so the checkbox is checked only if it was actually checked or initially set. Resetting works as well.

Hope it helps.

Check the link below
https://codesandbox.io/s/material-demo-forked-8lbmn?file=/demo.js:1808-1847

@VitDev17
Copy link

VitDev17 commented Apr 13, 2021

Hi everyone I don't know if it could still help someone but I find a solution . That's not necessarily a best practice but i made it work like this:

<Controller
	key={inputIndexCounter}
	name={`patientAnswers[${inputIndexCounter}]`}
	defaultValue={currentPatientRegistry.patientAnswers[currentPatAnswersIdx]}
	control={control}
	render={(props) => (
		<SpCheckbox
			formControlStyle={formControlStyleCheck}
			style={{ marginTop: 15 }}
			checked={props.value}
			onChange={(e) => {
				if (e.target.checked) {
					props.onChange(questionAnswer.id);
				} else {
					props.onChange(e.target.checked);
				}
			}}
			rightlabel
			variant='column'
			label={questionAnswer.answer}
			></SpCheckbox>
	)}
/>

@ViieeS
Copy link

ViieeS commented Jun 25, 2021

import FormControlLabel from '@material-ui/core/FormControlLabel';
import Switch, {SwitchProps} from '@material-ui/core/Switch';
import React from 'react';
import {useController, UseControllerOptions} from 'react-hook-form';

interface Props extends SwitchProps {
  control: UseControllerOptions['control'];
  rules?: UseControllerOptions['rules'];
  label: React.ReactNode;
}

const ControlledSwitch: React.VFC<Props> = ({control, name, rules, label, ...switcherProps}) => {
  const {
    field: {ref, onChange, ...inputProps},
  } = useController({
    name,
    control,
    rules,
    defaultValue: false,
  });

  return (
    <FormControlLabel
      className="m-0"
      control={
        <Switch
          {...inputProps}
          {...switcherProps}
          inputRef={ref}
          checked={value}
          onChange={(e) => onChange(e.target.checked)}
        />}
      label={<span className="font-xs">{label}</span>}
    />
  );
}

export default ControlledSwitch;

@zirkelc
Copy link

zirkelc commented Aug 27, 2021

Here's my solution without additional state and without a separate handler function. I use Lodash's _.xor()function to toggle the checkbox element from the value array.

<Controller
  name="topics"
  control={control}
  defaultValue={[]}
  render={({ field: { value, onChange, ...inputProps }, fieldState: { error } }) => (
    <>
      {topics.map((topic) => (
        <FormControlLabel
          key={topic}
          label={topic}
          control={
            <Checkbox checked={value?.includes(topic)} onChange={() => onChange(_.xor(value, [topic]))} />
          }
        />
      ))}
    </>
  )}
/>

@antialias
Copy link

why was this issue closed?

@moiseshp
Copy link

moiseshp commented Jan 17, 2022

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

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 11, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests