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>;
}
Related
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>
);
});
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}>
Action overriding feature allows me to override button but it overrides all the action buttons. For example, if I have two action buttons for edit and delete and I use the Action overriding, both of my buttons get overridden with that same custom code.
How can I specify different codes for different buttons?
My final goal is to conditionally disable edit and delete buttons based on the rowData. I have tried with isEditable feature as shown in the code below but it doesn't work either.
...
....
const components = {
Action: props => (
<IconButton aria-label="delete" size="small"
disabled={props.data.email === 'admin#pipilika.com'}
onClick={(event) => props.action.onClick(event, props.data)}
>
<Icon>delete</Icon>
</IconButton>
)
};
const actions = [
{
icon: 'edit',
tooltip: 'Edit Index',
onClick: (event, rowData) => {
this.onEditClick(null, rowData._id);
}
},
{
icon: 'delete',
tooltip: 'Delete Index',
onClick: (event, rowData) => {
this.onDeleteClick(null, rowData._id);
}
},
];
const options= {
showTitle: false,
actionsColumnIndex: -1,
searchFieldStyle: {
color: "#fff"
}
};
const editable= {
isEditable: rowData => rowData.dataType === "html",
isDeletable: rowData => rowData.dataType === "html",
};
return(
<MaterialTable
editable={editable}
title="Created Index List"
columns={columns}
data={dataTypes}
actions={actions}
options={options}
components={components}
style={{overflow: 'hidden'}}
/>
);
for this specific use case, you can choose which Button to render based on checking the right icon name like this.
components={{
Action:
props => {
if(props.action.icon === 'edit'){
return(
<Button
onClick={(event) => props.action.onClick(event, props.data)}
color="primary"
variant="contained"
style={{textTransform: 'none'}}
size="small"
disabled
>
My Button
</Button>
)
}
if(props.action.icon === 'save'){
return(
<Button
onClick={(event) => props.action.onClick(event, props.data)}
color="primary"
variant="contained"
style={{textTransform: 'none'}}
size="small"
>
My Button
</Button>
)
}
}
}}
This solution worded form me:
import React, { Component } from 'react';
import {Button} from '#material-ui/core/';
import Icon from '#material-ui/core/Icon';
class ButtonBack extends Component {
constructor(props) {
super(props);
this.state={
onClick: props.onClick,
icon: props.icon
}
};
render() {
return (
<Button label="Regresar" onClick={this.state.onClick}>
<Icon>{this.state.icon}</Icon>
Regresar
</Button>
);
}
}
export default ButtonBack;
components={{
Action: props => {
if (typeof props.action === "function"){
var element= props.action(props.data);
return (
<IconButton aria-label={element.icon} size="small"
onClick={element.onClick}
>
<Icon>{element.icon}</Icon>
</IconButton>
)
}else{
if (props.action.icon==="keyboard_backspace"){
return (
<ButtonBack icon={props.action.icon}
onClick={props.action.onClick}
>
</ButtonBack>
)
}else{
return (
<IconButton aria-label={props.action.icon} size="small"
onClick={props.action.onClick}
>
<Icon>{props.action.icon}</Icon>
</IconButton>
)
}
}
}
}}
I have a nav menu built with material-ui/core in Navbar.
I use useRef to track the position of clicked button on toggle menu close.
anchorRef.current.contains(event.target)
And I am getting 'Uncaught TypeError: anchorRef.current.contains is not a function' .
I tried 'Object.values(anchorRef.current).includes(event.target)' instead, it always returns false.
-- update --
anchorRef.current.props Object.
withStyles {
props:{
aria-haspopup: "true"
aria-owns: undefined
children: "계정"
className: "nav-menu--btn"
onClic: f onClick()
get ref: f()
isReactWarning: true
arguments: (...)
caller: (...)
length: 0
name: "warnAboutAccessingRef"
...
}, context{...}, refs{...}, ...}
ToggleMenuList
const ToggleMenuList = ({ navAdminList, navAdminItems, classes }) => {
const [activeId, setActiveId] = useState(null);
const anchorRef = useRef(null);
const handleToggle = id => {
setActiveId(id);
};
const handleClose = event => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setActiveId(null);
};
return (
<React.Fragment>
<div className={`nav-menu--admin ${classes.root}`}>
{navAdminList.map(e => (
<div key={e.id}>
<Button
ref={anchorRef}
aria-owns={activeId === e.id ? 'menu-list-grow' : undefined}
aria-haspopup="true"
onClick={() => handleToggle(e.id)}
>
{e.name}
</Button>
{activeId === e.id && (
<ToggleMenuItems
id={e.id}
activeId={activeId}
handleClose={handleClose}
anchorRef={anchorRef}
items={navAdminItems[e.id]}
/>
)}
</div>
))}
</div>
</React.Fragment>
);
};
export default withStyles(styles)(ToggleMenuList);
ToggleMenuItems
const ToggleMenuItems = ({
listId,
activeId,
handleClose,
anchorRef,
items,
}) => {
const isOpen = activeId === listId;
const leftSideMenu = activeId === 3 || activeId === 4 ? 'leftSideMenu' : '';
return (
<Popper
open={isOpen}
anchorEl={anchorRef.current}
keepMounted
transition
disablePortal
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === 'bottom' ? 'center top' : 'center bottom',
}}
className={`toggle-menu ${leftSideMenu}`}
>
<Paper id="menu-list-grow">
<ClickAwayListener
onClickAway={handleClose}
>
<MenuList className="toggle-menu--list">
{items.map(e => (
<MenuItem
key={e.id}
className="toggle-menu--item"
onClick={handleClose}
>
<Link
to={e.to}
className="anchor td-none c-text1 toggle-menu--link"
>
{e.name}
</Link>
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
);
};
export default ToggleMenuItems;
react: ^16.8.6
react-dom: ^16.8.6
react-router-dom: ^4.3.1
#material-ui/core: ^3.1.2
I assume your ToggleMenuItems sets up global(document-level?) event listener on click to collapse Menu on clicking somewhere outside.
And you have a sibling button element. Clicking on that you want to keep menu expanded, right? So that was the point to use .contains in onClick to check if we are clicked outside of ToggleMenuItems but in scope of specific Button. The reason why it does not work: <Button> is custom class-based React component so it returns React component instance in ref. And it does not have any DOM-specific methods like .contains
You can rework you current approach: just stop bubbling event in case Button has been clicked. It would stop global event handler set by ToggleMenuItems to react.
const stopPropagation = (event) => event.stopPropagation();
const ToggleMenuList = ({ navAdminList, navAdminItems, classes }) => {
const [activeId, setActiveId] = useState(null);
const anchorRef = useRef(null);
const handleToggle = id => {
setActiveId(id);
};
const handleClose = event => {
setActiveId(null);
};
return (
<React.Fragment>
<div className={`nav-menu--admin ${classes.root}`}>
{navAdminList.map(e => (
<div key={e.id}>
<div onClick={stopPropagation}>
<Button
aria-owns={activeId === e.id ? 'menu-list-grow' : undefined}
aria-haspopup="true"
onClick={() => handleToggle(e.id)}
>
{e.name}
</Button>
</div>
{activeId === e.id && (
<ToggleMenuItems
id={e.id}
activeId={activeId}
handleClose={handleClose}
anchorRef={anchorRef}
items={navAdminItems[e.id]}
/>
)}
</div>
))}
</div>
</React.Fragment>
);
};
export default withStyles(styles)(ToggleMenuList);
I've put stopPropagation handler outside since it does not depend on any internal variable.
I am getting cannot set property error.If I am removing this from this.state, It is giving error like 'state' is not defined no-undef.
Solve this problem.Thanks in advance. Imports statements are there.
Error: TypeError: Cannot set property 'state' of undefined.
this.state = {
anchorEl: null,
};
this.handleClick = event => {
this.setState({ anchorEl: event.currentTarget });
};
this.handleClose = () => {
this.setState({ anchorEl: null });
};
function SimpleCard(props) {
const { classes } = props;
const { anchorEl } = this.state;
const open = Boolean(anchorEl);
return (
<Card className={classes.card}>
<CardHeader
action={
<IconButton aria-label="More"
aria-owns={open ? 'long-menu' : null}
aria-haspopup="true"
onClick={this.handleClick}>
<MoreVertIcon />
<Menu
id="long-menu"
anchorEl={anchorEl}
open={open}
onClose={this.handleClose}
PaperProps={{
style: {
maxHeight: ITEM_HEIGHT * 4.5,
width: 100,
},
}}
>
{options.map(option => (
<MenuItem key={option} selected={option === 'Pyxis'} onClick={this.handleClose}>
{option}
</MenuItem>
))}
</Menu>
</IconButton>
}
title={props.products.title}
/>
<CardContent>
<Typography component="p">
{props.products.id}
</Typography>
</CardContent>
</Card>
);
}
SimpleCard.propTypes = {
classes: PropTypes.object.isRequired,
};
export default withStyles(styles)(SimpleCard);
handleClose and handleClick are functions, not object's methods: the use of this inside them has no real meaning out of the box, and you have no state using functional components.
The simplest fix is to switch to a class based component:
class SimpleCard extends React.PureComponent {
this.handleClick = event => {
this.setState({ anchorEl: event.currentTarget });
}
handleClose = () => {
this.setState({ anchorEl: null });
}
render() {
// your current SimpleCard code here
}
}