8

I am trying to build a form that accommodates multiple 'grouped' checkboxes using react-form-hook Material UI.

The checkboxes are created async from an 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.

ie. (boat_ids: [25, 29, 4])

How can I achieve that?

Here is a sample that I am trying to reproduce the issue.

Bonus point, validation of minimum selected checkboxes using Yup

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

1
  • couldn't you use setValue or reset API? Commented May 1, 2020 at 11:17

6 Answers 6

8
+100

Breaking API changes made in 6.X:

  • validation option has been changed to use a resolver function wrapper and a different configuration property name
    Note: Docs were just fixed for validationResolver->resolver, and code examples for validation in repo haven't been updated yet (still uses validationSchema for tests). It feels as if they aren't sure what they want to do with the code there, and it is in a state of limbo. I would avoid their Controller entirely until it settles down, or use Controller as a thin wrapper for your own form Controller HOC, which appears to be the direction they want to go in.
    see official sandbox demo and the unexpected behavior of "false" value as a string of the Checkbox for reference
import { yupResolver } from "@hookform/resolvers";
  const { register, handleSubmit, control, getValues, setValue } = useForm({
    resolver: yupResolver(schema),
    defaultValues: Object.fromEntries(
      boats.map((boat, i) => [
        `boat_ids[${i}]`,
        preselectedBoats.some(p => p.id === boats[i].id)
      ])
    )
  });
  • Controller no longer handles Checkbox natively (type="checkbox"), or to better put it, handles values incorrectly. It does not detect boolean values for checkboxes, and tries to cast it to a string value. You have a few choices:
  1. Don't use Controller. Use uncontrolled inputs
  2. Use the new render prop to use a custom render function for your Checkbox and add a setValue hook
  3. Use Controller like a form controller HOC and control all the inputs manually

Examples avoiding the use of Controller:
https://codesandbox.io/s/optimistic-paper-h39lq
https://codesandbox.io/s/silent-mountain-wdiov
Same as first original example but using yupResolver wrapper


Description for 5.X:

Here is a simplified example that doesn't require Controller. Uncontrolled is the recommendation in the docs. It is still recommended that you give each input its own name and transform/filter on the data to remove unchecked values, such as with yup and validatorSchema in the latter example, but for the purpose of your example, using the same name causes the values to be added to an array that fits your requirements.
https://codesandbox.io/s/practical-dijkstra-f1yox

Anyways, the problem is that your defaultValues doesn't match the structure of your checkboxes. It should be {[name]: boolean}, where names as generated is the literal string boat_ids[${boat.id}], until it passes through the uncontrolled form inputs which bunch up the values into one array. eg: form_input1[0] form_input1[1] emits form_input1 == [value1, value2]

https://codesandbox.io/s/determined-paper-qb0lf

Builds defaultValues: { "boat_ids[0]": false, "boat_ids[1]": true ... }
Controller expects boolean values for toggling checkbox values and as the default values it will feed to the checkboxes.

 const { register, handleSubmit, control, getValues, setValue } = useForm({
    validationSchema: schema,
    defaultValues: Object.fromEntries(
      preselectedBoats.map(boat => [`boat_ids[${boat.id}]`, true])
    )
  });

Schema used for the validationSchema, that verifies there are at least 2 chosen as well as transforms the data to the desired schema before sending it to onSubmit. It filters out false values, so you get an array of string ids:

  const schema = Yup.object().shape({
    boat_ids: Yup.array()
      .transform(function(o, obj) {
        return Object.keys(obj).filter(k => obj[k]);
      })
      .min(2, "")
  });
Sign up to request clarification or add additional context in comments.

5 Comments

Good explanation. It's better to also include the code in the answer itself (at least the important parts) in case codesandbox is not available.
This is the initial solution that i posted: codesandbox.io/s/react-hook-form-controller-079xx, but I think he wants the "value" instead of being checked true or false, there are multiple ways to tackle this, i would create a wrapper component and just let Controller to collect the "values", but if he just want checked value, it's really simple as you can see in the codesandbox link.
Exactly @Bill, I need the values instead of true/false. I would love a codesandbox with your proposed solution. I tried it before but I couldn't manage to make it work
I will do it tmr and paste here and github.
The validationSchema should convert the data passed to onSubmit. I've updated the codesandbox. You should see { boat_ids: [x1,x2] }
8

I've been struggling with this as well, here is what worked for me.

Updated solution for react-hook-form v6, it can also be done without useState(sandbox link below):

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(data => console.log(data))}>
      {["bill", "luo", "Manos", "user120242"].map(name => (
        <FormControlLabel
          control={
            <Controller
              name="names"
              render={({ onChange: onCheckChange }) => {
                return (
                  <Checkbox
                    checked={checkedValues.includes(name)}
                    onChange={() => onCheckChange(handleSelect(name))}
                  />
                );
              }}
              control={control}
            />
          }
          key={name}
          label={name}
        />
      ))}
      <button>Submit</button>
    </form>
  );
}


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

Another solution with default selected items done without useState: https://codesandbox.io/s/material-demo-bzj4i?file=/demo.js

2 Comments

I tried to modify your sandbox with the newest version of RHF codesandbox.io/s/material-demo-2m8je?file=/demo.js and also with the ability to reset the values afterwads, but I can see some strange behaviour from Controller...
@WojtekOwczarczyk , I updated the solution and made another sandbox for you - codesandbox.io/s/material-demo-gvbng?file=/demo.js You needed to reset both selectedValues and form fields.
3

This is my solution with react hook form 7, the other solutions don't work with reset or setValue.

<Controller
      name={"test"}
      control={control}
      render={({ field }) => (
        <FormControl>
          <FormLabel id={"test"}>{"label"}</FormLabel>
          <FormGroup>
            {items.map((item, index) => {
              const value = Object.values(item);
              return (
                <FormControlLabel
                  key={index}
                  control={
                    <Checkbox
                      checked={field.value.includes(value[0])}
                      onChange={() =>
                        field.onChange(handleSelect(value[0],field.value))
                      }
                      size="small"
                    />
                  }
                  label={value[1]}
                />
              );
            })}
          </FormGroup>
        </FormControl>
      )}
    />

link to codesandbox: Mui multiple checkbox

1 Comment

Cheers. Consider using const {name, label} = item, it's clearer than accessing indexes of const value = Object.values(item).
2

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

Comments

0

Here's my solution, which is not using all the default components from Material UI cause at my interface each radio will have an icon and text, besides the default bullet point not be showed:

const COMPANY = "company";

const INDIVIDUAL = "individual";

const [scope, setScope] = useState(context.scope || COMPANY);

const handleChange = (event) => {
  event.preventDefault();

  setScope(event.target.value);
};

<Controller
  as={
    <FormControl component="fieldset">
      <RadioGroup
        aria-label="scope"
        name="scope"
        value={scope}
        onChange={handleChange}
      >
        <FormLabel>
          {/* Icon from MUI */}
          <Business />

          <Radio value={COMPANY} />

          <Typography variant="body1">Company</Typography>
        </FormLabel>

        <FormLabel>
          {/* Icon from MUI */}
          <Personal />

          <Radio value={INDIVIDUAL} />

          <Typography variant="body1">Individual</Typography>
        </FormLabel>
      </RadioGroup>
    </FormControl>
  }
  name="scope"
  control={methods.control}
/>;

Observation: At this example I use React Hook Form without destruct:

const methods = useForm({...})

Comments

0

I spent 2 days to look for a code that has a "Check All" checkbox that would select all the checkboxes that are created dynamically. I wanted MUI material components, useForm and Zod.

This is my working solution that might help others:

This is the Zod file:

==== ScheduleDto.ts file ====
import { z } from 'zod';

// This is for the object that will be converted into multiple checkboxes
export const MoreCheckboxesDtoScheme= z.object({
    id: z.string(),
    name: z.string(),
    selected: z.boolean(),
});

// This is for the form with multiple checkboxes and other fields
export const FormDtoScheme = z.object({
    firstName: z.string().max(10).optional(),
    lastName: z.string().max(10).optional(),
    checkAll: z.boolean(),
    moreCheckboxes: z.array(MoreCheckboxesDtoScheme),
});

export type FormDto = z.infer<typeof FormDtoScheme >;

export const defaultFormValues = {
    firstName: '',
    lastName: '',
    checkAll: false,
    moreCheckboxes: [],
};

This is the React form file:

==== Form.tsx file ====
import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
    Accordion,
    AccordionDetails,
    AccordionSummary,
    Button,
    Checkbox,
    Dialog,
    DialogActions,
    DialogContent,
    DialogTitle,
    FormControl,
    FormControlLabel,
    FormGroup,
    InputLabel,
    MenuItem,
    Select,
    Stack,
    Typography,
} from '@mui/material';
import CloseIcon from '@mui/icons-material/CloseOutlined';
import { styled } from '@mui/material/styles';
import IconButton from '@mui/material/IconButton';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { Controller, useFieldArray, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import {
    FormDto,
    FormDtoScheme,
    defaultFormValues,
} from '@/features/manage-schedules/dto/ScheduleDto.ts';

export interface ScheduleDialogProps {
    open: boolean;
    onCancel: () => void;
    onSubmit: () => void;
}

export interface CheckboxesTemplate {
    id: number;
    name: string;
    selected: boolean;
    beginDate: string;
    endDate: string;
}

const moreCheckboxesArray: CheckboxesTemplate [] = [];
moreCheckboxesArray.push({
    id: 114664,
    name: '03/15/2023 SLT by Task',
    selected: false,
    beginDate: '2020-01-15',
    endDate: '2999-12-31',
} as CheckboxesTemplate);
moreCheckboxesArray.push({
    id: 105773,
    name: 'After HOO',
    selected: true,
    beginDate: '2022-01-01',
    endDate: '2999-12-31',
} as CheckboxesTemplate);
moreCheckboxesArray.push({
    id: 104789,
    name: 'By Checks (by Hours)',
    selected: false,
    beginDate: '2022-01-01',
    endDate: '2999-12-31',
} as CheckboxesTemplate);

export const ScheduleDialog: React.FC<
    ScheduleDialogProps
> = ({ open, onCancel, onSubmit }) => {

    const {
        control,
        getValues,
        formState: { isDirty, errors, isValid },
        handleSubmit,
        setValue,
    } = useForm<FormDto>({
        resolver: zodResolver(FormDtoScheme),
        mode: 'onChange',
        defaultValues: { ...defaultFormValues },
    });

    const { fields, append, remove } = useFieldArray({
        name: 'moreCheckboxes',
        control,
    });

useEffect(() => {
        remove();
        moreCheckboxesArray.forEach((template: any) => {
            append({
                id: template.id,
                name: template.name,
                selected: template.selected,
            });
        });
    }, []);

const handleSubmitAll = () => {
        console.log(isDirty);
        console.log(errors);
        console.log(isValid);
        console.log(getValues());
        console.log(getValues().moreCheckboxes);
        handleSubmit(onSubmit);
    };

    const handleCheckAll = (event: React.ChangeEvent<HTMLInputElement>) => {
        const { checked } = event.target;
        getValues().moreCheckboxes.forEach(
            (template) => (template.selected = checked)
        );
        setValue('moreCheckboxes', [...getValues().moreCheckboxes]);
    };

//...

return (
//...

<Stack direction="column">
    <Accordion>
        <AccordionSummary
            expandIcon={<ExpandMoreIcon />}
            aria-controls="panel1-content"
            id="panel1-header">
            <AccordionLabel>Many Checkboxes</AccordionLabel>
        </AccordionSummary>
        <AccordionDetails>
    <Stack direction="row">
            <FormGroup>
                <FormControl>
                    <Controller
                        control={control}                                                
                        name="checkAll"                                             
                        render={({ field }) => {
                        return (
                            <FormControlLabel
                                {...field}
                                control={
                                    <Checkbox 
                                    {...field} 
                                    onChange={(e) => {
                                        field.onChange(e);
                                        handleCheckAll(e);
                                    }}
                                    />
                                }
                                label={<BoldLabel>Check All</BoldLabel>}
                            />
                        );
                    }}
                    ></Controller>
            </FormControl>

{fields.map((item, index) => (
    <FormControl key={item.id}>
        <Controller 
            control={control} 
            name={`moreCheckboxes.${index}.selected`}
            render={({ field }) => {
                return (
                    <FormControlLabel
                        control={<Checkbox {...field} checked={field.value} />}
                        label={item.name}
                    />
                );
            }}
            ></Controller>
    </FormControl>
))}
</FormGroup>
</Stack>
</AccordionDetails>
</Accordion>
//...
    );
};

const SubmitButton = styled(Button)({
    paddingTop: '0.5rem',
    paddingBottom: '0.5rem',
    fontSize: '0.875rem',
    weight: 500,
    lineHeight: '24px',
    letterSpacing: '0.4px',
    paddingLeft: '1rem',
    paddingRight: '1rem',
});

const CancelButton = styled(SubmitButton)({
    marginLeft: '24px',
    color: grey[900],
});

const CloseIconButton = styled(IconButton)(({ theme }) => {
    return {
        position: 'absolute',
        right: theme.spacing(2),
        top: theme.spacing(1),
        color: theme.palette.grey[700],
    };
});

const AccordionLabel = styled(Typography)({
    paddingTop: '4px',
    fontSize: '1rem',
    letterSpacing: '0.4px',
    fontWeight: 500,
    lineHeight: '16px',
    color: grey[900],
    textAlign: 'left',
});

const BoldLabel = styled(Typography)({
    fontWeight: 600,
});

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.