I am trying to loop over nested object in using map on List Component from MUI. When performing click all elements are getting clicked.
This is my class component:
class TablePivot extends React.Component {
constructor(props) {
super(props)
this.state = {
pivotData: [],
firstLevelOpen: false,
secondLevelOpen: false,
thirdLevelOpen: false,
}
handleFirstListClick = () =>{
this.setState({firstLevelOpen:!this.firstLevelOpen})
console.log(this.secondLevelOpen)
}
handleSecondListClick = () =>{
this.setState({secondLevelOpen:!this.secondLevelOpen})
console.log(this.secondLevelOpen)
}
handleThirdListClick = () =>{
this.setState({thirdLevelOpen:!this.thirdLevelOpen})
console.log(this.thirdLevelOpen)
}
render () {
return (
<>
<List>
{Object.entries(this.state.pivotData).map((key, val) =>{
// console.log(key[1])
return (
<ListItemButton onClick = {event => this.handleFirstListClick}>
<ListItemText primary={key[0]} />
{this.state.firstLevelOpen ? <ExpandLess /> : <ExpandMore />}
{Object.entries(key[1]).map((key,index) =>{
return (
<Collapse in={this.state.firstLevelOpen} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
<ListItemButton onClick = {this.handleSecondListClick} sx={{ pl: 4 }}>
<ListItemText primary={key[0]} />
{this.state.secondLevelOpen ? <ExpandLess /> : <ExpandMore />}
{Object.entries(key[1]).map((key,index) =>{
<Collapse in={this.state.secondLevelOpen} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
<ListItemButton onClick = {this.handleThirdListClick} sx={{ pl: 4 }}>
<ListItemText primary={key[1]} />
{this.state.thirdLevelOpen ? <ExpandLess /> : <ExpandMore />}
{/* {
Object.entries(key[1]).map((key,index) =>{
return (console.log[key])
})
} */}
</ListItemButton>
</List>
</Collapse>
})
}
</ListItemButton>
</List>
</Collapse>
)
})}
</ListItemButton>
)
})}
</List>
<Snackbar anchorOrigin={{ vertical : "top", horizontal : "right" }} open={this.state.alert} autoHideDuration={5000} onClose={() => this.setState({alert : false})}>
<Alert variant="filled" elevation={6} onClose={() => this.setState({alert : false})} severity={this.state.severity}>
{this.state.alertMsg}
</Alert>
</Snackbar>
</>
)
}
I have tried passing index and event inside onclick like this:
handleFirstListClick(event, index) {
console.log(event.target)
this.setState(
prevState => ({
...prevState.firstLevelOpen,
[index]: !prevState.firstLevelOpen
})
)
console.log(this.state.firstLevelOpen)
// if (this.state.firstLevel === true)
// firstLevel[index] = false
// else
// firstLevel[index] = true
// this.setState({firstLevelOpen:firstLevel});
}
// Changes inside render() inside list component for passing value
<ListItemButton onClick = {event => this.handleFirstListClick(event, val)}>
But doing this will not update state properly. I am not understanding what is going wrong. I am learning react using project. I am struggling for this click event a lot.
When you click the first ListItemButton, it set the firstLevelOpen state to true.
And at this part of the code
{this.state.firstLevelOpen ? <ExpandLess /> : <ExpandMore />}
because the firstLevelOpen is now true, all the first level ExpandLess component will be visible.
To handle the state for each button in the list, you can change the state from boolean into object. What you already did is very similar to the code below.
this.state = {
pivotData: [],
firstLevelOpen: {},
secondLevelOpen: {},
thirdLevelOpen: {},
}
on the click handler
handleFirstListClick(event, index) {
this.setState(
prevState => ({
firstLevelOpen: {
...prevState.firstLevelOpen,
[index]: !prevState.firstLevelOpen[index]
}
})
)
}
and now change the conditional rendering to
{this.state.firstLevelOpen[index] ? <ExpandLess /> : <ExpandMore />}
I haven't test it yet, but I guess it should work.
Related
I'm using this excellent example (Nested sidebar menu with material ui and Reactjs) to build a dynamic nested menu for my application. On top of that I'm trying to go one step further and put it into a Material UI appbar/temporary drawer. What I'd like to achieve is closing the drawer when the user clicks on one of the lowest level item (SingleLevel) however I'm having a tough time passing the toggleDrawer function down to the menu. When I handle the click at SingleLevel I consistently get a 'toggle is not a function' error.
I'm relatively new to this so I'm sure it's something easy and obvious. Many thanks for any answers/comments.
EDIT: Here's a sandbox link
https://codesandbox.io/s/temporarydrawer-material-demo-forked-v11ur
Code is as follows:
Appbar.js
export default function AppBar(props) {
const [drawerstate, setDrawerstate] = React.useState(false);
const toggleDrawer = (state, isopen) => (event) => {
if (event.type === 'keydown' && (event.key === 'Tab' || event.key === 'Shift')) {
return;
}
setDrawerstate({ ...state, left: isopen });
};
return (
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static" color="secondary">
<Toolbar>
<IconButton
size="large"
edge="start"
color="primary"
aria-label="menu"
onClick={toggleDrawer('left', true)}
>
<MenuIcon />
</IconButton>
<img src={logo} alt="logo" />
</Toolbar>
<Drawer
anchor='left'
open={drawerstate['left']}
onClose={toggleDrawer('left', false)}
>
<Box>
<AppMenu toggleDrawer={toggleDrawer} />
</Box>
</Drawer>
</AppBar>
</Box >
)
}
Menu.js
export default function AppMenu(props) {
return MenuItemsJSON.map((item, key) => <MenuItem key={key} item={item} toggleDrawer={props.toggleDrawer} />);
}
const MenuItem = ({ item, toggleDrawer }) => {
const MenuComponent = hasChildren(item) ? MultiLevel : SingleLevel;
return <MenuComponent item={item} toggleDrawer={toggleDrawer} />;
};
const SingleLevel = ({ item, toggleDrawer }) => {
const [toggle, setToggle] = React.useState(toggleDrawer);
return (
<ListItem button onClick={() => { toggle('left', false) }}>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.title} />
</ListItem>
);
};
const MultiLevel = ({ item }) => {
const { items: children } = item;
const [open, setOpen] = useState(false);
const handleClick = () => {
setOpen((prev) => !prev);
};
return (
<React.Fragment>
<ListItem button onClick={handleClick}>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.title} secondary={item.description} />
{open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{children.map((child, key) => (
<MenuItem key={key} item={child} />
))}
</List>
</Collapse>
</React.Fragment>
);
};
You shouldn't call a react hook inside of any function that is not a react component. Please see React Rules of Hooks
What you could do instead is pass setToggle directly into the Drawer component as a prop and do something like this for it's onClick attribute:
onClick={() => setToggle(<value>)}
React JS - Material UI ListItem (with Collapse API) onClick expand only one at a time close other. on click i need to open only one item at time. and when i click other the present opened should close.requirement is sidenavbar when i open click any menu , one should open . remaining or others should close
const isExpandable = items && items.length > 0;
const [open, setOpen] = useState(false);
const [menu, setMenu] = useState(false);
// const { value, setValue } = useContext(SiderBarContext)
const { expand, setExpand } = useContext(SiderBarContext)
function handleClick() {
// setOpen(!open);
setMenu(!menu)
setOpen(prevState => ({
open: !prevState
}))
// setValue(!open);
setExpand(true)
}
const MenuItemRoot = (
<>
{isExpandable ? <AppMenuItemComponent
className={isExpandable && menu ? classes.menuItemActiveMore : classes.menuItemActiveLess}
link={link}
onClick={handleClick}
>
{/* Display an icon if any */}
{!!Icon && (
<ListItemIcon className={classes.menuItemIcon}>
{<Icon />}
</ListItemIcon>
)}
<ListItemText primary={name} inset={!Icon} />
{/* Display the expand menu if the item has children */}
{isExpandable && !open && <ArrowDropDownIcon />}
{isExpandable && open && <ArrowDropDownIcon />}
</AppMenuItemComponent> : <AppMenuItemComponent
className={classes.menuItem}
link={link}
onClick={handleClick}
>
{/* Display an icon if any */}
{!!Icon && (
<ListItemIcon className={classes.menuItemIcon}>
{/* <Avatar alt="Remy Sharp" src={Icon} className={classes.iconImage}/> */}
{<Icon />}
</ListItemIcon>
)}
<ListItemText primary={name} inset={!Icon} />
{/* Display the expand menu if the item has children */}
{isExpandable && !open && <ArrowDropDownIcon />}
{isExpandable && open && <ArrowDropDownIcon />}
</AppMenuItemComponent>}
</>
);
const MenuItemChildren = isExpandable ? (
<Collapse className={classes.menuChildItem} in={open} timeout="auto" unmountOnExit>
{/* <Divider /> */}
<List component="div" disablePadding >
{items.map((item, index) => (
<>
<AppMenuItem {...item} key={index} />
</>
))}
</List>
</Collapse>
) : null;
return (
<>
{MenuItemRoot}
{MenuItemChildren}
</>
);
};
I am creating the following component:
It will contain an array of objects, where each object is a prescription, with the medicine name from the select and a TextField for the Dosis.
My problem is that the TextField loses focus on every onChange() and is very frustrating because it cannot be edited on a single focus.
This is my component :
const MedicineSelect = ({ medications, setMedications, ...props }) => {
const { medicines } = useMedicines()
const classes = useStyles()
const handleChange = (index, target) => {
// setAge(event.target.value)
const newMedications = cloneDeep(medications)
newMedications[index][target.name] = target.value
setMedications(newMedications)
}
const handleAddMedicine = () => {
const newMedications = cloneDeep(medications)
newMedications.push({ medicine: '', dosis: '', time: '' })
setMedications(newMedications)
}
const handleDeleteMedicine = (index) => {
console.log('DELETE: ', index)
const newMedications = cloneDeep(medications)
newMedications.splice(index, 1)
setMedications(newMedications)
}
return (
<Paper style={{ padding: 5 }}>
<List>
{medications.map((medication, index) => (
<ListItem key={nanoid()} divider alignItems='center'>
<ListItemIcon>
<Tooltip title='Eliminar'>
<IconButton
className={classes.iconButton}
onClick={() => handleDeleteMedicine(index)}
>
<HighlightOffOutlinedIcon />
</IconButton>
</Tooltip>
</ListItemIcon>
<FormControl className={classes.formControl}>
<InputLabel
id={`${index}-select-${medication}-label`}
>
Medicamento
</InputLabel>
<Select
labelId={`${index}-select-${medication}-label`}
id={`${index}-select-${medication}`}
name='medicine'
value={medication.medicine}
onChange={(event) =>
handleChange(index, event.target)
}
>
{medicines.map((medicine) => (
<MenuItem
key={nanoid()}
value={medicine.name}
>
{medicine.name}
</MenuItem>
))}
</Select>
</FormControl>
<TextField
// fullWidth
id={`${index}-text-${medication}`}
label='Dosis'
name='dosis'
onChange={(event) =>
handleChange(index, event.target)
}
value={medication.dosis}
/>
</ListItem>
))}
<Button onClick={handleAddMedicine}>+ agregar</Button>
</List>
</Paper>
)
}
And here is where I set the component:
const [medications, setMedications] = useState([
{ medicine: '', dosis: '', time: '' },
])
...
<Grid item md={12} xs={12}>
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls='panel1a-content'
id='panel1a-header'
>
<Typography variant='h4'>
Tratamiento:
</Typography>
</AccordionSummary>
<AccordionDetails>
<Container disableGutters>
<MedicineSelect
medications={medications}
setMedications={setMedications}
/>
</Container>
</AccordionDetails>
</Accordion>
</Grid>
...
Adding and removing objects from the array works perfect. selecting the medicine from the select, also works perfect. the only problem I have is when editing the Dosis TextField, with every character, the focus is lost and I have to click again on the TextField.
Please help me getting this fixed!!!
After searching a lot, finally I found the solution. Actually when using nanoid() to create unique keys, on every state update React re-renders all components and since the id of both the List and the TextField component are regenerated by nanoid on every render, React loses track of the original values, that is why Focus was lost.
What I did was keeping the keys unmuttable:
<ListItem key={`medication-${index}`} divider alignItems='center'>
and
<TextField
key={`dosis-${index}`}
fullWidth
// id={`${index}-dosis-${medication}`}
label='Dosis'
name='dosis'
onChange={(event) =>
handleChange(index, event.target)
}
value={medication.dosis}
/>
I'm trying to learn react and fairly new to the framework. I am trying to create a simple navbar component wih material-ui that is responsive (will show all links on medium devices and up, and open a side drawer on small devices). I have most of it setup to my liking, however, the issue I am currently having, is getting and setting the active link according to the page I am on.
It seems to works correctly on the medium devices and up, but when transitioning to a smaller device, the link is not updated correctly, as it will keep the active link from the medium screen set, while updating the side drawer active link.
Navbar.js
const Navbar = () => {
const classes = useStyles();
const pathname = window.location.pathname;
const path = pathname === '' ? '' : pathname.substr(1);
const [selectedItem, setSelectedItem] = useState(path);
const handleItemClick = (event, selected) => {
setSelectedItem(selected);
console.log(selectedItem);
};
return (
<>
<HideNavOnScroll>
<AppBar position="fixed">
<Toolbar component="nav" className={classes.navbar}>
<Container maxWidth="lg" className={classes.navbarDisplayFlex}>
<List>
<ListItem
button
component={RouterLink}
to="/"
selected={selectedItem === ''}
onClick={event => handleItemClick(event, '')}
>
<ListItemText className={classes.item} primary="Home" />
</ListItem>
</List>
<Hidden smDown>
<List
component="nav"
aria-labelledby="main navigation"
className={classes.navListDisplayFlex}
>
<ListItem
button
component={RouterLink}
to="/account/login"
selected={selectedItem === 'account/login'}
onClick={event => handleItemClick(event, 'account/login')}
>
<ListItemText className={classes.item} primary="Login" />
</ListItem>
<ListItem
button
component={RouterLink}
to="/account/register"
selected={selectedItem === 'account/register'}
onClick={event => handleItemClick(event, 'account/register')}
>
<ListItemText className={classes.item} primary="Register" />
</ListItem>
</List>
</Hidden>
<Hidden mdUp>
<SideDrawer />
</Hidden>
</Container>
</Toolbar>
</AppBar>
</HideNavOnScroll>
<Toolbar id="scroll-to-top-anchor" />
<ScrollToTop>
<Fab aria-label="Scroll back to top">
<NavigationIcon />
</Fab>
</ScrollToTop>
</>
)
}
SideDrawer.js
const SideDrawer = () => {
const classes = useStyles();
const [state, setState] = useState({ right: false });
const pathname = window.location.pathname;
const path = pathname === "" ? "" : pathname.substr(1);
const [selectedItem, setSelectedItem] = useState(path);
const handleItemClick = (event, selected) => {
setSelectedItem(selected);
console.log(selectedItem);
};
const toggleDrawer = (anchor, open) => (event) => {
if (
event &&
event.type === "keydown" &&
(event.key === "Tab" || event.key === "Shift")
) {
return;
}
setState({ ...state, [anchor]: open });
};
const drawerList = (anchor) => (
<div
className={classes.list}
role="presentation"
onClick={toggleDrawer(anchor, false)}
onKeyDown={toggleDrawer(anchor, false)}
>
<List component="nav">
<ListItem
button
component={RouterLink}
to="/account/login"
selected={selectedItem === "account/login"}
onClick={(event) => handleItemClick(event, "account/login")}
>
<ListItemText className={classes.item} primary="Login" />
</ListItem>
<ListItem
button
component={RouterLink}
to="/account/login"
selected={selectedItem === "account/register"}
onClick={(event) => handleItemClick(event, "account/register")}
>
<ListItemText className={classes.item} primary="Register" />
</ListItem>
</List>
</div>
);
return (
<React.Fragment>
<IconButton
edge="start"
aria-label="Menu"
onClick={toggleDrawer("right", true)}
>
<Menu fontSize="large" style={{ color: "white" }} />
</IconButton>
<Drawer
anchor="right"
open={state.right}
onClose={toggleDrawer("right", false)}
>
{drawerList("right")}
</Drawer>
</React.Fragment>
);
};
Code Sandbox - https://codesandbox.io/s/async-water-yx90j
I came across this question on SO: Is it possible to share states between components using the useState() hook in React?, which suggests that I need to lift the state up to a common ancestor component, but I don't quite understand how to apply this in my situation.
I would suggest to put aside for a moment your code and do a playground for this lifting state comprehension. Lifting state is the basic strategy to share state between unrelated components. Basically at some common ancestor is where the state and setState will live. there you can pass down as props to its children:
const Parent = () => {
const [name, setName] = useState('joe')
return (
<>
<h1>Parent Component</h1>
<p>Child Name is {name}</p>
<FirstChild name={name} setName={setName} />
<SecondChild name={name} setName={setName} />
</>
)
}
const FirstChild = ({name, setName}) => {
return (
<>
<h2>First Child Component</h2>
<p>Are you sure child is {name}?</p>
<button onClick={() => setName('Mary')}>My Name is Mary</button>
</>
)
}
const SecondChild = ({name, setName}) => {
return (
<>
<h2>Second Child Component</h2>
<p>Are you sure child is {name}?</p>
<button onClick={() => setName('Joe')}>My Name is Joe</button>
</>
)
}
As you can see, there is one state only, one source of truth. State is located at Parent and it passes down to its children. Now, sometimes it can be troublesome if you need your state to be located at some far GreatGrandParent. You would have to pass down each child until get there, which is annoying. if you found yourself in this situation you can use React Context API. And, for most complicated state management, there are solutions like redux.
I'm using React material framework in one of my projects. I'm trying to add multiple controlled tooltips which are going to be visible only when their respective state is visible.
Unfortunately, right now I'm stuck because I'm sharing the same state with multiple components hence all the tooltips are visible once you hover on any one of them. Is there any way to do so ? I think this can be done by array.
P.S there are going to be multiple parent components inside a page each having three set of tooltip i.e Edit, Delete, View
class ControlledTooltips extends React.Component {
state = {
open: false,
};
handleTooltipClose = () => {
this.setState({ open: false });
};
handleTooltipOpen = () => {
this.setState({ open: true });
};
render() {
return (
<div>
<Tooltip
enterDelay={300}
id="tooltip-controlled"
leaveDelay={300}
onClose={this.handleTooltipClose}
onOpen={this.handleTooltipOpen}
open={this.state.open}
placement="bottom"
title="Edit"
>
<IconButton aria-label="Delete">
<Edit />
</IconButton>
</Tooltip>
<Tooltip
enterDelay={300}
id="tooltip-controlled"
leaveDelay={300}
onClose={this.handleTooltipClose}
onOpen={this.handleTooltipOpen}
open={this.state.open}
placement="bottom"
title="view"
>
<IconButton aria-label="view">
<Visibility />
</IconButton>
</Tooltip>
<Tooltip
enterDelay={300}
id="tooltip-controlleded"
leaveDelay={300}
onClose={this.handleTooltipClose}
onOpen={this.handleTooltipOpen}
open={this.state.open}
placement="bottom"
title="Delete"
>
<IconButton aria-label="Delete">
<DeleteOutlined />
</IconButton>
</Tooltip>
</div>
);
}
}
codesandbox link
Any help will be appreciated :)
I would advise against complicating your component state too much. In my opinion, each component should control a very precise part of its state.
What I would recommend is to create a custom tooltip that will handle the state for each element. You can build your 3 Edit, Delete, View width them.
class TooltipCustom extends React.Component {
state = {
open: false
};
toggleState = () => {
this.setState({ open: !this.state.open });
};
render() {
return (
<IconButton aria-label={this.props.title}>
<Tooltip
enterDelay={300}
leaveDelay={300}
onClose={this.toggleState}
onOpen={this.toggleState}
open={this.state.open}
placement="bottom"
title={this.props.title}
>
{this.props.children}
</Tooltip>
</IconButton>
);
}
}
const Delete = () => (
<TooltipCustom title="delete">
<DeleteIcon />
</TooltipCustom>
);
const Edit = () => (
<TooltipCustom title="Edit">
<EditIcon />
</TooltipCustom>
);
const View = () => (
<TooltipCustom title="View">
<VisibilityIcon />
</TooltipCustom>
);
const ControlledTooltips = () => (
<div>
<Delete />
<Edit />
<View />
</div>
);
In case someone is looking for an answer. As suggested by #Einar Ólafsson, I made a custom tooltip wrapper which had all three tooltips inside it.
Tooltip name which was needed to be shown was passed to handleTooltipOpen() and handleTooltipClose() function. Inside this function, I changed the state of the individual tooltip.
class ControlledTooltips extends React.Component {
state = {
edit: false,
delete: false,
view: false
};
handleTooltipClose = (name) => {
this.setState({ [name]: false });
};
handleTooltipOpen = (name) => {
this.setState({ [name]: true });
};
render() {
return (
<div>
<Tooltip
id="tooltip-controlled-delete"
onClose={() => this.handleTooltipClose("delete")}
onOpen={() => this.handleTooltipOpen("delete")}
open={this.state.delete}
placement="bottom"
title="Delete"
>
<IconButton name="delete" aria-label="Delete">
<DeleteOutlined name="delete" />
</IconButton>
</Tooltip>
<Tooltip
id="tooltip-controlled-edit"
onClose={() => this.handleTooltipClose("edit")}
onOpen={() => this.handleTooltipOpen("edit")}
open={this.state.edit}
placement="bottom"
title="edit"
>
<IconButton name="edit" aria-label="edit">
<Edit />
</IconButton>
</Tooltip>
<Tooltip
id="tooltip-controlled-view"
onClose={() => this.handleTooltipClose("view")}
onOpen={() => this.handleTooltipOpen("view")}
open={this.state.view}
placement="bottom"
title="view"
>
<IconButton name="view" aria-label="view">
<Visibility />
</IconButton>
</Tooltip>
</div>
);
}
}