Expand collapse (material-ui) only one at a time - React - reactjs

I have table that I can expand Collapse material-ui. Everything is worked perfectly.
But there is one thing I want to update that when I click to expand there is only one expand collapse opened at a time.
const InvoiceInfo = ({el}) => {
const [open, setOpen] = React.useState(false)
}
function handleExpand() {
setOpen(!open)
}
return (
<>
<TableRow key={el.id}>
<TableCell>
{el.name}
</TableCell>
<TableCell>
<IconButton onClick={handleExpand}>
{open ? (
<KeyboardArrowUp fontSize="large" />
) : (
<KeyboardArrowDown fontSize="large" />
)}
</IconButton>
</TableCell>
</TableRow>
{
<TableRow>
<Collapse in={open} timeout="auto" unmountOnExit>
<div>
EXPAND ROW
</div>
</Collapse>
</TableRow>
}
</>
)
Above is my code looks like.
Any advice is very meaningful to me. Thank you so much.

You can use a simple state based on the index of an array or a given number / value:
const [isOpenCollapse, setIsOpenCollapse] = useState(null);
const handleOpen = (clickedIndex) => {
if (isOpenCollapse === clickedIndex) {
setIsOpenCollapse(null);
} else {
setIsOpenCollapse(clickedIndex);
}
};
[...]
return (
<List>
{arr.map((elm, index) => {
return (
<>
<ListItem key={elm.ID} onClick={() => handleOpen(index)} id="listItem">
{isOpenCollapse === index ? <ExpandLess /> : <ExpandMore />}
<Box>
<Typography>{elm.label}</Typography>
</Box>
</ListItem>
<Collapse in={isOpenCollapse === index} timeout="auto" unmountOnExit>
<p>PUT SOMETHING HERE</p>
</Collapse>
</>
);
})}
</List>;
)

Related

React: How to pass the data from function located on the child component into parent component?

I have a function called handleDelete that contains the id of specific task. I want to pass the value of that function to my parent component because this will be my reference for my prompt component. The prompt component contains the title of the task that's the reason that I want to get the task id.
Parent Component
const Home = ({ history }) => {
const [taskDeleteId, setDaskDeleteId] = useState();
const [showPrompt, setShowPrompt] = useState(false);
return (
<>
<Grid>
<TasksList deleteId={(deleteId) => setDaskDeleteId(deleteId)} />
<PromptComponent showPrompt={showPrompt} taskId={taskIdref} />
</Grid>
</>
);
};
export default Home;
Child Component
const TasksList = ({ taskId, deleteId }) => {
//Function
const handleDelete = (e) => {
deleteId = e.currentTarget.getAttribute('taskId');
setAssignTask(assignTask.filter((task) => task._id !== deleteId));
};
return (
<>
<Container
component='div'
maxWidth='xl'
className={classes.taskContainer}
>
<Container
component='div'
maxWidth='xl'
className={classes.todoContainer}
onDragOver={(e) => onDragOverTask(e)}
onDragLeave={(e) => onDragLeaveTask(e)}
onDrop={(e) => onDropTask(e)}
>
{taskTodo.map((currentTask, index) => {
return (
<Card
variant='outlined'
className={classes.cardTask}
key={index}
style={{ background: '#f3f3f3', marginTop: '1rem' }}
onDragStart={(e) => onDragStartTask(e, currentTask._id)}
draggable
>
<div className={classes.topContent}>
<CardActions
className={
classes[
currentTask.priority === 'High'
? 'checkBoxContainerHigh'
: currentTask.priority === 'Minor'
? 'checkBoxContainerMinor'
: currentTask.priority === 'Low'
? 'checkBoxContainerLow'
: 'checkBoxContainer'
]
}
>
<Checkbox
icon={<CircleUnchecked />}
checkedIcon={<CircleCheckedFilled />}
color='primary'
className={classes.completeAction}
id={currentTask._id}
checked={currentTask.isComplete}
onChange={handleCheckStatus}
/>
</CardActions>
<CardContent className={classes.cardContentTop}>
<Typography variant='h6'>{currentTask.title}</Typography>
<Typography variant='caption' color='textSecondary'>
<EventIcon />
{moment(currentTask.dateDue).format('dddd, Do MMMM')}
</Typography>
</CardContent>
{currentTask.desc && (
<CardActions disableSpacing style={{ marginLeft: 'auto' }}>
<IconButton
className={clsx(classes.expand, {
[classes.expandOpen]: expanded,
})}
onClick={handleChange(`todo-panel_${index}`)}
>
<ExpandMoreIcon />
</IconButton>
</CardActions>
)}
</div>
{currentTask.desc && (
<Collapse
in={expanded === `todo-panel_${index}`}
timeout='auto'
unmountOnExit
className={classes.collapsePanel}
color='primary'
>
<CardContent className={classes.descPrevContainer}>
<Typography
variant='body1'
className={classes.text}
dangerouslySetInnerHTML={createMarkup(
textTruncate(currentTask.desc, 50)
)}
></Typography>
</CardContent>
</Collapse>
)}
<CardActions
disableSpacing
className={classes.bottomActionsContainer}
>
<Tooltip title='Delete'>
<IconButton
aria-label='delete'
className={classes.BottomDelete}
taskid={currentTask._id}
onClick={handleDelete}
>
<DeleteIcon />
</IconButton>
</Tooltip>
<Tooltip title='Edit'>
<IconButton
aria-label='edit'
className={classes.BottomEdit}
taskid={currentTask._id}
>
<EditIcon />
</IconButton>
</Tooltip>
</CardActions>
</Card>
);
})}
</Container>
</Container>
</>
);
};
As a previous commenter mentioned, pass a callback down to the Child. Update your parent component as follows
const Home = ({ history }) => {
const [taskDeleteId, setDaskDeleteId] = useState();
const [showPrompt, setShowPrompt] = useState(false);
return (
<>
<Grid>
<TasksList deleteId={taskDeleteId} setDeleteId={setDaskDeleteId} />
<PromptComponent showPrompt={showPrompt} taskId={taskIdref} />
</Grid>
</>
);
};
Then in your child you can call the function passed from the parent to set the state properly.
const TasksList = ({ taskId, deleteId, setDeleteId }) => {
// Function
const handleDelete = (e) => {
setDeleteId(e.currentTarget.getAttribute('taskId'));
...
};
return (...

How to get the key on click event

I have the following react component:
type MobileNavProp = RouteTableProp
export default function MobileNav({routes}: MobileNavProp) {
const classes = useStyles()
const [drawerOpen, setDrawerOpen] = useState<boolean>(false)
const handleOnClickItem = (event: React.MouseEvent<HTMLDivElement>): void => console.log(event.target)
return (
<React.Fragment>
<IconButton
className={classes.icon}
color="inherit"
aria-label="Open navigation"
edge="end"
onClick={() => setDrawerOpen(true)}
>
<MenuRoundedIcon fontSize="large"/>
</IconButton>
<SwipeableDrawer open={drawerOpen}
onClose={() => setDrawerOpen(false)}
onOpen={() => console.log("Drawer is open")}
disableBackdropTransition={!iOS}
anchor="top"
disableDiscovery={iOS}
>
<List subheader={
<ListSubheader component="div" id="logo" className={classes.company}>
<img className={classes.logo} src={logo} alt="databaker-logo"/>
<Typography variant="h6" className={classes.title}>
DATABAKER
</Typography>
</ListSubheader>
}>
{
routes.map((rt, index) => (
<ListItem
divider
key={index}
button
onClick={handleOnClickItem}
>
<ListItemText primary={rt.name}/>
</ListItem>
))
}
</List>
</SwipeableDrawer>
</React.Fragment>
)
}
When the user click on ListItem(the handler function is handleOnClickItem), then it shows me:
<span class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock">ABOUT US</span>
However, I would like to get the clicked index that I have provided:
<ListItem
divider
key={index}...
How to get it?
You can make handleOnClickItem a higher-order function that takes the index as a parameter, and call it:
const makeHandleOnClickItem = (i: number) => (event: React.MouseEvent<HTMLDivElement>) => {
console.log(event.target);
console.log(i);
};
and change
onClick={handleOnClickItem}
to
onClick={makeHandleOnClickItem(index)}

Cannot read property map of undefined react

I'm trying to render a component that uses an array of filters to make a list. I pass the array as an argument to the function but it returns an error. The array is not null or undefined because it shows the itens if I log to the console.
Console.log returns:
Here is my code:
const List = (filtrosPorTipo) => {
let filtros = filtrosPorTipo[0]
let valoresDosFiltros = filtrosPorTipo[1]
let lista = (
<List>
{filtros.map((item, index) => {
return (
<>
<ListItem
button
onClick={() => setOpen({ [item]: !open[item] })}
key={item}
>
<ListItemText primary={item} />
{open[item] ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open[item]} timeout='auto'>
<List component='div' disablePadding>
{valoresDosFiltros[index].map((filtro) => {
return (
<>
<ListItem key={filtro}>
<p>{`${filtro}\n`}</p>
<ListItemSecondaryAction>
<Checkbox
label={filtro}
key={filtro.toString()}
color='primary'
onChange={() => handleChecked(filtro)}
checked={checked[filtro]}
/>
</ListItemSecondaryAction>
</ListItem>
<Divider />
</>
)
})}
</List>
</Collapse>
</>
)
})}
</List>
)
return lista
}
Error:
Perhaps the component it's trying to render before "filtros" is assigned. It's a common and logical behavior in React.
Try adding a conditional before the .map() call:
{filtros ? filtros.map((item, index) => {
return (
<>
<ListItem
button
onClick={() => setOpen({ [item]: !open[item] })}
key={item}
>
<ListItemText primary={item} />
{open[item] ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open[item]} timeout='auto'>
<List component='div' disablePadding>
{valoresDosFiltros[index].map((filtro) => {
return (
<>
<ListItem key={filtro}>
<p>{`${filtro}\n`}</p>
<ListItemSecondaryAction>
<Checkbox
label={filtro}
key={filtro.toString()}
color='primary'
onChange={() => handleChecked(filtro)}
checked={checked[filtro]}
/>
</ListItemSecondaryAction>
</ListItem>
<Divider />
</>
)
})}
</List>
</Collapse>
</>
)
}) : null}
You can map over the values when they are present like so. If you are not planning on displaying a Loading screen in the process of waiting for the data then this would work. Otherwise use a ternary operator like the other answer.
{filtros && filtros.map((item, index) => {
return (
<>
<ListItem
button
onClick={() => setOpen({ [item]: !open[item] })}
key={item}
>
<ListItemText primary={item} />
{open[item] ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open[item]} timeout='auto'>
<List component='div' disablePadding>
{valoresDosFiltros[index].map((filtro) => {
return (
<>
<ListItem key={filtro}>
<p>{`${filtro}\n`}</p>
<ListItemSecondaryAction>
<Checkbox
label={filtro}
key={filtro.toString()}
color='primary'
onChange={() => handleChecked(filtro)}
checked={checked[filtro]}
/>
</ListItemSecondaryAction>
</ListItem>
<Divider />
</>
)
})}
</List>
</Collapse>
</>
)
})}
While rendering the component you have to check data in filtrosPorTipo. It has an array value or not. Like below:
const List = (filtrosPorTipo) => {
if (filtrosPorTipo && filtrosPorTipo.length > 0) {
let filtros = filtrosPorTipo[0]
let valoresDosFiltros = filtrosPorTipo[1]
let lista = (
<List>
{filtros.map((item, index) => {
return (
<>
<ListItem
button
onClick={() => setOpen({ [item]: !open[item] })}
key={item}
>
<ListItemText primary={item} />
{open[item] ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open[item]} timeout='auto'>
<List component='div' disablePadding>
{valoresDosFiltros[index].map((filtro) => {
return (
<>
<ListItem key={filtro}>
<p>{`${filtro}\n`}</p>
<ListItemSecondaryAction>
<Checkbox
label={filtro}
key={filtro.toString()}
color='primary'
onChange={() => handleChecked(filtro)}
checked={checked[filtro]}
/>
</ListItemSecondaryAction>
</ListItem>
<Divider />
</>
)
})}
</List>
</Collapse>
</>
)
})}
</List>
)
return lista
}
return 'No data available!'
}

categories and sub categories in ReactJS

Hello I'm trying to make a sidebar menu using material ui
This sidebar menu contain categories and there sub categories.
I have code this:
What I'm trying to make ?
I take some categories data from my Laravel API, on each categories they can have an object of sub categories, and I collapse this to show the sub categories from the parent category
If I collapse one every one collapse, this is normal because they share the same state, but I don't know how to make one state for each parent categories
const categoriesList = this.state.categories.map((item, k) => {
return (
<React.Fragment key={k}>
<ListItem button onClick={this.handleClick}>
<ListItemText primary={item.label} />
{item.category !== null ? this.state.open ? <ExpandLess /> : <ExpandMore /> : null}
</ListItem>
{item.category !== null ? <>
<Collapse in={this.state.open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{item.category.map((ite,l) => <ListItem key={l} button className={classes.nested}><ListItemText primary={ite.label} /></ListItem>)}
</List>
</Collapse>
</> : null}
</React.Fragment>
)
})
``
I would recommend that you create a component for each category. So instead you would have:
const categoriesList = this.state.categories.map((item, k) => {
return <Category key={k} {...item} />;
});
And elsewhere in your code you'd have another Component:
const Category = (props) => {
const [open, setOpen] = useState(false);
function handleClick(e) { ... }
return (
<React.Fragment>
<ListItem button onClick={handleClick}>
<ListItemText primary={props.label} />
{props.category !== null ? open ? <ExpandLess /> : <ExpandMore /> : null}
</ListItem>
{item.category !== null ? <>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{props.category.map((ite,l) => <ListItem key={l} button className={classes.nested}><ListItemText primary={ite.label} /></ListItem>)}
</List>
</Collapse>
</> : null}
</React.Fragment>
);
}
Then the state is contained with the Category component, and you can keep your container component simple.
[note] I prefer function components, so used that here, but a Class Component wouldn't change this answer.
This is my parent component with hook
first time I use hooks
import React, {useState} from 'react';
import ListItem from "#material-ui/core/ListItem";
import ListItemText from "#material-ui/core/ListItemText";
import ExpandLess from "#material-ui/icons/ExpandLess";
import ExpandMore from "#material-ui/icons/ExpandMore";
import Collapse from "#material-ui/core/Collapse";
import List from "#material-ui/core/List";
export function Category(props){
const [open, setOpen] = useState(false);
let handleClick = () => {
setOpen(!open)
}
return (
<React.Fragment key={props.id}>
<ListItem button onClick={handleClick}>
<ListItemText primary={props.categorie.label} />
{props.category !== null ? open ? <ExpandLess /> : <ExpandMore /> : null}
</ListItem>
{props.category !== null ? <>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{props.category.map((ite,l) => <ListItem key={l} button className={props.classes.nested}><ListItemText primary={ite.label} /></ListItem>)}
</List>
</Collapse>
</> : null}
</React.Fragment>
)
}

How to fix shadow for menu material-ui

When I use a few icon menus than box-shadow looks very dark. How to fix that?
1:
Codesandbox example https://codesandbox.io/embed/flamboyant-tdd-r83u1
<div>
{items.map((item, index) => {
return (
<Fragment key={index}>
<IconButton
aria-owns={open ? "long-menu" : undefined}
onClick={this.handleClick}
>
<MoreVertIcon />
</IconButton>
<Menu anchorEl={anchorEl} open={open} onClose={this.handleClose}>
{options.map(option => (
<MenuItem key={option} onClick={this.handleClose}>
{option}
</MenuItem>
))}
</Menu>
</Fragment>
);
})}
</div>
Because, actually you are triggering multiple menus with the same flag at the same time. So shadow is dark because there are multiple menus one after the other.
Below code should fix this, You don't have to render Menu in items loop
render() {
const items = [...Array(10).keys()];
const { anchorEl } = this.state;
const open = Boolean(anchorEl);
return (
<div>
{items.map((item, index) => {
return (
<Fragment key={index}>
<IconButton
aria-owns={open ? "long-menu" : undefined}
onClick={this.handleClick}
>
<MoreVertIcon />
</IconButton>
</Fragment>
);
})}
<Menu anchorEl={anchorEl} open={open} onClose={this.handleClose}>
{options.map(option => (
<MenuItem key={option} onClick={this.handleClose}>
{option}
</MenuItem>
))}
</Menu>
</div>
);
}

Resources