Sorry for the long question in advance but I have been struggling with this for some time
To give some context of what I am trying to accomplish I am loading this table when my app loads
And when I click the button inside red square will bring the user to edit mode
In edit mode user should be able to for example toggle the High School Core
(inside green square) checkbox and then discard the change by clicking Discard Changes
button (inside blue square)
The thing is that it's not working, this checkbox should not be enabled because that Discard Changes
button is only setting the editMode
state to false and by doing that the table should be created by mapping other object: const whichCourse = editMode ? modifiedValues : originalValues
The original data from which originalValues
and modifiedValues
are created is passed to the <Table />
component as a prop (that's a requirement for this app) from App.tsx
import { v4 as uuidv4 } from "uuid";
import { Container } from "@mui/material";
import Table from "./Table";
const ID1 = uuidv4();
const ID2 = uuidv4();
const ID3 = uuidv4();
export const typedData = {
bundles: [
{
id: ID1,
name: "High School Core",
},
{
id: ID2,
name: "Middle School Core",
},
{
id: ID3,
name: "Elementary School Core",
},
],
schools: [
{
id: uuidv4(),
name: "First School",
licensedproducts: [ID1, ID2],
},
{
id: uuidv4(),
name: "Second School",
licensedproducts: [ID2, ID3],
},
],
};
export default function App() {
return (
<Container>
<Table propsData={typedData} />
</Container>
);
}
File Table.tsx
contains the following to render the UI and handle all logic
import { useState, useEffect } from "react";
import CheckIcon from "@mui/icons-material/Check";
import { Box, Grid, Button, Checkbox } from "@mui/material";
import { typedData } from "./App";
const tableStyles = {
display: "block",
overflowX: "auto",
paddingTop: "36px",
whiteSpace: "nowrap",
fontFamily: "Helvetica Neue",
"& table": {
width: "100%",
textAlign: "center",
borderCollapse: "collapse",
},
"& th, td": {
px: "17px",
color: "#1E1E24",
fontSize: "14px",
fontWeight: "400",
lineHeight: "40px",
},
"& th": {
borderBottom: "2px solid #00006D",
},
"& td": {
borderBottom: "1px solid #dddddd",
},
"& th:nth-of-type(1), td:nth-of-type(1), th:nth-of-type(2), td:nth-of-type(2)": {
textAlign: "left",
},
};
export default function Table({ propsData }: { propsData: typeof typedData }) {
const [editMode, setEditMode] = useState(false);
const [originalValues, setOriginalValues] = useState({ ...propsData });
const [modifiedValues, setModifiedValues] = useState({ ...propsData });
useEffect(() => {
console.log("running useEffect");
setOriginalValues({ ...propsData });
setModifiedValues({ ...propsData });
}, [propsData]);
const whichCourse = editMode ? modifiedValues : originalValues;
const keyComplement = editMode ? "yes" : "not";
const toggleEdit = () => {
setEditMode((current) => !current);
};
const saveButton = () => {
setOriginalValues(modifiedValues);
};
return (
<Box sx={{ textAlign: "center", pt: "10px" }}>
<Grid container spacing={2}>
<Grid item xs>
<Button variant="contained" onClick={toggleEdit}>
{editMode ? "Discard changes" : `Edit Mode - ${keyComplement}`}
</Button>
</Grid>
{editMode && (
<Grid item xs>
<Button variant="contained" onClick={saveButton}>
Save changes
</Button>
</Grid>
)}
</Grid>
<Box sx={tableStyles}>
<Box component="table" sx={{ overflowX: "auto" }} tabIndex={0}>
<thead>
<tr>
<Box component="th">ID</Box>
<Box component="th">School Name</Box>
{whichCourse.bundles.map((thisBundle) => {
return (
<Box component="th" key={`th-${thisBundle.id}-${keyComplement}`}>
{thisBundle.name}
</Box>
);
})}
</tr>
</thead>
<tbody>
{whichCourse.schools.map((thisSchool, currentIndex) => {
return (
<tr key={`td-${thisSchool.id}-${keyComplement}`}>
<Box component="td">{thisSchool.id}</Box>
<Box component="td">{thisSchool.name}</Box>
{whichCourse.bundles.map((thisBundle) => {
const isEnabled = thisSchool.licensedproducts.includes(thisBundle.id);
return (
<Box component="td" key={`td-${thisBundle.id}-${keyComplement}`}>
{editMode ? (
<Checkbox
size="small"
checked={isEnabled}
sx={{
color: "#000000",
"&.Mui-checked": {
color: "#3F51B5",
},
}}
onChange={() =>
setModifiedValues((currentValue) => {
if (isEnabled) {
currentValue.schools[currentIndex].licensedproducts = currentValue.schools[currentIndex].licensedproducts.filter((value) => value !== thisBundle.id);
} else {
currentValue.schools[currentIndex].licensedproducts.push(thisBundle.id);
}
return { ...currentValue };
})
}
/>
) : (
isEnabled && <CheckIcon sx={{ verticalAlign: "middle" }} />
)}
</Box>
);
})}
</tr>
);
})}
</tbody>
</Box>
</Box>
</Box>
);
}
I created a very simple repo with this code and a CloudFlare Pages deploy
- GitHub repo: https://github.com/LuisEnMarroquin/table-toggles-discard
- Live preview: https://toggle.pages.dev/
CodePudding user response:
Problem with your code is this:
const [originalValues, setOriginalValues] = useState({ ...propsData });
const [modifiedValues, setModifiedValues] = useState({ ...propsData });
You just spread same object in two different states, by spread you shallowly copied only top level of object, but all other nested things are shared between originalvalues
and modifiedvalues
since those nested references were not copied.
And like that each time you check unchecked checkbox, like this:
else {
currentValue.schools[currentIndex].licensedproducts.push(thisBundle.id);
}
You are basically modifying both original and modified values, since you just pushed in that same licencesedproducts
array that was shared because you were not copied deeply when setting initial state.
You should either do deep copy on initial state set(which I suggest), or/and in this part where you modify state to use spread of array in order to create new one, eg: currentValue.schools[currentIndex].licensedproducts = [...currentValue.schools[currentIndex].licensedproducts, thisBundle.id]
CodePudding user response:
It looks like you need to apply another logic to set isEnabled
. I do not know
your business logic, however the most simple solution will be to use !
operator:
const isEnabled = !thisSchool.licensedproducts.includes(thisBundle.id);