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}>
Related
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>
))
}
I'm using Material UI's nested/select ItemList component to dynamically generates any number of dropdown menu items based on how many items belong in this header as you can maybe tell from the mapping function. On another file 1 layer above this one, I am again mapping and generating multiple of these DropDownMenus, is it possible for these components to communicate to each other?
This is the file in question
const useStyles = makeStyles((theme) => ({
root: {
width: '100%',
maxWidth: 330,
backgroundColor: theme.palette.background.paper,
},
nested: {
paddingLeft: theme.spacing(4),
}
}));
export default function DropDownMenu(props) {
const classes = useStyles();
const [open, setOpen] = React.useState(true);
let unitName = props.unit[0];
let chapterList = props.unit.slice(1);
const [selectedIndex, setSelectedIndex] = React.useState(1);
const handleListItemClick = (index) => {
console.log("ItemClicked");
console.log(index);
setSelectedIndex(index);
};
const handleClick = () => {
setOpen(!open);
};
const selectMenuItem = (chapter, index) => {
props.chooseChapter(chapter)
handleListItemClick(index)
}
let dropDownUnit = chapterList.map((chapter, index) => {
return (
<ListItem button
className={classes.selected}
selected={selectedIndex === index}
onClick={() => selectMenuItem(chapter, index)}
key={index}>
<ListItemText primary={chapter} />
</ListItem>
)
})
return (
<List
component="nav"
aria-labelledby="nested-list-subheader"
subheader={
<ListSubheader component="div" id="nested-list-subheader">
</ListSubheader>
}
className={classes.root}
>
<ListItem button onClick={handleClick}>
<ListItemText primary={unitName} />
{!open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={!open} timeout="auto" unmountOnExit>
<List component="div" disablePadding className={classes.selected}>
{dropDownUnit}
</List>
</Collapse>
</List>
);
}
Psudo Style - What I'm trying to accomplish
<DropDownMenu>
<MenuItem> // Suppose this is selected
<MenuItem>
<DropDownMenu>
<MenuItem> // onClick --> Select this and deselect all other selected buttons
You can have a parent of these components such that the parent will keep the state of who is active. That way you can pass that state & the state setter as props so that everyone will know who is active
export default function App() {
const [selectedItem, setSelectedItem] = React.useState();
return (
<>
<DropDownMenu
selectedItem={selectedItem} // pass down as props
setSelectedItem={setSelectedItem} // pass down as props
unit={...}
chooseChapter={function () {}}
/>
...
On the children, simply refactor the Call To Action (in this case onClick) to set the state using the passed down props. Pay attention to the selected prop of ListItem, we now use the state we have passed down from the parent
let dropDownUnit = chapterList.map((chapter, index) => {
return (
<ListItem
button
className={classes.selected}
selected={props.selectedItem === chapter}
onClick={() => props.setSelectedItem(chapter)}
key={index}
>
<ListItemText primary={chapter} />
</ListItem>
);
});
How can I only expand the area that I have clicked on?
I have already tried to pass the index of the map function into the handleExpandClick () function, but that didn't work either
For the moment, all cars are expanded if i click at one.
export default function Card({ wert, ergebnis }) {
const [expanded, setExpanded] = React.useState(false);
const handleExpandClick = () => {
setExpanded(!expanded);
};
[...]
{ergebnis.map((e, index) => (
[...]
<IconButton
className={clsx(classes.expand, {[classes.expandOpen]: expanded,})}
onClick={handleExpandClick}
aria-expanded={expanded}
aria-label="show more"
>
[...]
<Collapse in={expanded} timeout="auto" unmountOnExit>[...]</Collapse>
[...]
))}
I'd use an object to track each elements expanded state, something like:
const [expanded, setExpanded] = React.useState({});
const handleExpandClick = (index) => {
setExpanded({
...expanded,
[index]: !expanded[index]
});
};
<IconButton
className={clsx(classes.expand, {[classes.expandOpen]: !!expanded[index],})}
onClick={() => handleExpandClick(index)}
aria-expanded={expanded}
aria-label="show more"
>
<Collapse in={!!expanded[index]} timeout="auto" unmountOnExit>[...]</Collapse>
I have 2 onClick functions
function VisitGallery(name) {
const history = useHistory();
console.log("visitgallery", name)
history.push("/gallery")
}
function App() {
const accesstoken = "******************"
const [viewport, setviewport] = React.useState({
latitude: ******
longitude: *******
width: "100vw",
height: "100vh",
zoom: 11
})
const [details, setdetails] = React.useState([
])
React.useEffect(() => {
const fetchData = async () => {
const db = firebase.firestore()
const data = await db.collection("data").get()
setdetails(data.docs.map(doc => doc.data()))
}
fetchData();
}, [])
const [selectedpark, useselectedpark] = React.useState(null);
React.useEffect(() => {
const listener = e => {
if (e.key === "Escape") {
useselectedpark(null);
}
};
window.addEventListener("keydown", listener)
return () => {
window.removeEventListener("keydown", listener)
}
}, [])
return (
<div className="App">
<ReactMapGl {...viewport}
mapboxApiAccessToken={accesstoken}
mapStyle="mapbox://**************"
onViewportChange={viewport => {
setviewport(viewport)
}}>
{details.map((details) =>
<Marker key={details.name} latitude={details.lat} longitude={details.long}>
<button class="marker-btn" onClick={(e) => {
e.preventDefault();
useselectedpark(details);
}}>
<img src={icon} alt="icon" className="navbar-brand" />
</button>
</Marker>
)}
{selectedpark ?
(<Popup
latitude={selectedpark.lat}
longitude={selectedpark.long}
onClose={() => {
useselectedpark(null);
}}
>
<div>
<Card style={{ width: '18rem' }}>
<Card.Body>
<Card.Title>{selectedpark.name}</Card.Title>
<Card.Text>
{selectedpark.postalcode}
</Card.Text>
<Button variant="primary" onClick = VisitGallery() >Visit Gallery</Button>
</Card.Body>
</Card>
</div>
</Popup>)
: null}
{
console.log("in render", details)
}
</ReactMapGl>
</div>
);
}
export default App;
The outer onClick is assigned when the marker is first created, and when it is clicked the useselectedpark function is called, details is then assigned to selectedpark.
The inner onClick is assigned to the function VisitGallery(). When the inner onClick is triggered, i want to navigate to another page, hence the history.push().
Ideally, what i want for it to happen is, when the outer onClick is triggered, the cardview shows, and i have an option to visit the next page, which can be triggered by an onClick within the card. However, what is happening right now is both the onClicks are triggered when i click on the thumbnail. How do i fix it such that it is how i want it to be ideally?
ps: do let me know if my explanation is confusing and i will edit it accordingly
Try adding your second onClick into a callback function?
<Button variant="primary" onClick='()=>{ VisitGallery() }' >Visit Gallery</Button>
So that it doesn't automatically invoke the function until the click is triggered.
I am new to material-ui and React and I have a requirement to create multiple menus dynamically in a loop. Please find the code snippet as:
state = {
anchorEl: null,
};
handleClick = event => {
this.setState({ anchorEl: event.currentTarget });
};
handleClose = () => {
this.setState({ anchorEl: null });
};
render() {
const { anchorEl } = this.state;
let items = _.map(results, (item, index) => {
return (
<ListItem
key={item.ID}
divider
>
<ListItemSecondaryAction>
<IconButton
aria-label="More"
aria-owns={anchorEl ? 'long-menu' : null}
aria-haspopup="true"
onClick={this.handleClick}
>
<MoreVertIcon />
</IconButton>
<Menu
id="long-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={this.handleClose}
PaperProps={{
style: {
maxHeight: 200,
width: 200,
},
}}
>
<MenuItem>
<IconButton onClick={() => this.props.delete(item.ID)} >
Delete entry<DeleteIcon />
</IconButton>
</MenuItem>
</Menu>
<ListItemSecondaryAction>
</ListItem>
)
})
return (
<Fragment>
<List>
{items}
</List>
</Fragment>
)
}
Now, with the above code, the menus work fine and the UI is good. But whenever I try to delete an entry by clicking on Delete icon inside the menu, always the last entry is deleted i.e. item.ID passes the value of the last element and the last entry is deleted.
Is there a way I can create unique menuitems for each entry and manage the state in such a way which makes sure that the correct item is deleted and not the last one always.
Note: 'results' is any list loaded dynamically and 'delete' function implements the functionality to delete the corresponding entry
Thanks in advance.
I would suggest use another child component for render your list item. In your current example you only one anchorEl, which means wherever you click, always one menu open and take action of that, which is last one. If you have child component for menu item, each component will have there own state and work for that item only.
Example
class Main extends Component {
render() {
let items = _.map(results, (item, index) => {
return (
<MenuItemComponent key={item.ID} item={item} onClick={this.handleClick} onDelete={(item) => this.props.delete(item.ID)} />
)
})
return (
<Fragment>
<List>
{items}
</List>
</Fragment>
)
}
}
class MenuItemComponent extends Component {
state = {
anchorEl: null,
};
handleClick = event => {
this.setState({ anchorEl: event.currentTarget });
};
handleClose = () => {
this.setState({ anchorEl: null });
};
render() {
const { item } = this.props;
const { anchorEl } = this.state;
return (
<ListItem
divider
>
<ListItemSecondaryAction>
<IconButton
aria-label="More"
aria-owns={anchorEl ? 'long-menu' : null}
aria-haspopup="true"
onClick={this.handleClick.bind(this)}
>
<MoreVertIcon />
</IconButton>
<Menu
id="long-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={this.handleClose.bind(this)}
PaperProps={{
style: {
maxHeight: 200,
width: 200,
},
}}
>
<MenuItem>
<IconButton onClick={() => this.props.onDelete(item)} >
Delete entry<DeleteIcon />
</IconButton>
</MenuItem>
</Menu>
</ListItemSecondaryAction>
</ListItem>
)
}
}
Here's a working example https://codesandbox.io/s/nn555l48xm
import * as React from "react";
import {
Menu,
MenuItem,
IconButton
} from "#material-ui/core";
import MoreVertIcon from "#material-ui/icons/MoreVert";
export default function Demo() {
const [openElem, setOpenElem] = React.useState(null);
const [anchorEl, setAnchorEl] = React.useState(null);
const handleClick = (elem) => (event) => {
setAnchorEl(event.currentTarget);
setOpenElem(elem);
};
const handleClose = () => {
setAnchorEl(null);
setOpenElem(null);
};
let arr = [0, 1, 2];
let body = arr.map((elem) => {
return (
<div key={elem}>
<IconButton
aria-label="more"
aria-controls={"long-menu" + elem}
aria-haspopup="true"
onClick={handleClick(elem)}
>
<MoreVertIcon />
</IconButton>
<Menu
id={"long-menu" + elem}
anchorEl={anchorEl}
keepMounted
open={openElem === elem}
onClose={handleClose}
>
<MenuItem
onClick={(e) => {
handleClose();
}}
>
{elem}
</MenuItem>
</Menu>
</div>
);
});
return <div>{body}</div>;
}