Im fetching data from an API and changing the value from the frontend to display it in a table. Im fetching a list of objects and storing it in a state and displaying the objects in the state in a html table. The table has a checkbox to display a boolean value. If the value is true, then defaultChecked is true in the checkbox. There's a checkbox in the table header to check or uncheck all items.
The following is the json object which i fetch.
{
completed: false
id: 1
title: "delectus aut autem"
userId: 1
}
If I checked the checkbox in the table header, I want to set completed to true in all 200 items.
The following is the code to set all items to either true or false.
const checkAllHandler = (e) => {
let val = e.target.checked;
console.log(val);
let allTodoList = [];
if (val === true) {
if (todo.length > 0) {
for (let index = 0; index < todo.length; index++) {
const newObject = {
userId: todo[index].userId,
id: todo[index].id,
title: todo[index].title,
completed: true,
};
allTodoList.push(newObject);
}
setTodo(allTodoList);
}
} else if (val === false) {
if (todo.length > 0) {
for (let index = 0; index < todo.length; index++) {
const newObject = {
userId: todo[index].userId,
id: todo[index].id,
title: todo[index].title,
completed: false,
};
allTodoList.push(newObject);
}
setTodo(allTodoList);
}
}
};
When I console the todo state, all the values are updated to either true or false but it doesnt show on the table.
The table has a filter function. If I filter a word and go back, then the values of entire table is changing. I want to display the change when the checkbox is clicked and not when search and go back. How to I do that?
The complete code of the component is below.
const DataTable = () => {
const { loading, items } = useSelector((state) => state.allData);
// console.log(items)
const dispatch = useDispatch();
// const history = useNavigate();
const [searchText, setsearchText] = useState("");
const [todo, setTodo] = useState(items);
console.log("todo");
console.log(todo);
const [isFetch, setisFetch] = useState(false);
const [checkedloading, setcheckedLoading] = useState(false);
const [isChecked, setisChecked] = useState(false);
console.log(isChecked);
useEffect(() => {
const setDataToState = () => {
if (loading === false) {
setTodo(items);
}
};
setDataToState();
}, [loading]);
useEffect(() => {
dispatch(getData());
// setTodo(items)
// console.log(items)
}, [dispatch]);
//etTodo(items)
const handleSearch = (event) => {
//let value = event.target.value.toLowerCase();
setsearchText(event.target.value);
};
const onChangeHandler = (e, item) => {
console.log(e.target.checked);
console.log(item);
if (e.target.checked === true) {
// const item = todo.filter(x=> x.id === id)
// console.log("added")
// console.log(item)
for (let index = 0; index < todo.length; index++) {
if (todo[index].id === item.id) {
console.log(todo[index]);
console.log("deleting");
todo.splice(index, 1);
console.log("deleted");
const newObject = {
userId: item.userId,
id: item.id,
title: item.title,
completed: true,
};
console.log("adding updated object");
todo.splice(index, 0, newObject);
console.log("added");
console.log(todo);
}
}
} else {
// const item = todo.filter(x=> x.id === id)
// console.log("removed")
// console.log(item)
for (let index = 0; index < todo.length; index++) {
if (todo[index].id === item.id) {
console.log(todo[index]);
console.log("deleting");
todo.splice(index, 1);
console.log("deleted");
const newObject = {
userId: item.userId,
id: item.id,
title: item.title,
completed: false,
};
console.log("adding updated object");
todo.splice(index, 0, newObject);
console.log("added");
console.log(todo);
}
}
}
};
const onSubmitHandler = () => {
localStorage.setItem("items", JSON.stringify(todo));
};
const getItem = () => {
const items = localStorage.getItem("items");
console.log("local storage items");
console.log(JSON.parse(items));
};
const checkAllHandler = async (e) => {
const { checked } = e.target;
console.log(checked);
setTodo((todos) =>
todos.map((todo) => ({
...todo,
completed: checked,
}))
);
};
return (
<>
{console.log("todo in render")}
{console.log(todo)}
<div className={styles.container}>
<div className={styles.top}>
<div className={styles.search_bar}>
<input
type="text"
onChange={(e) => handleSearch(e)}
placeholder="search by name"
/>
</div>
</div>
<div className={styles.btn_container}>
<button onClick={onSubmitHandler}>Submit</button>
</div>
<div className={styles.data_table_container}>
{checkedloading === false ? (
<>
<div className={styles.data_table}>
{loading || todo === null || todo === undefined ? (
<>
<p>Loading!!</p>
</>
) : (
<>
<table>
<tr>
<th>ID</th>
<th>userId</th>
<th>Title</th>
<th>
<>
Completed
<input type="checkbox" onChange={checkAllHandler} />
</>
</th>
</tr>
<tbody>
{todo
.filter((val) => {
if (searchText === "") {
return val;
} else if (
val.title.toLowerCase().includes(searchText)
) {
return val;
}
})
.map((item) => (
<>
<tr key={item.id}>
<td>{item.id}</td>
<td>{item.userId}</td>
<td>{item.title}</td>
<td>
<input
type="checkbox"
defaultChecked={item.completed}
onClick={(e) => onChangeHandler(e, item)}
/>
</td>
</tr>
</>
))}
</tbody>
</table>
</>
)}
</div>
</>
) : (
<>Loading</>
)}
</div>
</div>
</>
);
};
CodeSandbox link: https://codesandbox.io/s/flamboyant-proskuriakova-60t19
I don't see any overt issues in this code, but it is quite verbose. Both logic branches are identical save for the completed boolean value assigned. When updating arrays in React it is common to use functional state updates to make the shallow copy of the previous state, not whatever state may be closed over in the callback scope.
Example:
const checkAllHandler = (e) => {
const { checked } = e.target;
console.log(checked);
setTodo(todos => todos.map(todo => ({
...todo,
completed: checked,
})));
};
If there's still further issue from here with updating the table then please add the rest of the component code and any other code you think is relevant.
Update
The reason none of the checkbox inputs are changing in the table is because you've used the defaultChecked prop, which makes these inputs fully uncontrolled inputs. They take the initial item.completed value when mounted and don't change from there other than if you interact with the checkbox.
If you want them to respond to changes/updates to the todo state then they should be converted to fully controlled inputs and use the checked prop.
<input
type="checkbox"
checked={item.completed}
onClick={(e) => onChangeHandler(e, item)}
/>
Update #2
The individual checkbox inputs were being mutated with Array.prototype.splice in onChangeHandler. .splice does an in-place mutation. Again a functional state update should be used to shallow copy the previous state and check for the matched todo object by id.
const onChangeHandler = (e, item) => {
const { checked } = e.target;
setTodo((todos) =>
todos.map((todo) =>
todo.id === item.id
? {
...todo,
completed: checked
}
: todo
)
);
};
Related
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"
/>
)}
</>
);
})}
</>
);
}}
/>
);
};
I'm building this website with MERN stack and having this rendering bug:
On start, I have a foodList table rendering out all of the food in the database.
I already have a useEffect() with the foodList inside the dependency array - so anytime the users make changes to the foodList table (Add/Edit/Delete), it will instantly render out that added dish without refreshing the page.
When users search for something in this Search & Filter bar, it will hide the foodList table and return a table of searchedFood that is filtered from the foodList array.
But when the users use this Search & Filter functionality and then try to Edit/Delete from that searchedFood table. It won't render the changes instantly - they have to refresh the page to see the changes they made.
This might relate to the useEffect() but I don't know how to apply it for the searchedFood table without disrupting the foodList table.
App.js
export default function App() {
const [foodName, setFoodName] = useState('')
const [isVegetarian, setIsVegetarian] = useState('no')
const [priceRange, setPriceRange] = useState('$')
const [foodUrl, setFoodUrl] = useState('')
const [foodList, setFoodList] = useState([])
const [searchedFood, setSearchedFood] = useState([])
const [noResult, setNoResult] = useState(false)
// Display food list:
useEffect(() => {
let unmounted = false
Axios.get("https://project.herokuapp.com/read")
.then((response) => {
if (!unmounted) {
setFoodList(response.data)
}
})
.catch(error => {
console.log(`The error is: ${error}`)
return
})
return () => {
unmounted = true
}
}, [foodList])
// Add Food to list:
const addToList = async (event) => {//Axios.post logic in here}
// Paginate states:
const [currentPage, setCurrentPage] = useState(1)
const [foodPerPage] = useState(5)
// Get current food:
const indexOfLastFood = currentPage * foodPerPage
const indexOfFirstFood = indexOfLastFood - foodPerPage
const currentFood = foodList.slice(indexOfFirstFood, indexOfLastFood)
const currentSearchedFood = searchedFood.slice(indexOfFirstFood, indexOfLastFood)
const paginate = (pageNumber) => {
setCurrentPage(pageNumber)
}
return (
<section>
<FilterSearch
foodList={foodList}
searchedFood={searchedFood}
setSearchedFood={setSearchedFood}
noResult={noResult}
setNoResult={setNoResult}
paginate={paginate}
/>
{noResult ? <ResultNotFound/>
:
<FoodListTable
foodName={foodName}
priceRange={priceRange}
isVegetarian={isVegetarian}
foodUrl={foodUrl}
foodList={foodList}
currentFood={currentFood}
searchedFood={searchedFood}
currentSearchedFood={currentSearchedFood}
totalFood={foodList.length}
totalSearchedFood={searchedFood.length}
currentPage={currentPage}
paginate={paginate}
noResult={noResult}
foodPerPage={foodPerPage}
/>
}
</section>
)
}
FoodListTable.js
export default function FoodListTable(props) {
return (
<div>
<table>
<thead>
<tr>
<th>
Food name
</th>
<th>Price</th>
<th>
Action
</th>
</tr>
</thead>
<body>
// Return a table with data from searchFood on search:
{props.searchedFood.length > 0 ? props.currentSearchedFood.map((val) => {
return (
<FoodListRow
val={val}
key={val._id}
foodName={val.foodName}
isVegetarian={val.isVegetarian}
priceRange={val.priceRange}
foodUrl={val.foodUrl}
/>
)
}) : props.currentFood.map((val) => { // If not on search, return a table with data from foodList:
return (
<FoodListRow
val={val}
key={val._id}
foodName={val.foodName}
isVegetarian={val.isVegetarian}
priceRange={val.priceRange}
foodUrl={val.foodUrl}
/>
)
})
}
</tbody>
</table>
// Display different Pagination on searched table and food list table:
{props.searchedFood.length > 0 ?
<Pagination foodPerPage={props.foodPerPage} totalFood={props.totalSearchedFood} paginate={props.paginate} currentPage={props.currentPage} />
:<Pagination foodPerPage={props.foodPerPage} totalFood={props.totalFood} paginate={props.paginate} currentPage={props.currentPage} />
}
</div>
)
}
FoodListRow.js
export default function FoodListRow(props) {
// Edit food name:
const [editBtn, setEditBtn] = useState(false)
const handleEdit = () => {
setEditBtn(!editBtn)
}
// Update Food Name:
const [newFoodName, setNewFoodName] = useState('')
const updateFoodName = (id) => {
if (newFoodName) {
Axios.put("https://project.herokuapp.com/update", {
id: id,
newFoodName: newFoodName,
})
.catch(error => console.log(`The error is: ${error}`))
}
}
// Delete food:
const deleteFood = (id) => {
const confirm = window.confirm(`This action cannot be undone.\nAre you sure you want to delete this dish?`);
if(confirm === true){
Axios.delete(`https://project.herokuapp.com/delete/${id}`)
}
}
return (
<tr key={props.val._id}>
<td>
{props.val.foodName}
{editBtn &&
<div>
<input
type="text"
name="edit"
placeholder="New food name.."
autoComplete="off"
onChange={(event) => {setNewFoodName(event.target.value)}}
/>
<button
onClick={() => updateFoodName(props.val._id)}
>
✓
</button>
</div>
}
</td>
<td>{props.val.priceRange}</td>
<td>
<a
href={props.val.foodUrl}
target="_blank"
rel="noopener noreferrer"
>
🔗
</a>
<button
onClick={handleEdit}
>
✏️
</button>
<button
onClick={() => deleteFood(props.val._id)}
>
❌
</button>
</td>
</tr>
);
}
As Mohd Yashim Wong mentioned, we need to re-render every time there's change to the backend.
I ditched the foodList inside the useEffect()'s dependency array and try another method because this is not the correct way to re-render the axios calls. It just keeps sending read requests indefinitely if I use this way. That might be costly.
This is what I have switched to:
I set the dependency array empty
Pull the data from the backend and return it to the frontend after the axios calls
addToList function:
const addToList = async (event) => {
event.preventDefault()
try {
await Axios.post(
"https://project.herokuapp.com/insert",
{
foodName: foodName,
isVegetarian: isVegetarian,
priceRange: priceRange,
foodUrl: foodUrl,
}
)
.then((response) => {
// Return the data to the UI:
setFoodList([...foodList, { _id: response.data._id, foodName: foodName, isVegetarian: isVegetarian, priceRange: priceRange, foodUrl: foodUrl }])
setFoodName('')
setIsVegetarian('no')
setPriceRange('$')
setFoodUrl('')
})
} catch(err) {
console.error(`There was an error while trying to insert - ${err}`)
}
}
updateFoodName function:
const updateFoodName = (id) => {
if (newFoodName) {
Axios.put("https://project.herokuapp.com/update", {
id: id,
newFoodName: newFoodName,
})
.then(() => {
// Update on searchedFood:
props.searchedFood.length > 0 ?
props.setSearchedFood(props.searchedFood.map((val) => {
return (
val._id === id ?
{
_id: id,
foodName: newFoodName,
isVegetarian: props.isVegetarian, priceRange: props.priceRange,
foodUrl: props.foodUrl,
} : val
)
})) //Update on foodList
: props.setFoodList(props.foodList.map((val) => {
return (
val._id === id ?
{
_id: id,
foodName: newFoodName,
isVegetarian: props.isVegetarian, priceRange: props.priceRange,
foodUrl: props.foodUrl,
} : val
)
}))
})
.catch(error => console.log(`Update name failed: ${error}`))
}
}
deleteFood function:
const deleteFood = (id) => {
const confirm = window.confirm(`This action cannot be undone.\nAre you sure you want to delete this dish?`);
if(confirm === true){
Axios.delete(`https://project.herokuapp.com/delete/${id}`)
.then(() => {
props.searchedFood.length > 0
? props.setSearchedFood(props.searchedFood.filter((val) => {
return val._id !== id
}))
: props.setFoodList(props.foodList.filter((val) => {
return val._id !== id
}))
})
}
}
You are never updating the text of the food name. Inside FoodListRow, you should create a state for the name of the food. Set this equal to props.val.foodName and then update it at the end of updateFoodName() after the axios request.
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
I need to make that when selecting checkbox Select All, the rest of the checkboxes from the list are checked, and each checkbox can be selected separately. But when select one of the checkboxes does not checked the previous checkbox.
sandbox
const options = [ 'Selected Item 1', 'Selected Item 2', 'Selected Item 3'];
export default function App() {
const [selected, setSelected] = useState([]);
const isAllSelected =
options.length > 0 && selected.length === options.length;
const handleChange = (event) => {
const value = event.target.value;
console.log(value)
if (value === "all") {
setSelected(selected.length === options.length ? [] : options);
return;
}
setSelected(value);
};
const listItem = options.map((option) => {
return (
<div key={option}>
<Checkbox
value={option}
onChange={handleChange}
checked={selected.includes(option) } />
<span>{option}</span>
</div>
)
})
return (
<div style={{ display: 'flex', alignItems: 'center', margin: 10 }}>
<Checkbox
value='all'
onChange={handleChange}
checked={isAllSelected}
/>
<span> Select All</span>
{listItem}
</div>
);
}
When you click on checkboxes other than "all" checkbox, you are just setting your selected state to that value: setSelected(value);. There are 2 problems with this code - 1) In your case the selected state should always be an array, but you are changing that to a string. 2) you are disregarding any previous values that might be added to the selected state. You must figure out if the clicked checkbox was already in the selected state or not. if not, you should add it to the state. If it was then you should remove it from the state. Here is how you can do this:
const handleChange = (event) => {
const value = event.target.value;
console.log(value)
if (value === 'all') {
setSelected(selected.length === options.length ? [] : options);
return;
}
if (selected.indexOf(value) !== -1) { // if value already present
const newSelected = selected.filter((s) => s !== value );
setSelected(newSelected);
} else { // if value not present
setSelected([ ...selected, value ]);
}
}
You can make a data-structure like [{data:any, selected: boolean}, {data:any, selected: boolean}]
Then you loop through it and display them as
const [items, setItems] = useState<Asset[]>([]);
const [select, setSelect] = useState(false)
const handleChange = (index, checked) => {
if(select && !checked) setSelect(false)
const itemRef = [...items];
itemRef[index].selected = checked
setItems([...itemRef]);
}
const handleChangeAll = (checked) => {
const itemRef = [...items];
itemRef.forEach((_item, i) => {
itemRef[i].selected = checked
})
setSelect(checked)
setItems([...itemRef]);
}
------
All selection checkbox
<Checkbox
checked={select}
onChange={(_e, checked) => handleChangeAll(checked)}
/>
** items **
items.map((item, i) => (
<Checkbox
key={item.data.id}
checked={item.selected}
onChange={(_e, checked) => handleChange(index, checked)}
/>
))
Once it is done if you want to extract the selected data, you can just apply the filter method based on item.selected from items array,
Need to update selected options array instead of assigning the value directly inside handleChange method. So first check if selected list contains the checked option if it contains then remove it, otherwise add it to the selected list.
const handleChange = (event) => {
const value = event.target.value;
console.log(value);
if (value === "all") {
setSelected(selected.length === options.length ? [] : options);
return;
}
// added below code to update selected options
const list = [...selected];
const index = list.indexOf(value);
index === -1 ? list.push(value) : list.splice(index, 1);
setSelected(list);
};
Sandbox link here
I have a list of buttons and they are multi selectable. when I select the buttons I want to add , it will be added to the array perfectly and turned to blue and when I click on a already selected button it should be get removed from the array and turned to white but it doesn't. Below shows what I tried so far.
The first array (products) is to save the API data. Second one is to save the selected products.
const [products, setProducts] = useState([]);
const [selectedProducts, setselectedProducts] = useState<any>([]);
{products.length !== 0 ? (
products?.map(
(item: any, index) => (
<SButton
key={item.key}
label={item.value}
onClick={() => {
selectedProducts(item);
}}
isSelected={item.selected === "YES"}
/>
)
)
) : (
<p>No products</p>
)}
function selectedProducts(item:any){
if(selectedProducts.length !== 0){
selectedProducts.map((selecteditem:any)=>{
if(selecteditem.key == item.key ){
item.selected = "NO";
setselectedProducts(selectedProducts.filter((item: any )=> item.key !== selecteditem.key))
}else{
item.selected = "YES";
setselectedProducts([...selectedProducts, item]);
}
})
}else{
setselectedProducts([...selectedProducts, item]);
item.selected = "YES";
}
}
How about something like this?
const [products, setProducts] = useState([]);
const [selectedProduct, setSelectedProduct] = useState();
{(products.length > 0) ? (
<Fragment>
{products.map((item)=>{
const {key, value, selected } = item;
return (
<SButton
key={key}
label={value}
onClick={() => {
setSelectedProduct(item);
const newState = !selected;
products.forEach((p)=>{
if (p.key === item.key) p.selected = newState;
});
setProducts([...products]);
}}
isSelected={selected}
/>
);
})}
</Fragment>
): (
<p>No products</p>
)}
First, you are using selectedProducts both as function name and name of selected products state.
Second, you should not assign values to item. Use spread operator instead.
Also, you can access the previous state from setState instead of using state directly.
function removeFromSelectedProducts(item: any) {
// Set selected to 'NO' in products array
setProducts((prevProducts) =>
prevProducts.filter((product) =>
product.key === item.key ? { ...product, selected: 'NO' } : product
)
)
// Remove product from selectedProducts
setSelectedProducts((prevSelectedProducts) =>
prevSelectedProducts.filter((product) => product.key !== item.key)
)
}
function addToSelectedProducts(item: any) {
// Set selected to 'YES' in products array
setProducts((prevProducts) =>
prevProducts.filter((product) =>
product.key === item.key ? { ...product, selected: 'YES' } : product
)
)
// Add item to selectedProducts
setSelectedProducts((prevSelectedProducts) => [...prevSelectedProducts, { ...item, selected: 'YES'}])
}
function selectProduct(item: any) => {
if (selectedProducts.some((product) => product.key === item.key)) {
removeFromSelectedProducts(item)
} else {
addToSelectedProducts(item)
}
}
You can simplify this using useReducer hook instead of using separate functions for addition & removal of selected products.