I have an onClick event listener on a Menu Item in material UI, MUI.
When I load the page, the onclick calls itself automatically. How can I fix this?
const [anchorEl, setAnchorEl] = useState(null)
const open = Boolean(anchorEl)
const handleClick = (event) => {
setAnchorEl(event.currentTarget)
}
const handleClose = () => {
setAnchorEl(null)
}
const handleDelete = async (postId, token) => {
setLoading(true)
const deleted = await deletePost(postId, token)
if (deleted) {
setLoading(false)
toast.success(`Post deleted successfully`)
history.push('/feed')
handleClose()
} else {
toast.error('Something went wrong')
setLoading(false)
handleClose()
}
}
<IconButton
id='demo-positioned-button'
aria-controls={open ? 'demo-positioned-menu' : undefined}
aria-haspopup='true'
aria-expanded={open ? 'true' : undefined}
onClick={handleClick}
>
<MoreVertIcon sx={{ color: '#E4E4E4' }} />
</IconButton>
<Menu
id='demo-positioned-menu'
aria-labelledby='demo-positioned-button'
anchorEl={anchorEl}
open={open}
onClose={handleClose}
anchorOrigin={{
vertical: 'top',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<MenuItem onClick={handleDelete(post._id, auth.token)}>
<ListItemIcon>
<DeleteIcon fontSize='small' />
</ListItemIcon>
<ListItemText>Delete</ListItemText>
</MenuItem>
</Menu>
When I load the page, the delete is called automatially and it repeats continuously, (Infinite loop) I have to reload the page.
I tried with simple window.alert('Hello')
It also alerts infinitely until i reload the page.
It's happening because you are calling the handleDelete directly.
<MenuItem onClick={() => handleDelete(post._id, auth.token)}>
<ListItemIcon>
<DeleteIcon fontSize='small' />
</ListItemIcon>
<ListItemText>Delete</ListItemText>
</MenuItem>
Replace handleDelete(post._id, auth.token) with () => handleDelete(post._id, auth.token)
You have to call the handleDelete in an arrow function:
onClick={() => handleDelete(id)}
Related
I am doing a project and I want to show a modal window to compulsorily accept some Cookies.
I need that cookie window to be launched automatically, without pressing any button, when the application starts, that is, before the dashboard is loaded.
This is my code:
ModalCookie.js
imports ...
const style = {
position: 'absolute',
top: '50%',
left: '50%',
};
export default function CookiesModal() {
const handleOpen = () => setOpen(true);
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setCookiesPreview(false);
setOpen(false);
};
const handleNotClose = (event, reason) => {
if (reason && reason == "backdropClick")
return;
}
const [value, setValue] = React.useState('1');
const handleChange = (event, newValue) => {
setValue(newValue);
};
return (
<ThemeProvider theme={theme}>
<div>
<Fade in={open}>
<Box>
<BootstrapDialog
onClose={handleNotClose}
aria-labelledby="customized-dialog-title"
open={open}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500,
}}
>
<BootstrapDialogTitle id="customized-dialog-title" onClose={handleClose}>
</BootstrapDialogTitle>
<TabContext value={value}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<TabList onChange={handleChange} aria-label="lab API tabs example">
<Tab
label="AAAAA"
value="1" />
</TabList>
</Box>
<TabPanel value="1">
<p>...</p>
</TabPanel>
</TabContext>
<Button
onClick={handleClose}
variant="contained"
color="secondary"
>
Aceptar
</Button>
</BootstrapDialog>
</Box>
</Fade>
</div>
</ThemeProvider>
);
}
Config.js
// COOKIES
export const setCookiesPreview = ( state ) => {
localStorage.setItem('cookiesPreview', state)
}
// --
export const isCookiesPreview = () => {
const active=localStorage.getItem('cookiesPreview') ? localStorage.getItem('cookiesPreview') : true;
setCookiesPreview(false);
return(
active
);
}
And my dashboard:
imports...
// MAIN
export const Dashboard = () => {
const state = useSelector( state => state);
console.log('isCookiesPreview='+isCookiesPreview());
if(isCookiesPreview()){
console.log('CookiesPreview ON ------------------------------------------------')
setTimeout(() => {
CookiesModal.handleOpen();
}, 15000);
}
return (
<>
<ThemeProvider theme={theme}>
<div>
<HeaderBar/>
{(alertMessage != null) && (alertMessage.length>0) ? <Alert severity={alertSeverity}>{alertMessage}</Alert> : <></>}
<Grid container item={true} xs={12}>
<BodyGrid/>
</Grid>
<Grid container item={true} xs={12} pt={4}>
<BottomBar/>
</Grid>
</div>
</ThemeProvider>
</>
)
I am trying to use the handleOpen() constant from ModalCookie.js to open the window from Dashboard.js and save the locale that those cookies have been accepted so as not to show it the following times
I can't get the window to show up, but it does show me the logs I've put on the Dashboard related to cookies.
It tells me that HandleOpen is not a function.
So I've created a get request, which shows all the exercises and the options to edit (which will redirect to another url) and delete. I wanted to use material ui's Dialog, so there will be a confirmation before deleting. The problem is that, say I have 3 exercises and want to delete exercise with index 1, when click the on delete button it appears the last index in the element i.e the last exercise, although I want to delete the second. How can I fix that?
And also can I move Dialog in another file for more clear coding?
Exercises.js
const Exercises = () => {
const { categories, deletedCategories, error, isLoading, exercises, open, handleClickOpen, handleClose, deleteExercise } = useFormExercises();
const [showCategories, setShowCategories] = useState(false);
if (isLoading) {
return <div>Loading...</div>
}
if (error) {
return <div>There was an error: {error}</div>
}
return (
<Grid container className='content' >
<Card className='exerciseCard'>
<h1>Exercises</h1>
<FormControlLabel sx={{ width:'500px'}}
label="Show Categories"
control={
<Checkbox
sx={{ml:2}}
checked={showCategories}
onChange={(event) => setShowCategories(event.target.checked)}
/>
}
/>
<Button sx={{pr:6}} variant='fab' href={('/exercises/add')}>
<Add />
</Button>
<Divider />
<Grid className="exerciseContent" >
{exercises.map(exercise => (
<Card>
<tr key={exercise.id}>
<List>
<ListItem
key={exercise.id}
secondaryAction={
<IconButton onClick={handleClickOpen}>
<Delete />
</IconButton>
}>
<Button
sx={{mr:4}}
href={(`/exercises/edit/${exercise.id}`)} >
<Edit />
</Button>
<ListItemText sx={{justifyContent:'center', width:'400px'}}
primary={exercise.exercise_name}
secondary={showCategories ? exercise["exerciseCategories.category_name"] : null}
/>
</ListItem>
<Dialog
open={open}
onClose={handleClose}
style={{ borderColor: 'red' }}
>
<Box sx={{ borderTop: 3, color: 'red' }}>
<DialogTitle sx={{ color: 'black', backgroundColor: 'gainsboro', pl: 11 }}>
Delete Exercise
</DialogTitle>
<DialogContent>
<DialogContentText color='black' >
<Warning fontSize='large' color='error' id='warning' />
Are you sure you want to delete the exercise: {exercise.exercise_name} ?
</DialogContentText>
</DialogContent>
<DialogActions >
<Button variant='contained' onClick={() => deleteExercise(exercise.id)} autoFocus>
Yes
</Button>
<Button variant='outlined' onClick={handleClose}>No</Button>
</DialogActions>
</Box>
</Dialog>
</List>
</tr>
</Card>
))}
</Grid>
</Card>
</Grid>
)
}
export default Exercises
My delete function
useFormExercises.js
...
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
}
const deleteExercise = async (id) => {
try {
await fetch(`${BASE_URL}/exercises/${id}`, {
method: "DELETE",
}).then(response => {
setExercises(exercises.filter(exercise => exercise.id !== id))
return response.json()
})
} catch (error) {
console.log(error)
}
}
And if another thing seems of, please share your opinion!
Thanks in advance!
I have a dynamically generated set of dropdowns and accordions that populate at render client-side (validated user purchases from db).
I'm running into an error that I'm sure comes from my menu anchorEl not knowing 'which' menu to open using anchorEl. The MUI documentation doesn't really cover multiple dynamic menus, so I'm unsure of how to manage which menu is open
Here is a pic that illustrates my use-case:
As you can see, the menu that gets anchored is actually the last rendered element. Every download button shows the last rendered menu. I've done research and I think I've whittled it down to the anchorEl and open props.
Here is my code. Keep in mind, the data structure is working as intended, so I've omitted it to keep it brief, and because it's coming from firebase, I'd have to completely recreate it here (and I think it's redundant).
The component:
import { useAuth } from '../contexts/AuthContext'
import { Accordion, AccordionSummary, AccordionDetails, Button, ButtonGroup, CircularProgress, ClickAwayListener, Grid, Menu, MenuItem, Typography } from '#material-ui/core'
import { ExpandMore as ExpandMoreIcon } from '#material-ui/icons'
import LoginForm from '../components/LoginForm'
import { motion } from 'framer-motion'
import { useEffect, useState } from 'react'
import { db, functions } from '../firebase'
import styles from '../styles/Account.module.scss'
export default function Account() {
const { currentUser } = useAuth()
const [userPurchases, setUserPurchases] = useState([])
const [anchorEl, setAnchorEl] = useState(null)
const [generatingURL, setGeneratingURL] = useState(false)
function openDownloads(e) {
setAnchorEl(prevState => (e.currentTarget))
}
function handleClose(e) {
setAnchorEl(prevState => null)
}
function generateLink(prefix, variationChoice, pack) {
console.log("pack from generate func", pack)
setGeneratingURL(true)
const variation = variationChoice ? `${variationChoice}/` : ''
console.log('link: ', `edit-elements/${prefix}/${variation}${pack}.zip`)
setGeneratingURL(false)
return
if (pack.downloads_remaining === 0) {
console.error("No more Downloads remaining")
setGeneratingURL(false)
handleClose()
return
}
handleClose()
const genLink = functions.httpsCallable('generatePresignedURL')
genLink({
fileName: pack,
variation: variation,
prefix: prefix
})
.then(res => {
console.log(JSON.stringify(res))
setGeneratingURL(false)
})
.catch(err => {
console.log(JSON.stringify(err))
setGeneratingURL(false)
})
}
useEffect(() => {
if (currentUser !== null) {
const fetchData = async () => {
// Grab user products_owned from customers collection for user UID
const results = await db.collection('customers').doc(currentUser.uid).get()
.then((response) => {
return response.data().products_owned
})
.catch(err => console.log(err))
Object.entries(results).map(([product, fields]) => {
// Grabbing each product document to get meta (title, prefix, image location, etc [so it's always current])
const productDoc = db.collection('products').doc(product).get()
.then(doc => {
const data = doc.data()
const productMeta = {
uid: product,
title: data.title,
main_image: data.main_image,
product_prefix: data.product_prefix,
variations: data.variations
}
// This is where we merge the meta with the customer purchase data for each product
setUserPurchases({
...userPurchases,
[product]: {
...fields,
...productMeta
}
})
})
.catch(err => {
console.error('Error retrieving purchases. Please refresh page to try again. Full error: ', JSON.stringify(err))
})
})
}
return fetchData()
}
}, [currentUser])
if (userPurchases.length === 0) {
return (
<CircularProgress />
)
}
return(
currentUser !== null && userPurchases !== null ?
<>
<p>Welcome, { currentUser.displayName || currentUser.email }!</p>
<Typography variant="h3" style={{marginBottom: '1em'}}>Purchased Products:</Typography>
{ userPurchases && Object.values(userPurchases).map((product) => {
const purchase_date = new Date(product.purchase_date.seconds * 1000).toLocaleDateString()
return (
<motion.div key={product.uid}>
<Accordion style={{backgroundColor: '#efefef'}}>
<AccordionSummary expandIcon={<ExpandMoreIcon style={{fontSize: "calc(2vw + 10px)"}}/>} aria-controls={`${product.title} accordion panel`}>
<Grid container direction="row" alignItems="center">
<Grid item xs={3}><img src={product.main_image} style={{ height: '100%', maxHeight: "200px", width: '100%', maxWidth: '150px' }}/></Grid>
<Grid item xs={6}><Typography variant="h6">{product.title}</Typography></Grid>
<Grid item xs={3}><Typography variant="body2"><b>Purchase Date:</b><br />{purchase_date}</Typography></Grid>
</Grid>
</AccordionSummary>
<AccordionDetails style={{backgroundColor: "#e5e5e5", borderTop: 'solid 6px #5e5e5e', padding: '0px'}}>
<Grid container direction="column" className={styles[`product-grid`]}>
{Object.entries(product.packs).map(([pack, downloads]) => {
// The pack object right now
return (
<Grid key={ `${pack}-container` } container direction="row" alignItems="center" justify="space-between" style={{padding: '2em 1em'}}>
<Grid item xs={4} style={{ textTransform: 'uppercase', backgroundColor: 'transparent' }}><Typography align="left" variant="subtitle2" style={{fontSize: 'calc(.5vw + 10px)'}}>{pack}</Typography></Grid>
<Grid item xs={4} style={{ backgroundColor: 'transparent' }}><Typography variant="subtitle2" style={{fontSize: "calc(.4vw + 10px)"}}>{`Remaining: ${downloads.downloads_remaining}`}</Typography></Grid>
<Grid item xs={4} style={{ backgroundColor: 'transparent' }}>
<ButtonGroup variant="contained" fullWidth >
<Button id={`${pack}-btn`} disabled={generatingURL} onClick={openDownloads} color='primary'>
<Typography variant="button" style={{fontSize: "calc(.4vw + 10px)"}} >{!generatingURL ? 'Downloads' : 'Processing'}</Typography>
</Button>
</ButtonGroup>
<ClickAwayListener key={`${product.product_prefix}-${pack}`} mouseEvent='onMouseDown' onClickAway={handleClose}>
<Menu anchorOrigin={{ vertical: 'top', horizontal: 'right' }} transformOrigin={{ vertical: 'top', horizontal: 'right' }} id={`${product}-variations`} open={Boolean(anchorEl)} anchorEl={anchorEl}>
{product.variations && <MenuItem onClick={() => generateLink(product.product_prefix, null, pack) }>{`Pack - ${pack}`}</MenuItem>}
{product.variations && Object.entries(product.variations).map(([variation, link]) => {
return (
<MenuItem key={`${product.product_prefix}-${variation}-${pack}`} onClick={() => generateLink(product.product_prefix, link, pack)}>{ variation }</MenuItem>
)
})}
</Menu>
</ClickAwayListener>
</Grid>
</Grid>
)}
)}
</Grid>
</AccordionDetails>
</Accordion>
</motion.div>
)
})
}
</>
:
<>
<p>No user Signed in</p>
<LoginForm />
</>
)
}
I think it also bears mentioning that I did check the rendered HTML, and the correct lists are there in order - It's just the last one assuming the state. Thanks in advance, and please let me know if I've missed something, or if I can clarify in any way. :)
i couldn't manage to have a menu dynamic,
instead i used the Collapse Panel example and there i manipulated with a property isOpen on every item of the array.
Check Cards Collapse Example
On the setIsOpen method you can change this bool prop:
const setIsOpen = (argNodeId: string) => {
const founded = tree.find(item => item.nodeId === argNodeId);
const items = [...tree];
if (founded) {
const index = tree.indexOf(founded);
founded.isOpen = !founded.isOpen;
items[index]=founded;
setTree(items);
}
};
<IconButton className={clsx(classes.expand, {
[classes.expandOpen]: node.isOpen,
})}
onClick={()=>setIsOpen(node.nodeId)}
aria-expanded={node.isOpen}
aria-label="show more"
>
<MoreVertIcon />
</IconButton>
</CardActions>
<Collapse in={node.isOpen} timeout="auto" unmountOnExit>
<CardContent>
<MenuItem onClick={handleClose}>{t("print")}</MenuItem>
<MenuItem onClick={handleClose}>{t("commodities_management.linkContainers")}</MenuItem>
<MenuItem onClick={handleClose}>{t("commodities_management.linkDetails")}</MenuItem>
</CardContent>
</Collapse>
I think this is the right solution for this: https://stackoverflow.com/a/59531513, change the anchorEl for every Menu element that you render. :D
This code belongs to TS react if you are using plain JS. Then remove the type.
import Menu from '#mui/material/Menu';
import MenuItem from '#mui/material/MenuItem';
import { useState } from 'react';
import { month } from '../../helper/Utilities';
function Company() {
const [anchorEl, setAnchorEl] = useState<HTMLElement[]>([]);
const handleClose = (event: any, idx: number) => {
let array = [...anchorEl];
array.splice(idx, 1);
setAnchorEl(array);
};
<div>
{month &&
month.map((val: any, ind: number) => {
return (
<div
key={val.id + 'w9348w344ndf allBankAndCardAccountOfClient'}
style={{ borderColor: ind === 0 ? '#007B55' : '#919EAB52' }}
>
<Menu
id='demo-positioned-menu'
aria-labelledby='demo-positioned-button'
anchorEl={anchorEl[ind]}
open={anchorEl[ind] ? true : false}
key={val.id + 'w9348w344ndf allBankAndCardAccountOfClient' + ind}
onClick={(event) => handleClose(event, ind)}
anchorOrigin={{
vertical: 'top',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<MenuItem
key={val.id + 'w9348w344ndf allBankAndCardAccountOfClient' + ind}
onClick={(event) => handleClose(event, ind)}
style={{
display: ind === 0 ? 'none' : 'inline-block',
}}
>
<span
style={{
marginLeft: '.5em',
color: 'black',
background: 'inherit',
}}
>
Make Primary
</span>
</MenuItem>
<MenuItem onClick={(event) => handleClose(event, ind)}>
<span style={{ marginLeft: '.5em', color: 'black' }}>Edit</span>
</MenuItem>
<MenuItem
onClick={(event) => handleClose(event, ind)}
style={{
display: ind === 0 ? 'none' : 'inline-block',
}}
>
<span style={{ marginLeft: '.5em', color: 'red' }}>Delete</span>
</MenuItem>
</Menu>
</div>
);
})}
</div>;
}
export default Company;
I have Avatar and Menu components inside the Navbar component. I want to trigger the function from Menu component by clicking on Avatar component. My question is how to pass a clickEvent from avatar to Menu component.
<AppBar position="sticky" className={classes.navBarStyle}>
<Toolbar>
<Avatar alt="" className={classes.userAvatar} src="./avatar.jpg"/>
<DropDownMenu/>
</Toolbar>
</AppBar>
function DropDownMenu() {
const [anchorEl, setAnchorEl] = React.useState(null);
const open = Boolean(anchorEl);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
return (
<div>
<Menu
id="fade-menu"
anchorEl={anchorEl}
keepMounted
open={open}
onClose={handleClose}
TransitionComponent={Fade}
>
<MenuItem onClick={handleClose}>Profile</MenuItem>
<MenuItem onClick={handleClose}>My account</MenuItem>
<MenuItem onClick={handleClose}>Logout</MenuItem>
</Menu>
</div>
);
}
You don't need to pass the click event around, you just need to move the anchorEl state to your component with AppBar and pass anchorEl and onClose to DropDownMenu as props:
function MainAppBar() {
const [anchorEl, setAnchorEl] = React.useState(null);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
<AppBar position="sticky" className={classes.navBarStyle}>
<Toolbar>
<Avatar alt="" className={classes.userAvatar} src="./avatar.jpg" onClick={handleClick} />
<DropDownMenu anchorEl={anchorEl} onClose={handleClose} />
</Toolbar>
</AppBar>
}
function DropDownMenu({ anchorEl, onClose }) {
const open = Boolean(anchorEl);
return (
<div>
<Menu
id="fade-menu"
anchorEl={anchorEl}
keepMounted
open={open}
onClose={onClose}
TransitionComponent={Fade}
>
<MenuItem onClick={handleClose}>Profile</MenuItem>
<MenuItem onClick={handleClose}>My account</MenuItem>
<MenuItem onClick={handleClose}>Logout</MenuItem>
</Menu>
</div>
);
}
I wrote a library, material-ui-popup-state, that is less cumbersome to use for cases like this:
import {bindTrigger, bindMenu, usePopupState} from 'material-ui-popup-state/hooks';
function MainAppBar() {
const popupState = usePopupState({ variant: 'popover', popupId: 'fade-menu' });
<AppBar position="sticky" className={classes.navBarStyle}>
<Toolbar>
<Avatar {...bindTrigger(popupState)} alt="" className={classes.userAvatar} src="./avatar.jpg" />
<DropDownMenu popupState={popupState} />
</Toolbar>
</AppBar>
}
function DropDownMenu({ popupState }) {
return (
<div>
<Menu
{...bindMenu(popupState)}
keepMounted
TransitionComponent={Fade}
>
<MenuItem onClick={popupState.close}>Profile</MenuItem>
<MenuItem onClick={popupState.close}>My account</MenuItem>
<MenuItem onClick={popupState.close}>Logout</MenuItem>
</Menu>
</div>
);
}
The trick is to place the function in the parent component that and are both children of, and pass it down as a prop, that way it can be a shared function.
https://reactjs.org/docs/faq-functions.html
You have ToolBar component which is parent of your avatar and menudropdown components. Place those handleClick, handleClose and const [anchorEl, setAnchorEl] = React.useState(null); in parent component of those childs.
Next pass handleClose funtion to dropDownMenu component and handleClick to Avatar component as props. You should also pass state to those components who base their actions on it, in this case to menuDropDowncomponent because i see that he need this state data.
I created a navigation bar from the material-ui website and I have a onClick that when the user clicks the icon button the navigation will get redirected to a new page and I would like for the navigation to close afterwards. I'v tried different things, but for some reason it will not close.
The only thing that it does now is gets redirected to a new page and the navigation drawer continues to stay open.
I have a a function called handleDrawerClose() that closes the drawer, a const called navigation that creates the text and components and I created a const called handleNavigation that pushes the links, which makes the page redirect. Is there a way to call both of these someway. Thank you.
Below is my code:
const navigation = [
{ to: '/', text: 'Upload', Icon: InboxIcon },
{ to: '/email', text: 'Send', Icon: MailIcon }
]
const NavLinks = ({ links, onClick }) => {
const _onClick = to => () => onClick(to);
return (
<List>
{links.map(({ to, text, Icon }) => (
<ListItem key={to} button onClick={_onClick(to)}>
<ListItemIcon>
<Icon />
</ListItemIcon>
<ListItemText primary={text} />
</ListItem>
))}
</List>
)
}
const IconArrow = ({ onClick }) => {
return (
<IconButton onClick={onClick}>
<ChevronLeftIcon />
</IconButton>
)
}
export default withRouter(({ history }) => {
const classes = useStyles();
const [_, { logout }] = useAppAuth();
const [open, setOpen] = React.useState(false);
const [state, setState] = React.useState({
left: false,
});
function handleDrawerOpen() {
setOpen(true);
}
function handleDrawerClose() {
setOpen(false);
}
const handleNavigation = to => () => history.push(to);
return (
<Fragment>
<AppBar position='static'>
<Toolbar className={classes.toolbar}>
<IconButton
color='inherit'
edge='start'
aria-label='open menu drawer'
onClick={handleDrawerOpen}
>
<MenuIcon />
</IconButton>
<Link to='/'>
<img alt='Logo' src={logo} className={classes.image} />
</Link>
<Typography variant='h6' className={classes.title}>
{process.env.REACT_APP_NAME}
</Typography>
<Tooltip title='Logout'>
<Link to='/login'>
<IconButton onClick={logout} className={classes.logout}>
<LogoutIcon />
</IconButton>
</Link>
</Tooltip>
</Toolbar>
</AppBar>
<Drawer
className={classes.drawer}
variant='temporary'
anchor='left'
open={open}
>
<div className={classes.iconArrow}>
<IconArrow onClick={handleDrawerClose} />
</div>
<Divider />
<NavLinks
links={navigation}
onClick={() => handleNavigation}
/>
</Drawer>
</Fragment>
)
})
Why dont you just close it in the handler?
const handleNavigation = to => {
setOpen(false); // add this
history.push(to);
}