javascriptformscheckboxmaterial-uireact-hook-form

Material UI + React Form Hook + multiple checkboxes + default selected


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, "")


Solution

  • Breaking API changes made in 6.X:

    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)
          ])
        )
      });
    
    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, "")
      });