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>
);
});
Related
I have a MetaTable Component that has LeftSidePanel wrapped with the UseContext. I want to open the panel when button is click on the MetaTable (passing some data to show detail of a particular record). My code works to open the Panel but when I click outside it doesn't close.I think I would need to set the State back on the parent. Tried a few things but failed. Any guidance please?
MetaTable Component
export const DrawerDataContext = createContext();
export const DrawerContext = createContext();
const [isDrawerOpen, setDrawerOpen] = useState();
const [bookId, setBookId] = useState({});
const toggleSidePanel = (row) => {
setBookId(row)
setDrawerOpen(true);
}
... <TableCell className={classes.tableCell}>
<Stack spacing={0}>
<Typography variant="body2" my={0}>
{row}
</Typography>
<Typography variant="body2" my={0}>
{row}
</Typography>
</Stack>
<Stack spacing={2}>
<button onClick={() => { toggleSidePanel(row); }} >toggle drawer</button>
</Stack>
</TableCell>...
<DrawerDataContext.Provider value={bookId}>
<DrawerContext.Provider value={isDrawerOpen} >
<LeftSidePanel />
</DrawerContext.Provider>
</DrawerDataContext.Provider>
LeftSidePanel Component
const book= useContext(DrawerDataContext);
const open = useContext(DrawerContext);
return (
<>
<Drawer open={open} onClose={() => !open} anchor='left'>
<List style={{ width: 500 }}>
</List>
</Drawer>
</>
);
In addition to the value of your state, you can also share a function with your context to change its value:
<DrawerContext.Provider value={{
isOpen: isDrawerOpen,
close: () => setDrawerOpen(false)
}}>
And in your component:
const book = useContext(DrawerDataContext);
const { isOpen, close } = useContext(DrawerContext);
return (
<>
<Drawer open={isOpen} onClose={close} anchor='left'>
<List style={{ width: 500 }}>
</List>
</Drawer>
</>
);
Trying to get an an onclick function to delete an item (that's been clicked) from an array.
However, it doesnt delete anything. setListOfDocs is supposed to set the listOfDocs array with the clicked item gone (onClick function is a filter)
It does not work. There's no error in the console. I don't know why it's not updating the state so that the listOfDocs is the new filtered array (with the clicked item removed from the array).
Im using material ui.
function NewMatterDocuments() {
const [listOfDocs, setListOfDocs] = useState(['item1', 'item2', 'item3', 'item4']);
const [disableButton, toggleDisableButton] = useState(false);
const [displayTextField, toggleDisplayTextField] = useState('none');
const [textInputValue, setTextInputValue] = useState();
const buttonClick = () => {
toggleDisableButton(true);
toggleDisplayTextField('box');
};
//this function works
const handleEnter = ({ target }) => {
setTextInputValue(target.value);
const newItem = target.value;
setListOfDocs((prev) => {
return [...prev, newItem];
});
setTextInputValue(null);
}; //
//this function does not work....which is weird because it pretty much
// does the same thing as the previous function except it deletes an item
// instead of adding it to the array. Why does the previous function work
// but this one doesnt?
const deleteItem = ({ currentTarget }) => {
const deletedId = currentTarget.id;
const result = listOfDocs.filter((item, index) => index !== deletedId);
setListOfDocs(result)
};
return (
<div>
<Typography variant="body2">
<FormGroup>
<Grid container spacing={3}>
<Grid item xs={6}>
<Grid item xs={6}>
Document Type
</Grid>
{listOfDocs.map((item, index) => {
return (
<ListItem id={index} secondaryAction={<Checkbox />}>
<ListItemAvatar>
<Avatar>
<DeleteIcon id={index} onClick={deleteItem} />
</Avatar>
</ListItemAvatar>
<ListItemButton>
<ListItemText id={index} primary={item} />
</ListItemButton>
</ListItem>
);
})}
<List sx={{ width: '100%', bgcolor: 'background.paper' }}>
<Button disabled={!disableButton ? false : true} color="inherit" onClick={buttonClick}>
Add
</Button>
<ListItem sx={{ display: `${displayTextField}` }} id={uuid()} key={uuid()}>
<TextField
id="standard-basic"
label="Additional Document"
variant="standard"
value={textInputValue}
onKeyDown={(e) => {
e.key === 'Enter' && handleEnter(e);
}}
></TextField>
</ListItem>
</List>
</Grid>
</Grid>
</FormGroup>
</Typography>
</div>
);
}
export default NewMatterDocuments;
I tried to change the function that wasnt working into the following:
const deleteItem = ({ currentTarget }) => {
setListOfDocs((prev) => {
prev.filter((item, index) => index != currentTarget.id);
});
};
It gives the error
"Uncaught TypeError: Cannot read properties of undefined (reading 'map')"
The next map function inside the jsx doesnt work...
You should return your data after filter:
const deleteItem = ({ currentTarget }) => {
setListOfDocs((prev) => {
return prev.filter((item, index) => index != currentTarget.id);
});
};
Update:
There is a problem with your first try. The currentTarget.id field is string but in your filter method, you're comparing it with the index (which is number) with !== which also checks types of the 2 variables.
So you can fix it by replacing !== with != or converting string to number.
I believe that you made a small mistake in your filtering function: you are comparing item index with deletedId, where you should compare item with deletedId
This should be correct:
const deleteItem = ({ currentTarget }) => {
const deletedId = currentTarget.id;
const result = listOfDocs.filter((item, index) => item !== deletedId);
setListOfDocs(result)
};
This is either very simple or I am doing it completely wrong. I am a novice so please advise.
I am trying to show different components inside different tabs using Material UI using array map. The tabs are showing fine but the components do not render. Basically if the array label is 'Welcome', the tab name should be 'Welcome' and the Welcome component should show up and so on. Please help!
return (
<Box sx={{ width: '100%' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={value} onChange={handleChange} aria-label="basic tabs example">
{fetchedCategories.map((category) => (
<Tab key={category.label} label={category.label} />
))}
</Tabs>
</Box>
{fetchedCategories.map((category, index) => {
const Component=myComponents[category.label];
})}
{fetchedCategories.map((category, index) => (
<TabPanel key={category.label} value={value} index={index}>
<Component label={category.label} />
</TabPanel>
))}
</Box>
);
Here is my props & Component function:
interface ComponentProps {
label: string;
value?: number;
}
function Component (props: ComponentProps)
{
const {label, value} = props;
return myComponents[label];
}
const myComponents = {
'Welcome': Welcome,
'Salad/Soup': Welcome
}
Try something like:
function Component({ label, value }: ComponentProps) {
const [Comp, setComponent] = useState(<div />);
React.useEffect(() => {
const LabelComp = myComponents[label];
if (label && LabelComp) {
setComponent(<LabelComp value={value} />); // <-- if you want to pass value to you component
}
}, [value, label]);
return Comp;
}
And you will use it like:
const App = () => {
return <Component label={"myComponentLabel"} value={"some value"} />;
};
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}>
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>;
}