Asking
Asking

Reputation: 4200

Save Form values in ReactJS using checkboxes

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

Upvotes: 4

Views: 623

Answers (2)

white-wolf97
white-wolf97

Reputation: 429

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

Upvotes: 0

John Li
John Li

Reputation: 7447

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

Upvotes: 2

Related Questions