Combining Material UI with React Form Hook to handle multiple checkboxes with pre-selected defaults

I am working on creating a form with grouped checkboxes using react-form-hook and Material UI.

The checkboxes are generated dynamically through an HTTP Request.

I am aiming to set the default values by providing an array of object IDs:

defaultValues: { boat_ids: trip?.boats.map(boat => boat.id.toString()) || [] }

In addition, I want to update the form's values in react-hook-form when a checkbox is selected or deselected by adding or removing the ID of the object.

For example: (boat_ids: [25, 29, 4])

Can you suggest how I can achieve this functionality?

You can access the sample that showcases the issue here.

As a bonus, I am also looking into implementing validation for minimum selected checkboxes using Yup:

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

Answer №1

Recent Updates to API in Version 6.X:

  • The validation option has undergone significant changes, now requiring the use of a resolver function wrapper and a different configuration property name.
    Note: While documentation has been updated to reflect the change from validationResolver to resolver, code examples in the repository have not been updated yet (still using validationSchema for tests). It appears that there is some uncertainty surrounding the code implementation, so it may be best to avoid using their Controller until things are more settled. Alternatively, consider using the Controller as a thin wrapper for your own form Controller HOC, which seems to align with the direction they are headed.
    see official sandbox demo for reference on unexpected behavior of "false" value as a string in the Checkbox
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 natively handles Checkbox (type="checkbox") or deals with values correctly. It fails to recognize boolean values for checkboxes and attempts to convert them to string values. Your options include:
  1. Avoid using Controller. Opt for uncontrolled inputs instead
  2. Utilize the new render prop to create a custom render function for Checkbox and incorporate a setValue hook
  3. Use Controller as a form controller HOC and manually manage all inputs

Examples demonstrating how to avoid the use of Controller:
https://codesandbox.io/s/optimistic-paper-h39lq
https://codesandbox.io/s/silent-mountain-wdiov
A similar example to the original one but utilizing the yupResolver wrapper


Description for Version 5.X:

Here is a simplified example that eliminates the need for Controller. The documentation recommends using an uncontrolled approach. It is advised to assign each input its unique name and filter/transmute the data to exclude unchecked values, such as demonstrated with Yup and validatorSchema in the latter example. However, in the context of your example, sharing the same name leads to the aggregation of values into an array that meets your requirements.
https://codesandbox.io/s/practical-dijkstra-f1yox

An issue arises when the structure of your checkboxes doesn't align with your defaultValues. It should be { [name]: boolean }, where names generated are the literal string boat_ids[${boat.id}], until they pass through the uncontrolled form inputs that consolidate the values into a single array. For instance, form_input1[0] form_input1[1] results in form_input1 == [value1, value2]

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

Constructing defaultValues: { "boat_ids[0]": false, "boat_ids[1]": true ... }

Controller anticipates boolean values for toggling checkbox states and uses these values as defaults fed to the checkboxes.

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

The schema employed for validationSchema ensures at least two choices are made and transforms the data to the desired format before submission. It filters out false values, resulting in 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, "")
  });

Answer №2

Dealing with this issue has been a challenge for me too, but I found a solution that worked well.

Here's an updated approach for react-hook-form version 6. You can achieve it without relying on useState (check out the sandbox link provided 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>
  );
}


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

Additionally, here's another method that involves default selected items and doesn't require the use of useState: https://codesandbox.io/s/material-demo-bzj4i?file=/demo.js

Answer №3

My approach using react hook form 7 differs from others as it effectively handles reset and setValue functionalities.

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

Check out the code on codesandbox: Mui multiple checkbox

Answer №4

Here is a functional example:

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 advise against using this method because Checkbox in material UI may be expected to return checked (boolean) instead of (value).

Answer №5

Here is a custom solution that diverges from the default Material UI components. In my interface, each radio button is accompanied by an icon and text, without displaying the default bullet point:

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

Note: This example utilizes React Hook Form without object destructuring:

const methods = useForm({...})

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Exploring nested JSON data to access specific elements

When I use console.log(responseJSON), it prints the following JSON format logs on the screen. However, I am specifically interested in printing only the latlng values. When I attempt to access the latlng data with console.log(responseJSON.markers.latlng) o ...

Increase the count for each category with the help of JavaScript

Currently, I am working on an application using PHP and have 180 vendors registered in the database. Each vendor has a unique ID stored in the database. I need to create a system where every time a user clicks on the "view details" button for a specific ...

What is the method for configuring the URL of an ajax request to open in a separate window?

I am currently working on an ajax call where I need to open a URL in a new tab or window. Since I'm still learning about ajax, I would greatly appreciate any help and explanation that you can provide. Below is the code snippet: $.ajax({ url: &apo ...

Express not invoking Passport LocalStrategy

I added a console.log statement in the LocalStrategy callback of passport.js, but it never seemed to execute. I am using Sequelize for ORM with MySQL. passport.js const LocalStrategy = require('passport-local').Strategy const passport = require( ...

Employing Modernizer.js to automatically redirect users to a compatible page if drag and drop functionality is not supported

I recently set up modernizer.js to check if a page supports drag and drop functionality. Initially, I had it set up so that one div would display if drag and drop was supported, and another div would show if it wasn't. However, I ran into issues with ...

Check the row in a JQuery table by using the .on("click") function to determine if a hyperlink within the row was clicked or if

I am in the process of building a website using the following libraries: Parse.js: 1.4.2 JQuery: 1.11.2 and 1.10.3 (U.I.) Twitter Bootstrap: 3.3.4 To demonstrate what I am trying to achieve, I have set up this JSfiddle with placeholder data: https://jsf ...

Is it possible to run two commands in npm scripts when the first command initiates a server?

When running npm scripts, I encountered an issue where the first command successfully starts a node server but prevents the execution of the second command. How can I ensure that both commands are executed successfully? package.json "scripts": { "dev ...

Getting the ID of a select input option in Vue.js using FormulateInput

Recently, I've been working with a FormulateInput select component that looks like this: <FormulateInput name="broj_zns-a" @change="setZns" :value="brojZnsa" type="select" label="Broj ZNS- ...

The MUI select box stays fixed in its position relative to both the height and width of the viewport while scrolling

Whenever I click on the MUI select, the dropdown box stays fixed in the viewport both horizontally and vertically as I scroll. Ideally, it should move along with the select. Any ideas on how to fix this issue? I attempted adjusting the positioning of the ...

Ways to display or conceal multiple div elements using JavaScript

A group of colleagues and I are currently collaborating on a project to create a website showcasing our research. We have incorporated 1 page that contains detailed descriptions of six different subjects: system biology, proteomics, genomics, transcripto ...

Creating a concise TypeScript declaration file for an established JavaScript library

I'm interested in utilizing the neat-csv library, however, I have encountered an issue with it not having a typescript definition file available. Various blogs suggest creating a basic definition file as a starting point: declare var neatCsv: any; M ...

When attempting to navigate to a different page in Next.js, the Cypress visit functionality may not function as

In my upcoming application, there are two main pages: Login and Cars. On the Cars page, users can click on a specific car to view more details about it. The URL format is as follows: /cars for the general cars page and /cars/car-id for the individual car p ...

Troubleshooting the issue of AngularJs location.path not successfully transferring parameters

My page has a Login section. Login.html <ion-view view-title="Login" name="login-view"> <ion-content class="padding"> <div class="list list-inset"> <label class="item item-input"> <input type="te ...

Setting up Mongoose with Admin JS in NestJS: A Step-By-Step Guide

After successfully configuring adminJS in my Nest JS application, it now runs smoothly on localhost:5000/admin. @Module({ imports: [ import('@adminjs/nestjs').then(({ AdminModule }) => AdminModule.createAdminAsync({ ...

What's the best way to navigate across by simply pressing a button located nearby?

Hey there! I'm new to JavaScript and I'm working on a shopping list page for practice. In my code, I can add new items to the list, but what I really want is to be able to cross out an item when I click the "Done" button next to it, and then uncr ...

Can anyone suggest a more efficient approach to handling variations using CSS?

Within the message component, there are currently only two variants available: error and success. This project is built using vue3 script setup and utilizes SCSS for styling. <script setup lang="ts"> defineOptions({ name: 'Notificat ...

Pattern to prevent consecutive hyphens and identical digits next to one another in a series

Here is a regular expression that can validate all numbers not being the same even after a hyphen: ^(\d)(?!\1+$)\d{3}-\d{1}$ For example, in the pattern: 0000-0 would not be allowed (all digits are the same) 0000-1 would be allowed 111 ...

Unable to sort array within a computed property in Vue.JS

Currently, I am working on enhancing my skills in Vue.js and have encountered an issue with sorting arrays in Vue.js. Despite using the sort(a-b) method for ascending order (least alphabet/value first), the array remains unchanged. Even with a function ins ...

Toggle a jQuery bar based on the presence of a specific CSS class

I am working on a feature where I can select and deselect images by clicking on a delete button. When an image is selected, a bar appears at the top. If I click the same delete button again, the image should be deselected and the bar should disappear. This ...

The AngularJS directive seems to be having trouble receiving the data being passed through its scope

Check out this HTML code snippet I created: <div ng-controller="ctrl"> <custom-tag title = "name" body = "content"> </custom-tag> </div> Take a look at the controller and directive implementation below: var mod = angular.mod ...