how to display menu items dynamically in react js - reactjs

Here is the menu UI
Trying to load menu and menu item dynamically from backend through redux state.not able render menu items for specific menu.
json Data structre:
menuContext:{
Define:[{id:1,label:"user"},{id:2,label:"test"}]
Manage:[{id:1,label:"test2"},{id:2,label:"test3"}]
}
function MenuContext() {
const [anchorEl, setAnchorEl] = React.useState(null);
const open = Boolean(anchorEl);
const theme = useTheme();
const colors = tokens(theme.palette.mode);
const handleClick = (event) => {
console.log(event.currentTarget)
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const { viewService, isError, message } = useSelector(state => state.auth)
const [menu, setMenu] = useState([])
const [items, setItems] = useState([])
const dispatch = useDispatch();
useEffect(() => {
return () => {
console.log("cleaned up");
dispatch(resetMenuContext())
setMenu([])
};
}, []);
useEffect(() => {
if (viewService !== undefined) {
if (viewService?.menuContext !== undefined) {
const { menuContext } = viewService && viewService
setMenu(menuContext)
}
}
}, [viewService])
return (
<>
{
Object.entries(menu).map(([key, value]) => (
<>
<Button
id={`button-${key}`}
aria-controls={open ? `menu-${key}` : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
variant="contained"
disableElevation
onClick={handleClick}
endIcon={<KeyboardArrowDownIcon />}
disableRipple
style={{ textTransform: 'none' }}
>
{i18next.t(`label.${key}`)}
</Button>
<StyledMenu
id={`menu-${key}`}
MenuListProps={{
'aria-labelledby': `button-${key}`,
}}
anchorEl={anchorEl}
open={open}
onClose={handleClose}
key={key}
>
{
Object.entries(value).map(({ id }) => (
<MenuItem>
{id}
</MenuItem>
))
}
</StyledMenu>
</>
))
}
</>
)
}
what changes i need to make, to display menu i items for each menu.

Value seems to be an array
{
value?.map({ id, label }) => (
<MenuItem key={id}>
{label}
</MenuItem>
))
}

Related

Warning: Failed prop type: The prop `onSubmit` is marked as required in `ProjectWizardForm`, but its value is `undefined`

This error is shown in the browser console when Dialog (modal) is open:
Warning: Failed prop type: The prop onSubmit is marked as required
in ProjectWizardForm, but its value is undefined.
Code:
const UstHomeToolbar = ({ destroyProjectWizardForm, handleOpenAddPattern }) => {
const [open, setOpen] = useState(false);
const [openUploadForm, setOpenUploadForm] = useState(false);
const [projectWizardModalTitle, setProjectWizardModalTitle] = useState("");
const classes = useStyles();
const [selectedFile, setSelectedFile] = useState();
const [isFilePicked, setIsFilePicked] = useState(false);
const [statusText, setStatusText] = useState({
type: null,
msg: null,
});
const [userData, setUserData] = useState({});
useEffect(() => {
const userDataForPermissions = localStorage.getItem(LOGGED_IN_USER);
setUserData(JSON.parse(userDataForPermissions));
}, []);
const handleProjectDialogClickOpen = () => {
setOpen(true);
};
const handleProjectDialogClose = async () => {
setOpen(false);
};
const changeUploadHandler = (event) => {
setSelectedFile(event.target.files[0]);
setIsFilePicked(true);
};
const clearUploadHandler = (event) => {
setSelectedFile(null);
setIsFilePicked(false);
};
const handleUploadSubmission = () => {
const formData = new FormData();
formData.append("excel", selectedFile);
axios
.post("/project/excel", formData, {
headers: { "Content-Type": "multipart/form-data" },
})
.then((res) => {
clearUploadHandler();
setStatusText({
type: "success",
msg: "Excel file is uploaded successfully and taking you to the design....",
});
const design = res.data;
window.location.href = `/project/${design.project_id}/design/${design.id}`;
})
.catch((err) => {
console.error("Error:", err?.response?.data?.message);
clearUploadHandler();
setStatusText({
type: "error",
msg: err?.response?.data?.message,
});
});
};
const handleProjectDialogExited = () => {
destroyProjectWizardForm();
};
const handleProjectWizardModalTitle = (newModalTitle) =>
setProjectWizardModalTitle(newModalTitle);
return (
<Toolbar className={classes.toolbarHome}>
<Grid container justify="space-between">
<Grid item>
<Box className={classes.projectTitleStyle}>Projects</Box>
<Box className={classes.projectTitleDetailsStyle}>
List of ongoing and finished projects
</Box>
</Grid>
<Grid item>
<div className={classes.addProjectButtonStyle}>
<IxButton
onClick={() => setOpenUploadForm(true)}
handleClose={() => setOpenUploadForm(false)}
customClassName={TOOLBAR_BUTTON}
text="UPLOAD EXCEL"
inline={true}
disabled={!getPermission(userData, PROJECT).can_edit}
/>
<IxButton
onClick={handleProjectDialogClickOpen}
customClassName={TOOLBAR_BUTTON}
text="ADD PROJECT"
inline={true}
disabled={!getPermission(userData, PROJECT).can_edit}
/>
</div>
</Grid>
</Grid>
<IxDialog
handleClose={() => setOpenUploadForm(false)}
open={openUploadForm}
modalTitle={"Upload Design Excel"}
>
<div style={{ minHeight: "50px" }}>
Select an excel (*.xls) file:
<input
type="file"
onChange={changeUploadHandler}
accept=".xls"
style={{ marginLeft: "10px" }}
/>
</div>
<div style={{ marginBottom: "5px" }}>
{statusText.type && (
<Alert className={classes.statusText} severity={statusText.type}>
{statusText.msg}{" "}
</Alert>
)}
</div>
<div>
<Button
variant="contained"
component="label"
onClick={handleUploadSubmission}
disabled={!isFilePicked}
>
Upload File
</Button>
</div>
</IxDialog>
<IxDialog
handleClose={handleProjectDialogClose}
onExited={handleProjectDialogExited}
open={open}
modalTitle={projectWizardModalTitle}
>
<ProjectWizardForm
handleOpenAddPattern={handleOpenAddPattern}
handleClose={handleProjectDialogClose}
handleProjectWizardModalTitle={handleProjectWizardModalTitle}
/>
</IxDialog>
</Toolbar>
);
};
export default UstHomeToolbar;
ProjectWizardForm component:
const ProjectWizardForm = props => {
const [page, setPage] = useState(1);
const [touchedOnLoad, setTouchedOnLoad] = useState(false);
const history = useHistory();
const {
handleClose,
dispatch,
handleOpenAddPattern,
handleProjectWizardModalTitle
} = props;
useEffect(() => {
switch (page) {
case 1:
handleProjectWizardModalTitle('Add project');
break;
case 2:
handleProjectWizardModalTitle('Add target specs');
break;
case 3:
handleProjectWizardModalTitle('Add version details');
break;
default:
return;
}
}, [page, handleProjectWizardModalTitle]);
const saveData = values => {
const { submitButtonType } = values;
var errors = ProjectValidate(values);
if (Object.entries(errors).length > 0) {
if (errors?.number || errors?.drawn_date) {
setTouchedOnLoad(true);
setPage(1);
return;
} else if (errors.mandrel_id) {
setTouchedOnLoad(true);
setPage(3);
return;
}
}
const openAddPatternForm = newProject => {
const openAddPatternForm = true;
handleOpenAddPattern(openAddPatternForm, newProject);
};
dispatch(
addDesign(
formatProjectWizardSubmittedValue(values),
(err, newProject) => {
if (!err) {
dispatch(fetchProjects());
submitButtonType === ADD_PATTERN_NEXT_SUBMIT_BUTTON_TYPE
? openAddPatternForm(newProject)
: history.push(
`/project/${newProject.project_id}/details/${newProject.id}`
);
} else {
dispatch(setError(getFormatError(err)));
}
}
)
);
handleClose();
};
return (
<div>
{page === 1 && (
<UstProjectDetailForm
getDialogActionButtons={getAddProjectDetailsDialogActionButtons}
{...props}
touchedOnLoad={touchedOnLoad}
onSubmit={saveData}
nextPage={() => setPage(page + 1)}
/>
)}
{page === 2 && (
<UstProjectTargetSpecForm
{...props}
getDialogActionButtons={getAddTargetSpecDialogActionButtons}
onSubmit={saveData}
previousPage={() => setPage(page - 1)}
nextPage={() => setPage(page + 1)}
/>
)}
{page === 3 && (
<UstDesignDetailForm
{...props}
getDialogActionButtons={getAddDesignDetailsDialogActionButtons}
touchedOnLoad={touchedOnLoad}
onSubmit={saveData}
previousPage={() => setPage(page - 1)}
nextPage={() => setPage(page + 1)}
/>
)}
</div>
);
};
ProjectWizardForm.propTypes = {
onSubmit: PropTypes.func.isRequired
};
How can I fix this issue?

Update Child state from Parent using Context in React

I have a few buttons and "view all" button. The individual buttons load the coresponding data of that index or will show all the data by clicking the "view all" button. Problem I am running into is when I click my "view all" button in the parent it's not updating the state in the child component. On mounting it works as normal but on event handler in the "view all" it doesn't update. Any thoughts on where I am going wrong here?
JS:
...
const Context = createContext(false);
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
"& > *": {
margin: theme.spacing(1)
}
},
orange: {
color: theme.palette.getContrastText(deepOrange[500]),
backgroundColor: deepOrange[500],
border: "4px solid black"
},
info: {
margin: "10px"
},
wrapper: {
display: "flex"
},
contentWrapper: {
display: "flex",
flexDirection: "column"
},
elWrapper: {
opacity: 0,
"&.active": {
opacity: 1
}
}
}));
const ToggleItem = ({ id, styles, discription }) => {
const { activeViewAll, handleChange } = useContext(Context);
const [toggleThisButton, setToggleThisButton] = useState();
const handleClick = () => {
setToggleThisButton((prev) => !prev);
handleChange(discription, !toggleThisButton);
};
return (
<>
<Avatar
className={toggleThisButton && !activeViewAll ? styles.orange : ""}
onClick={handleClick}
>
{id}
</Avatar>
<p>{JSON.stringify(toggleThisButton)}</p>
</>
);
};
const ToggleContainer = ({ className, selected }) => {
return (
<div className={className}>
{selected.map((item, idx) => (
<div key={idx}>Content {item}</div>
))}
</div>
);
};
export default function App() {
const data = ["first", "second", "third"];
const classes = useStyles();
const [selected, setSelected] = useState([]);
const [activeViewAll, setActiveViewAll] = useState(false);
useEffect(() => {
setActiveViewAll(true);
setSelected([...data]);
}, []);
const handleChange = (val, action) => {
let newVal = [];
if (activeViewAll) {
selected.splice(0, 3);
setActiveViewAll(false);
}
if (action) {
newVal = [...selected, val];
} else {
// If toggle off, then remove content from selected state
newVal = selected.filter((v) => v !== val);
}
console.log("action", action);
setSelected(newVal);
};
const handleViewAll = () => {
console.log("all clicked");
setActiveViewAll(true);
setSelected([...data]);
};
return (
<Context.Provider value={{ activeViewAll, handleChange }}>
<div className={classes.wrapper}>
<Avatar
className={activeViewAll ? classes.orange : null}
onClick={handleViewAll}
>
<span style={{ fontSize: "0.75rem", textAlign: "center" }}>
View All
</span>
</Avatar>
{data.map((d, id) => {
return (
<div key={id}>
<ToggleItem id={id} styles={classes} discription={d} />
</div>
);
})}
</div>
<div className={classes.contentWrapper}>
<ToggleContainer styles={classes} selected={selected} />
</div>
</Context.Provider>
);
}
Codesanbox:
https://codesandbox.io/s/72166087-forked-jvn59i?file=/src/App.js:260-3117
Issue
The issue seems to be that you are mixing up the management of the boolean activeViewAll state with the selected state.
Solution
When activeViewAll is true, pass the data array as the selected prop value to the ToggleContainer component, otherwise pass what is actually selected, the selected state.
Simplify the handlers. The handleViewAll callback only toggles the view all state to true, and the handleChange callback toggles the view all state back to false and selects/deselects the data item.
function App() {
const data = ["first", "second", "third"];
const classes = useStyles();
const [selected, setSelected] = useState([]); // none selected b/c view all true
const [activeViewAll, setActiveViewAll] = useState(true); // initially view all
const handleChange = (val, action) => {
setActiveViewAll(false); // deselect view all
setSelected(selected => {
if (action) {
return [...selected, val];
} else {
return selected.filter(v => v !== val)
}
});
};
const handleViewAll = () => {
setActiveViewAll(true); // select view all
};
return (
<Context.Provider value={{ activeViewAll, handleChange }}>
<div className={classes.wrapper}>
<Avatar
className={activeViewAll ? classes.orange : null}
onClick={handleViewAll}
>
<span style={{ fontSize: "0.75rem", textAlign: "center" }}>
View All
</span>
</Avatar>
{data.map((d, id) => {
return (
<div key={id}>
<ToggleItem id={id} styles={classes} discription={d} />
</div>
);
})}
</div>
<div className={classes.contentWrapper}>
<ToggleContainer
styles={classes}
selected={activeViewAll ? data : selected} // pass all data, or selected only
/>
</div>
</Context.Provider>
);
}
In the ToggleContainer don't use the array index as the React key since you are mutating the array. Use the element value since they are unique and changing the order/index doesn't affect the value.
const ToggleContainer = ({ className, selected }) => {
return (
<div className={className}>
{selected.map((item) => (
<div key={item}>Content {item}</div>
))}
</div>
);
};
Update
Since it is now understood that you want to not remember what was previously selected before toggling activeViewAll then when toggling true clear the selected state array. Instead of duplicating the selected state in the children components, pass the selected array in the context and computed a derived isSelected state. This maintains a single source of truth for what is selected and removes the need to "synchronize" state between components.
const ToggleItem = ({ id, styles, description }) => {
const { handleChange, selected } = useContext(Context);
const isSelected = selected.includes(description);
const handleClick = () => {
handleChange(description);
};
return (
<>
<Avatar
className={isSelected ? styles.orange : ""}
onClick={handleClick}
>
{id}
</Avatar>
<p>{JSON.stringify(isSelected)}</p>
</>
);
};
const ToggleContainer = ({ className, selected }) => {
return (
<div className={className}>
{selected.map((item) => (
<div key={item}>Content {item}</div>
))}
</div>
);
};
Update the handleChange component to take only the selected value and determine if it needs to add/remove the value.
export default function App() {
const data = ["first", "second", "third"];
const classes = useStyles();
const [selected, setSelected] = useState([]);
const [activeViewAll, setActiveViewAll] = useState(true);
const handleChange = (val) => {
setActiveViewAll(false);
setSelected((selected) => {
if (selected.includes(val)) {
return selected.filter((v) => v !== val);
} else {
return [...selected, val];
}
});
};
const handleViewAll = () => {
setActiveViewAll(true);
setSelected([]);
};
return (
<Context.Provider value={{ activeViewAll, handleChange, selected }}>
<div className={classes.wrapper}>
<Avatar
className={activeViewAll ? classes.orange : null}
onClick={handleViewAll}
>
<span style={{ fontSize: "0.75rem", textAlign: "center" }}>
View All
</span>
</Avatar>
{data.map((d, id) => {
return (
<div key={d}>
<ToggleItem id={id} styles={classes} description={d} />
</div>
);
})}
</div>
<div className={classes.contentWrapper}>
<ToggleContainer
styles={classes}
selected={activeViewAll ? data : selected}
/>
</div>
</Context.Provider>
);
}

ReactJS Redux toolkit Component does not update, only after refreshing the page

Im using Redux-toolkit (already using it for a while). When I reset my redux store, and load my website for the first time the SharedList component does not update if I update the store.
But the strange thing... after refreshing the page, everything works perfect!
Im using the [...array] and Object.assign({}) methods. Also console logged, and the objects are NOT the same.
this is the reducer: (Only updates the ReactJS component after I refresh my page)
CREATE_SHARED_LIST_ITEM_LOCAL: (
proxyState,
action: PayloadAction<{
active: boolean;
checked: boolean;
id: string;
text: string;
}>
) => {
const state = current(proxyState);
const [groupIndex, group] = getCurrentGroupIndex(state);
const { id, text, active, checked } = action.payload;
const listItemIndex = group.shared_list.findIndex((item) => {
return item.id === id;
});
const group_id =
proxyState.groups[groupIndex].shared_list[listItemIndex].group_id;
const sharedItem: SharedListItem = {
text,
id,
checked,
active,
group_id,
};
proxyState.groups[groupIndex].shared_list[listItemIndex] = Object.assign(
{},
sharedItem
);
},
This is the ReactJS component
const GroceryList: React.FC = () => {
const [update, setUpdate] = useState(false);
const handleUpdate = () => {};
const dispatch = useDispatch();
const listItems = useSelector(selectSharedList);
const { setTitle } = useContext(HeaderContext);
const editItem = async (
id: string,
checked: boolean,
text: string,
active: boolean
) => {
dispatch(MUTATE_SHARED_LIST_LOCAL({ id, text, active, checked }));
};
const fetchList = async () => {
await dispatch(QUERY_SHARED_LIST({}));
};
useEffect(() => {
setTitle("Lijst");
fetchList();
}, []);
const list = listItems.filter((item) => {
return item.active;
});
return (
<>
<Card
// style={{
// paddingBottom: 56,
// }}
>
<CardContent>
<List>
{list.length === 0 ? (
<Box textAlign="center" justifyContent="center">
{/* <Center> */}
<Box>
<EmptyCartIcon style={{ width: "45%" }} />
</Box>
{/* </Center> */}
<Typography>De gedeelte lijst is leeg</Typography>
</Box>
) : null}
{list.map((item, index) => {
const { checked, text, id, active } = item;
return (
<ListItem dense disablePadding key={"item-" + index}>
<Checkbox
checked={checked}
onClick={() => {
editItem(id, !checked, text, active);
}}
/>
<EditTextSharedListItem item={item} />
<Box>
<IconButton
onClick={() => {
editItem(id, checked, text, false);
}}
>
<DeleteOutlineRoundedIcon />
</IconButton>
</Box>
</ListItem>
);
})}
</List>
</CardContent>
</Card>
<AddSharedListItem />
</>
);
};
This is the selector
export const selectSharedList = (state: RootState) => {
const group = selectCurrentGroup(state);
return group.shared_list.filter((item) => {
return item.active;
});
};

Get the Category ID of a button in React

I'm trying to get the value of the category ID on a click of a button but it's getting all the id's of my category when I click one of the buttons.
const CategoryPage = ({ categories }) => {
const classes = useStyles()
const [click, setClick] = useState()
const handleClick = (e) => {
e.preventDefault()
const id = categories.map((category) => category._id)
console.log(id)
}
return (
<div className={classes.scrollMenu}>
{categories.map((category) => {
return (
<Button
key={category._id}
className={classes.button}
onClick={(e) => handleClick(e)}
>
{category.name}
</Button>
)
})}
</div>
)
}
You can pass the category.id as the argument to the caller function.
const CategoryPage = ({ categories }) => {
const classes = useStyles()
const [click, setClick] = useState()
const handleClick = (categoryId) => { // clicked category id
e.preventDefault()
console.log(categoryId)
}
return (
<div className={classes.scrollMenu}>
{categories.map((category) => {
return (
<Button
key={category._id}
className={classes.button}
onClick={() => handleClick(category._id)} // this way
>
{category.name}
</Button>
)
})}
</div>
)
}

map multiple Card components with Menu

I have an array of items and for each item, I want to display a Card component. Each Card has a pop up Menu. I am having trouble opening just the specific clicked Menu to open. My code opens all Menus together. Here is the code snippet.
Second issue is that I get a warning about not being able to having a button within a button. I make the Card Header clickable, and then I have the Menu. What's the correct way to implement this in order to avoid the warning?
const [anchorEl, setAnchorEl] = useState(null)
const handleMenuClick = (e) => {
e.stopPropagation()
setAnchorEl(e.currentTarget)
}
return (
{
props.items.map( (k, i) => (
<Card className={classes.root}>
<CardActionArea onClick={(e) => handleRedirect(e)}>
<MyMenu key={i} index={i} anchor={anchorEl} />
<CardHeader
action={
<IconButton id={i} aria-label="settings" onClick={handleMenuClick}>
<MoreVertIcon />
</IconButton>
}
title={k.title}
subheader={getTimestamp(k._id)}
/>
</CardActionArea>
MyMenu code:
const MyMenu = ( { index, anchor } ) => {
const [anchorEl, setAnchorEl] = useState({})
useEffect(() => {
//setAnchorEl({[e.target.id]: anchor})
if (anchor!==null) {
if (index===anchor.id)
setAnchorEl({[index]: anchor})
}
}, [anchor, index])
const handleRedirect = (e) => {
e.stopPropagation()
//history.push('/item/'+ id)
}
const handleClose = (e) => {
e.stopPropagation()
setAnchorEl({[e.target.id]: null})
};
return (
<Menu
id={index}
anchorEl={anchorEl[index]}
open={Boolean(anchorEl[index])}
onClose={handleClose}
>
<MenuItem onClick={(e) => handleRedirect(e)}>Read</MenuItem>
<MenuItem onClick={(e) => handleRedirect(e)}>Edit</MenuItem>
</Menu>
)
}
You may try below.
const handleMenuClick = (e, setter) => {
e.stopPropagation()
setter(e.currentTarget)
}
return (
{
props.items.map( (k, i) => {
const [anchorEl, setAnchorEl] = useState(null)
return (
<Card className={classes.root}>
<CardActionArea onClick={(e) => handleRedirect(e)}>
<MyMenu key={i} index={i} anchor={anchorEl} />
<CardHeader
action={
<IconButton id={i} aria-label="settings" onClick={(e) => handleMenuClick(e, setAnchorEl)}>
<MoreVertIcon />
</IconButton>
}
title={k.title}
subheader={getTimestamp(k._id)}
/>
</CardActionArea>
</Card>
)
Basically in above, you are creating separate state for each mapped object. And I tweaked the click event to accept a setState callback.
Hope it helps.
Thanks #Sandy. I was able to resolve this by moving this code to the parent component
const [anchorEl, setAnchorEl] = useState({})
const handleMenuClick = (e) => {
e.stopPropagation()
setAnchorEl({[e.currentTarget.id]: e.currentTarget})
}
...
<IconButton id={i} onClick={handleMenuClick}>

Resources