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
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>
);
})}
</>
);
}}
/>
);
};
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"
/>
)}
</>
);
})}
</>
);
}}
/>
);
};
Related
Hello everyone and thank you for reading this! Here is my problem that i can't solve:
My application has the following functionality:
There are 2 inputs, then a button, when clicked, 2 more inputs appear and a button to send data from all inputs to the console, however, in the additional field, one input is required. This is where my problem arises: now, if I called additional inputs and filled in all the data, they are transferred to the console, if I didn’t fill in the required field, an error message goes to the console, BUT. I also need, in the event that I did NOT call additional inputs, the data of 2 basic inputs was transferred to the console. At the moment I can't figure it out.
import React, { useState } from "react";
import ReactDOM from "react-dom/client";
import produce from "immer";
const FunctionalBlock = ({
id,
idx,
isDeleted,
toggleBlockState,
additionalValue,
additionalTitle,
setNewBlock,
index,
}) => {
return (
<div
style={{
display: "flex",
maxWidth: "300px",
justifyContent: "space-between",
}}
>
{!isDeleted ? (
<React.Fragment>
<strong>{idx}</strong>
<input
type="text"
value={additionalTitle}
onChange={(e) => {
const additionalTitle = e.target.value;
setNewBlock((currentForm) =>
produce(currentForm, (v) => {
v[index].additionalTitle = additionalTitle;
})
);
}}
/>
<input
type="text"
value={additionalValue}
onChange={(e) => {
const additionalValue = e.target.value;
setNewBlock((currentForm) =>
produce(currentForm, (v) => {
v[index].additionalValue = additionalValue;
})
);
}}
/>
<button onClick={toggleBlockState}>now delete me</button>
</React.Fragment>
) : (
<button onClick={toggleBlockState}>REVIVE BLOCK</button>
)}
</div>
);
};
const Application = () => {
const [newBlock, setNewBlock] = useState([]);
const [firstInput, setFirstInput] = useState("");
const [secondInput, setSecondInput] = useState("");
const getNewBlock = (idx) => ({
id: Date.now(),
idx,
isDeleted: false,
additionalValue: "",
additionalTitle: "",
});
const toggleIsDeletedById = (id, block) => {
if (id !== block.id) return block;
return {
...block,
isDeleted: !block.isDeleted,
};
};
const createOnClick = () => {
const block = getNewBlock(newBlock.length + 1);
setNewBlock([...newBlock, block]);
};
const toggleBlockStateById = (id) => {
setNewBlock(newBlock.map((block) => toggleIsDeletedById(id, block)));
};
const showInputData = () => {
newBlock.map((item) => {
if (item.additionalTitle.length < 3) {
console.log("it is less than 3");
} else if (!item.additionalTitle && !item.additionalValue) {
console.log(firstInput, secondInput);
} else {
console.log(
firstInput,
secondInput,
item.additionalTitle,
item.additionalValue
);
}
});
};
return (
<div>
<div>
<input
type="text"
value={firstInput}
onChange={(e) => {
setFirstInput(e.target.value);
}}
/>
<input
type="text"
value={secondInput}
onChange={(e) => {
setSecondInput(e.target.value);
}}
/>
</div>
<div>
<button onClick={createOnClick}>ADD NEW INPUTS</button>
</div>
<div>
{newBlock.map((block, index) => (
<FunctionalBlock
key={index}
{...block}
toggleBlockState={() => toggleBlockStateById(block.id)}
setNewBlock={setNewBlock}
index={index}
/>
))}
</div>
<button onClick={showInputData}>send data</button>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Application />);
Here is this code on sandbox for those who decided to help me. Thank you!
https://codesandbox.io/s/vigilant-booth-xnef6t
I'm building a dynamic table using React-Table and i want to add a new row of editable cells.
At the moment i can add new row but only when i press the global edit button i can edit it, instead i want to add a row which would be editable at first.
This is my code -
Main component
function StyledTable() {
useEffect(() => {
dispatch(getData(mokeJsonData));
}, []);
const [datatoColumns] = useState(columnDataaa.slice(1));
const [skipPageReset, setSkipPageReset] = useState(false);
const data = useSelector((state) => state.dataReducer.data);
const dispatch = useDispatch();
const columns = useMemo(
() => [
{
Header: "",
id: "expander",
Cell2: ({ row }) => {
return (
<span {...row.getToggleRowExpandedProps()}>
{row.isExpanded ? "-" : "+"}
</span>
);
},
Cell: () => {
return <div></div>;
},
},
{
Header: columnDataaa[0].Header,
accessor: columnDataaa[0].accessor,
Cell: ({ value, row }) => {
return (
<FlexDiv>
<HighlightOffIcon
style={{ marginRight: "5px", color: "grey", width: "20px" }}
onClick={() => dispatch(deleteRow(row.index))}
/>
{value}
</FlexDiv>
);
},
},
...datatoColumns,
],
[]
);
useEffect(() => {
setSkipPageReset(false);
}, [data]);
const renderRowSubComponent = useCallback(
({ row }) => ({
values: row.original.addInfo && row.original.addInfo,
}),
[]
);
return (
<Styles>
<h1>הגדרת מנהל</h1>
<Table
columns={columns}
skipPageReset={skipPageReset}
renderRowSubComponent={renderRowSubComponent}
/>
</Styles>
);
}
export default StyledTable;
Editable Cell
const EditableCell = ({
value: initialValue,
row: { index },
column: { id, editable, type, width, valueOptions },
}) => {
const [value, setValue] = useState(initialValue);
const onChange = (e) => {
setValue(e.target.value);
};
const dispatch = useDispatch();
const onBlur = () => {
if (value === "") return alert("requiredddd");
return dispatch(updateMyData({ index, id, value }));
};
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
if (type === "singleSelect")
return (
<InputSelect
value={value}
onChange={onChange}
onBlur={onBlur}
style={{ width: width }}
>
{valueOptions.map((item, i) => {
return (
<option value={item.label} key={i}>
{item.label}
</option>
);
})}
</InputSelect>
);
if (type === "date")
return (
<DatePicker
style={{ width: width }}
type="date"
disabled={editable === false}
value={value}
onChange={onChange}
onBlur={onBlur}
/>
);
return (
<input
style={{ width: width }}
disabled={editable === false}
value={value}
onChange={onChange}
onBlur={onBlur}
/>
);
};
export default EditableCell;
Add Row function
addRow: (state, action) => {
const obj = {};
action.payload.slice(1).forEach((item) => {
obj[item.accessor] = '';
});
if (
obj &&
Object.keys(obj).length === 0 &&
Object.getPrototypeOf(obj) === Object.prototype
)
return;
else {
state.data.splice(0, 0, obj);
state.originalData = state.data;
}
},
Thanks
Pass the state variable and method to the useTable() root hook. custom plugin hooks and other variables/methods maintaining the component state are returned from the table instance. These you can later retrieve from anywhere you want.
const {
// all your hooks...
} = useTable(
{
columns,
data,
// all your other hooks...
updateMyData,
// pass state variables so that we can access them in edit hook later
editableRowIndex, // index of the single row we want to edit
setEditableRowIndex // setState hook for toggling edit on/off switch
},
// other hooks...
(hooks) => {
hooks.allColumns.push((columns) => [
// other hooks such as selection hook
...columns,
// edit hook
{
accessor: "edit",
id: "edit",
Header: "edit",
Cell: ({ row, setEditableRowIndex, editableRowIndex }) => (
<button
className="action-button"
onClick={() => {
const currentIndex = row.index;
if (editableRowIndex !== currentIndex) {
// row requested for edit access
setEditableRowIndex(currentIndex);
} else {
// request for saving the updated row
setEditableRowIndex(null); // keep the row closed for edit after we finish updating it
const updatedRow = row.values;
console.log("updated row values:");
console.log(updatedRow);
// call your updateRow API
}
}}
>
{/* single action button supporting 2 modes */}
{editableRowIndex !== row.index ? "Edit" : "Save"}
</button>
)
}
]);
}
);
you can found example from bellow link
github repo link: https://github.com/smmziaul/only-one-row-editable
code sandbox link: https://codesandbox.io/s/github/smmziaul/only-one-row-editable
Full example on CodeSandbox
(Css is a bit borked)
Writing anything into the input field or the textarea and then clicking on the select wipes the input field & the textarea, I am not sure why -
It seems that is because I am passing jsx elements to the HoverWrapper element.
When I just inlined the WrapInHover element it behaved as expected. Am I passing Elements in a bad way ?
Adding a key to the passed elements didn't seem to solve the issue ...
const Form = () => {
const selectInit = {
open: false,
initial: true,
selected: 'please select',
};
const selectReducer = (state, action) => {
switch (action.type) {
case 'toggle': {
return { ...state, open: !state.open };
}
case 'select': {
return { ...state, selected: action.selected, open: !state.open, initial: false };
}
}
};
const [selectState, selectDispatch] = useReducer(selectReducer, selectInit);
const selectHelp = selected => selectDispatch({ type: 'select', selected });
const OptionComp = ({ txt, value, onClick }) => (
<Option onClick={onClick} state={selectState} value={value}>
{selectState.open && selectState.selected === value ? null : <HoverBorder />}
{txt}
</Option>
);
const WrapInHover = ({ elements }) => {
const [hover, setHover] = useState(false);
return (
<div
css={css`
position: relative;
`}
onMouseEnter={() => {
setHover(true);
}}
onMouseLeave={() => {
setHover(false);
}}>
{elements}
<HoverBorder hover={hover} />
</div>
);
};
return (
<FormEl>
<WrapInHover elements={<Input key='ContactEmailInput' type='email' required />} />
<Label htmlFor='subject' onClick={() => selectDispatch({ type: 'toggle' })}>
Subject
</Label>
<Select>
<OptionComp
onClick={() => selectHelp('art')}
txt='I want you to paint something !'
value='art'
/>
{selectState.initial && !selectState.open ? (
<OptionComp
txt='Please Select An Option'
value='please select'
onClick={() => selectDispatch({ type: 'toggle' })}
/>
) : null}
</Select>
</FormEl>
);
};
Store value of input and message inside state. Also input will lose focus if your WrapInHover is inside main function
export default function App() {
const Form = () => {
const [formState, setFormState] = useState({ email: "", message: "" });
...
const handleFormDataChange = (e, type) => {
const {target: { value }} = e;
setFormState((prevState) => ({ ...prevState, [type]: value }));
};
return (
<FormEl>
<FormTitle>Contact me</FormTitle>
<Label htmlFor="email">Email</Label>
<WrapInHover
elements={
<Input
key="ContactEmailInput"
type="email"
value={formState.email}
onChange={(e) => handleFormDataChange(e, "email")}
required
/>
}
/>
...
<Label htmlFor="message">Message</Label>
<WrapInHover
elements={
<TextArea
key="ContactMessageTextArea"
name="message"
value={formState.message}
onChange={(e) => handleFormDataChange(e, "message")}
/>
}
/>
CSB Example - I will delete after 24 hours.
I'm making a filter function by checkbox. I made a reproduce like below. I changed values in array but checkbox checked status not change, what I missed? I think I must re-render list but it's also refill checked array to initial state. What should I do? Thanks!
import * as React from "react";
import "./styles.css";
import { Checkbox } from "antd";
const arr = [
{
name: "haha"
},
{
name: "haha2"
},
{
name: "hahaha"
}
];
const App = () => {
let [viewAll, setViewAll] = React.useState(true);
let checked = new Array(3).fill(true);
// render calendars checkbox
let list = arr.map((value, index) => {
return (
<Checkbox
style={{ color: "white" }}
checked={checked[index]}
onChange={() => handleFilter(value, index)}
className="check-box"
>
haha
</Checkbox>
);
});
const handleViewAll = () => {
setViewAll(!viewAll);
checked = checked.map(() => viewAll);
};
const handleFilter = (value, index) => {
setViewAll(false);
checked.map((_value, _index) => {
if (_index === index) {
return (checked[_index] = !checked[_index]);
} else {
return checked[_index];
}
});
console.log(checked);
};
return (
<div className="App">
<Checkbox checked={viewAll} onChange={() => handleViewAll()}>Check all</Checkbox>
{list}
</div>
);
};
export default App;
Here is codesanboxlink
You should create checked state. Check the code below.
let [viewAll, setViewAll] = React.useState(true);
let [checked, setChecked] = React.useState([true, true, true]);
// render calendars checkbox
let list = arr.map((value, index) => {
return (
<Checkbox
style={{ color: "black" }}
checked={checked[index]}
onChange={() => handleFilter(value, index)}
className="check-box"
>
{value.name}
</Checkbox>
);
});
const handleViewAll = () => {
setViewAll(!viewAll);
setChecked(() => checked.map(item => !viewAll));
};
const handleFilter = (value, index) => {
setViewAll(false);
setChecked(() =>
checked.map((_value, _index) => {
if (_index === index) return !checked[_index];
return checked[_index];
})
);
};
return (
<div className="App">
<Checkbox checked={viewAll} onChange={() => handleViewAll()}>
{checked}
</Checkbox>
{list}
</div>
);
codesandbox demo
You have to define checked array as a state value.
Right now your code is not firing render function because checked array is not props but also not state.
please find below code which contains name id and am rendering initially using map
am replacing id value to input type in UI
with the updated input type am trying to update the value onchange
update is not capturing and unable to update the input field
any suggestion?
please refer below snippet
import React, { useState } from "react";
const CstmInput = (props) => {
return (
<input
name={props.name}
type="text"
value={props.value}
onChange={(event) => props.onInputChange(event)}
/>
);
};
export default CstmInput;
import React, { useState } from "react";
import CstmInput from "./CstmInput";
const HierarcyTest = () => {
let rowData = [
{ name: "first", id: 10 },
{ name: "second", id: 20 },
];
const [data, setData] = useState(rowData);
const [name, setName] = useState({ fn: "test" });
const onInputChange = (e) => {
console.log("---event---", e.target.value);
setName({ ...name, fn: e.target.value });
};
let updateValue = () => {
let newData = data.map(
(item, index) =>
(item.id = (
<CstmInput name={item.name} value={item.id} onInputChange={(e) => onInputChange(e)} />
))
);
setData([...data, newData]);
};
return (
<div>
<div>Testing</div>
{data.map((val) => (
<h6>
{" "}
{val.name} {val.id}
</h6>
))}
<button onClick={updateValue}> Click </button>
</div>
);
};
export default HierarcyTest;
A few things why your code isn't working as intended:
1.
let updateValue = () => {
let newData = data.map((item, index) => {
if (item.id === 10) {
return [
(item.id = (
<CstmInput
value={item.id}
onInputChange={(e) => onInputChange(e)}
/>
)),
];
}
});
setData([...data, newData]);
};
In the above function inside the callback of map, you're only returning when a condition satisfies. Are you trying to filter the array instead? If not then return something when the if condition fails.
And why are you returning an array?
return [
(item.id = (
<CstmInput
value={item.id}
onInputChange={(e) => onInputChange(e)}
/>
)),
];
the above code seems logically wrong.
2.
const onInputChange = (e) => {
console.log("---event---", e.target.value);
setName({ ...name, fn: e.target.value });
};
If you want to update state which depends on the previous state then this is how you do it:
setName((prevState) => ({ ...prevState, fn: e.target.value }));
but since you're not actually relying on the properties of the previous state you can just use:
setName({fn: e.target.value });
Note that since your state only has one property and you want to update that single property you can completely overwrite the state, you don't need to spread the previous state.
update
change the updateValue function as the following:
let updateValue = () => {
setData(prevData => {
return prevData.map(el => {
return { ...el, id: <CstmInput value={el.id} onInputChange={(e) => onInputChange(e)} /> };
})
});
};
A stackblitz example I've created that implements what you're trying to do.