Reputation: 4200
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
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
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