4

I created a form component using react hook forms. The component is composed from a group of checkboxes and a text input. The text input appears when user click on the last checkbox custom. The idea of this one is: when the user will click on it appears a text input and the user can add a custom answer/option. Ex: if user type test within the input then when the user will save the form, there should appear in an array test value, but custom text should't be in the array. In my application i don't have access to const onSubmit = (data) => console.log(data, "submit");, so i need to change the values within Component component. Now when i click on submit i get in the final array the custom value.
Question: how to fix the issue described above?

const ITEMS = [
  { id: "one", value: 1 },
  { id: "two", value: 2 },
  { id: "Custom Value", value: "custom" }
];

export default function App() {
  const name = "group";
  const methods = useForm();
  const onSubmit = (data) => console.log(data, "submit");

  return (
    <div className="App">
      <FormProvider {...methods}>
        <form onSubmit={methods.handleSubmit(onSubmit)}>
          <Component ITEMS={ITEMS} name={name} />
          <input type="submit" />
        </form>
      </FormProvider>
    </div>
  );
}
export const Component = ({ name, ITEMS }) => {
  const { control, getValues } = useFormContext();
  const [state, setState] = useState(false);

  const handleCheck = (val) => {
    const { [name]: ids } = getValues();

    const response = ids?.includes(val)
      ? ids?.filter((id) => id !== val)
      : [...(ids ?? []), val];

    return response;
  };

  return (
    <Controller
      name={name}
      control={control}
      render={({ field, formState }) => {
        return (
          <>
            {ITEMS.map((item, index) => {
              return (
                <>
                  <label>
                    {item.id}
                    <input
                      type="checkbox"
                      name={`${name}[${index}]`}
                      onChange={(e) => {
                        field.onChange(handleCheck(e.target.value));
                        if (index === ITEMS.length - 1) {
                          setState(e.target.checked);
                        }
                      }}
                      value={item.value}
                    />
                  </label>
                  {state && index === ITEMS.length - 1 && (
                    <input
                      {...control.register(`${name}[${index}]`)}
                      type="text"
                    />
                  )}
                </>
              );
            })}
          </>
        );
      }}
    />
  );
};


demo: https://codesandbox.io/s/winter-brook-sml0ww?file=/src/Component.js:151-1600

10
  • 1
    It looks like you already got that working. I verified using that link that on submit the text the user typed in the custom field is what is being logged to the console. Maybe I did not understand your question. Are you trying to prevent seeing the "custom" text once the field is made visible? Commented Jan 19, 2023 at 17:31
  • 1
    @codejockie, try this: select all checkboxes and add text in input, after submit, then deselect one checkbox and submit , you will see that values are not saved correctly. Did you find the issue? Commented Jan 19, 2023 at 17:37
  • 1
    @codejockie, could you help please? Commented Jan 19, 2023 at 18:04
  • 1
    I have slightly modified your code. Please see the following link for the example: codesandbox.io/s/cocky-aryabhata-7jprlr?file=/src/Custom.js Commented Jan 19, 2023 at 18:08
  • 1
    @codejockie, how to get an array of values? Example: [first, second, inputValue] Commented Jan 19, 2023 at 18:45

2 Answers 2

2
+75

Assuming that the goal is to keep all the selections in the same group field, which must be an array that logs the selected values in provided order, with the custom input value as the last item if specified, perhaps ideally it would be easier to calculate the values in onSubmit before submitting.

But since the preference is not to add logic in onSubmit, maybe an alternative option could be hosting a local state, run the needed calculations when it changes, and call setValue manually to sync the calculated value to the group field.

Forked demo with modification: codesandbox

import "./styles.css";
import { Controller, useFormContext } from "react-hook-form";
import React, { useState, useEffect } from "react";

export const Component = ({ name, ITEMS }) => {
  const { control, setValue } = useFormContext();
  const [state, setState] = useState({});

  useEffect(() => {
    const { custom, ...items } = state;
    const newItems = Object.entries(items).filter((item) => !!item[1]);
    newItems.sort((a, b) => a[0] - b[0]);
    const newValues = newItems.map((item) => item[1]);
    if (custom) {
      setValue(name, [...newValues, custom]);
      return;
    }
    setValue(name, [...newValues]);
  }, [name, state, setValue]);

  const handleCheck = (val, idx) => {
    setState((prev) =>
      prev[idx] ? { ...prev, [idx]: null } : { ...prev, [idx]: val }
    );
  };

  const handleCheckCustom = (checked) =>
    setState((prev) =>
      checked ? { ...prev, custom: "" } : { ...prev, custom: null }
    );

  const handleInputChange = (e) => {
    setState((prev) => ({ ...prev, custom: e.target.value }));
  };

  return (
    <Controller
      name={name}
      control={control}
      render={({ field, formState }) => {
        return (
          <>
            {ITEMS.map((item, index) => {
              const isCustomField = index === ITEMS.length - 1;
              return (
                <React.Fragment key={index}>
                  <label>
                    {item.id}
                    <input
                      type="checkbox"
                      name={name}
                      onChange={(e) =>
                        isCustomField
                          ? handleCheckCustom(e.target.checked)
                          : handleCheck(e.target.value, index)
                      }
                      value={item.value}
                    />
                  </label>
                  {typeof state["custom"] === "string" && isCustomField && (
                    <input onChange={handleInputChange} type="text" />
                  )}
                </React.Fragment>
              );
            })}
          </>
        );
      }}
    />
  );
};
Sign up to request clarification or add additional context in comments.

Comments

0

Ok, so after a while I got the solution. I forked your sandbox and did little changes, check it out here: Save Form values in ReactJS using checkboxes

Basically, you should have an internal checkbox state and also don't register the input in the form, because this would add the input value to the end of the array no matter if that value is "".

Here is the code:

import "./styles.css";
import { Controller, useFormContext } from "react-hook-form";
import { useEffect, useState } from "react";

export const Component = ({ name, ITEMS }) => {
  const { control, setValue } = useFormContext();
  const [state, setState] = useState(false);
  const [checkboxes, setCheckboxes] = useState(
    ITEMS.filter(
      (item, index) => index !== ITEMS.length - 1
    ).map(({ value }, index) => ({ value, checked: false }))
  );
  useEffect(() => {
    setValue(name, []); //To initialize the array as empty
  }, []);

  const [inputValue, setInputValue] = useState("");

  const handleChangeField = (val) => {
    const newCheckboxes = checkboxes.map(({ value, checked }) =>
      value == val ? { value, checked: !checked } : { value, checked }
    );
    setCheckboxes(newCheckboxes);

    const response = newCheckboxes
      .filter(({ checked }) => checked)
      .map(({ value }) => value);
    return state && !!inputValue ? [...response, inputValue] : response;
  };

  const handleChangeInput = (newInputValue) => {
    const response = checkboxes
      .filter(({ checked }) => checked)
      .map(({ value }) => value);
    if (state) if (!!newInputValue) return [...response, newInputValue];
    return response;
  };

  return (
    <Controller
      name={name}
      control={control}
      render={({ field, formState }) => {
        return (
          <>
            {ITEMS.map((item, index) => {
              return (
                <>
                  <label>
                    {item.id}
                    <input
                      type="checkbox"
                      name={`${name}[${index}]`}
                      onChange={(e) => {
                        if (index === ITEMS.length - 1) {
                          setState(e.target.checked);
                          return;
                        }
                        field.onChange(handleChangeField(e.target.value));
                      }}
                      value={item.value}
                    />
                  </label>
                  {state && index === ITEMS.length - 1 && (
                    <input
                      value={inputValue}
                      onChange={(e) => {
                        setInputValue(e.target.value);
                        field.onChange(handleChangeInput(e.target.value));
                      }}
                      type="text"
                    />
                  )}
                </>
              );
            })}
          </>
        );
      }}
    />
  );
};

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.