I would like to set up a sidebar with the possibility to collapse it to have only the sidebar icons.
I use Material-UI, I managed to set up the basic sidebar (Mini Variant Drawer) and it works fine. However, I can't keep the state (boolean) when I change the page or when I refresh the page I'm on.
Does anyone know how I can do this?
function:
const [open, setOpen] = useState(true)
const handleDrawerOpen = () => {
setOpen(true);
};
const handleDrawerClose = () => {
setOpen(false);
};
Full code:
import React, { useState } from "react";
import clsx from "clsx";
import { makeStyles } from "@material-ui/core/styles";
import Drawer from "@material-ui/core/Drawer";
// import List from '@material-ui/core/List';
import Divider from "@material-ui/core/Divider";
import ListItem from "@material-ui/core/ListItem";
import ListItemText from "@material-ui/core/ListItemText";
import { Link as Route, NavLink, withRouter } from "react-router-dom";
import styled, { createGlobalStyle } from "styled-components/macro";
import {
Chip,
Collapse,
List as MuiList,
ListItemIcon,
Tooltip,
Typography,
} from "@material-ui/core";
import { darken, rgba } from "polished";
import { ExpandLess, ExpandMore } from "@material-ui/icons";
import PropTypes from "prop-types";
import {
sidebarMainRoutes as mainRoutes,
sidebarShortRoutes as shortRoutes,
} from "../../routes";
import ReactRouterPropTypes from "react-router-prop-types";
import desktopLogo from "../../assets/images/logos/data_is_logo.png";
import mobileLogo from "../../assets/images/logos/data_is_logo_icon.png";
import { CertificationIcon, LogOutIcon } from "../Icons/Icons";
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
import ChevronRightIcon from "@material-ui/icons/ChevronRight";
import CertificationModal from "../../pages/CertificationPage/CertificationModal";
import LogOutModal from "../../pages/AuthPages/components/LogOutModal";
const drawerWidth = 240;
const GlobalStyle = createGlobalStyle`
.MuiListItem-gutters {
padding-left: 8px !important;
padding-right: 10px !important;
}
`;
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
},
appBar: {
zIndex: theme.zIndex.drawer 1,
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
},
appBarShift: {
marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`,
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
menuButton: {
marginRight: 36,
},
hide: {
display: "none",
},
drawer: {
width: drawerWidth,
flexShrink: 0,
whiteSpace: "nowrap",
borderRight: "1px solid #F4F4F4",
},
drawerOpen: {
width: drawerWidth,
borderRight: "1px solid #F4F4F4",
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
drawerClose: {
borderRight: "1px solid #F4F4F4",
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
overflowX: "hidden",
width: theme.spacing(7) 1,
[theme.breakpoints.up("sm")]: {
width: theme.spacing(9) 20,
},
},
toolbar: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: theme.spacing(0, 1),
// necessary for content to be below app bar
...theme.mixins.toolbar,
},
}));
const List = styled(MuiList)`
background-color: ${(props) => props.theme.sidebar.background};
padding: ${(props) => props.theme.spacing(2)}px !important;
`;
const Items = styled.div`
padding-top: ${(props) => props.theme.spacing(2.5)}px;
padding-bottom: ${(props) => props.theme.spacing(2.5)}px;
`;
const Category = styled(ListItem)`
padding-top: ${(props) => props.theme.spacing(3)}px;
padding-bottom: ${(props) => props.theme.spacing(3)}px;
padding-left: ${(props) => props.theme.spacing(8)}px;
padding-right: ${(props) => props.theme.spacing(7)}px;
font-weight: ${(props) => props.theme.typography.fontWeightRegular};
border-radius: 10px !important;
svg {
color: ${(props) => props.theme.sidebar.color};
font-size: 20px;
width: 20px;
height: 20px;
}
&:hover {
background-color: ${(props) =>
props.theme.palette.customer.blue} !important;
color: ${(props) => props.theme.palette.primary.contrastText} !important;
border-radius: 10px !important;
svg {
color: ${(props) => props.theme.palette.common.white};
}
span {
color: ${(props) => props.theme.palette.common.white};
}
}
&.${(props) => props.activeClassName} {
background-color: ${(props) =>
darken(0.03, props.theme.palette.customer.blue)};
color: ${(props) => props.theme.palette.primary.contrastText} !important;
svg {
color: ${(props) => props.theme.palette.common.white};
}
span {
color: ${(props) => props.theme.palette.common.white};
}
}
`;
const CategoryText = styled(ListItemText)`
margin: 0;
span {
color: ${(props) => props.theme.sidebar.color};
font-size: ${(props) => props.theme.typography.body1.fontSize}px;
}
`;
const CategoryIconLess = styled(ExpandLess)`
color: ${(props) => rgba(props.theme.sidebar.color, 0.5)};
`;
const CategoryIconMore = styled(ExpandMore)`
color: ${(props) => rgba(props.theme.sidebar.color, 0.5)};
`;
const Link = styled(ListItem)`
padding-left: ${(props) => props.theme.spacing(17.5)}px;
padding-top: ${(props) => props.theme.spacing(2)}px;
padding-bottom: ${(props) => props.theme.spacing(2)}px;
span {
color: ${(props) => rgba(props.theme.sidebar.color, 0.7)};
}
&:hover span {
color: ${(props) => rgba(props.theme.sidebar.color, 0.9)};
}
&:hover {
background-color: ${(props) =>
darken(0.015, props.theme.sidebar.background)};
}
&.${(props) => props.activeClassName} {
background-color: ${(props) =>
darken(0.03, props.theme.sidebar.background)};
span {
color: ${(props) => props.theme.sidebar.color};
}
}
`;
const LinkText = styled(ListItemText)`
color: ${(props) => props.theme.sidebar.color};
span {
font-size: ${(props) => props.theme.typography.body1.fontSize}px;
}
margin-top: 0;
margin-bottom: 0;
`;
const LinkIcon = styled(ListItemIcon)`
color: ${(props) => props.theme.sidebar.color};
`;
const LinkBadge = styled(Chip)`
font-size: 11px;
font-weight: ${(props) => props.theme.typography.fontWeightBold};
height: 20px;
position: absolute;
right: 28px;
top: 8px;
background: ${(props) => props.theme.sidebar.badge.background};
span.MuiChip-label,
span.MuiChip-label:hover {
cursor: pointer;
color: ${(props) => props.theme.sidebar.badge.color};
padding-left: ${(props) => props.theme.spacing(2)}px;
padding-right: ${(props) => props.theme.spacing(2)}px;
}
`;
const CategoryBadge = styled(LinkBadge)`
top: 12px;
`;
const SidebarSection = styled(Typography)`
color: ${(props) => props.theme.sidebar.color};
padding: ${(props) => props.theme.spacing(4)}px
${(props) => props.theme.spacing(4)}px
${(props) => props.theme.spacing(2)}px;
opacity: 0.9;
display: block;
font-size: 20px !important;
`;
const Brand = styled(ListItem)`
font-size: ${(props) => props.theme.typography.h5.fontSize};
font-weight: ${(props) => props.theme.typography.fontWeightMedium};
color: ${(props) => props.theme.sidebar.header.color};
background-color: ${(props) => props.theme.sidebar.header.background};
font-family: ${(props) => props.theme.typography.fontFamily};
min-height: 56px;
padding-left: ${(props) => props.theme.spacing(6)}px;
padding-right: ${(props) => props.theme.spacing(6)}px;
justify-content: center;
cursor: pointer;
${(props) => props.theme.breakpoints.up("sm")} {
min-height: 64px;
}
&:hover {
background-color: ${(props) => props.theme.sidebar.header.background};
}
`;
const StyledDrawer = styled(Drawer)`
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
}
::-webkit-scrollbar-thumb {
background-color: ${(props) => props.theme.palette.primary.main};
outline: 1px solid ${(props) => props.theme.palette.primary.main};
}
`;
const SidebarCategory = (props) => {
const { name, icon, isOpen, isCollapsable, badge, ...rest } = props;
return (
<Category {...rest}>
<Tooltip title={name} placement="right">
<ListItemIcon>{icon}</ListItemIcon>
</Tooltip>
<CategoryText>{name}</CategoryText>
{isCollapsable ? (
isOpen ? (
<CategoryIconMore />
) : (
<CategoryIconLess />
)
) : null}
{badge ? <CategoryBadge label={badge} /> : ""}
</Category>
);
};
const SidebarLink = ({ name, to, badge, icon }) => {
return (
<Link
button
dense
component={NavLink}
exact
to={to}
activeClassName="active"
>
<Tooltip title={name} placement="right">
<LinkIcon>{icon}</LinkIcon>
</Tooltip>
<LinkText>{name}</LinkText>
{badge ? <LinkBadge label={badge} /> : ""}
</Link>
);
};
const Sidebar = ({
onDrawerChange,
staticContext,
location,
addModal,
addLogOutModal,
...rest
}) => {
const initOpenRoutes = () => {
/* Open collapse element that matches current url */
const pathName = location.pathname;
let _routes = {};
mainRoutes.forEach((route, index) => {
const isActive = pathName.indexOf(route.path) === 0;
const isOpen = route.open;
const isHome = route.containsHome && pathName === "/";
_routes = Object.assign({}, _routes, {
[index]: isActive || isOpen || isHome,
});
});
return _routes;
};
const [openRoutes, setOpenRoutes] = useState(() => initOpenRoutes());
const classes = useStyles();
const [open, setOpen] = React.useState(true);
const toggle = (index) => {
// Collapse all elements
Object.keys(openRoutes).forEach(
(item) =>
openRoutes[index] ||
setOpenRoutes((openRoutes) =>
Object.assign({}, openRoutes, { [item]: false })
)
);
// Toggle selected element
setOpenRoutes((openRoutes) =>
Object.assign({}, openRoutes, { [index]: !openRoutes[index] })
);
};
const handleDrawerOpen = () => {
setOpen(true);
};
const handleDrawerClose = () => {
setOpen(false);
};
const [isOpen, setIsOpen] = useState(false);
const [isOpenLogoutDialog, setIsOpenLogoutDialog] = useState(false);
const handleDialogCertificationOpen = () => {
setIsOpen(true);
};
const handleDialogCertificationClose = () => {
setIsOpen(false);
};
const handleDialogLogOutOpen = () => {
setIsOpenLogoutDialog(true);
};
const handleDialogLogOutClose = () => {
setIsOpenLogoutDialog(false);
};
return (
<React.Fragment>
<GlobalStyle />
<CertificationModal
isOpen={isOpen}
handleClose={handleDialogCertificationClose}
handleModalAction={() => console.log("Handle Modal Action")}
/>
<LogOutModal
isOpen={isOpenLogoutDialog}
handleClose={handleDialogLogOutClose}
handleModalAction={() => console.log("Déconnexion")}
/>
<StyledDrawer
variant="permanent"
className={clsx(classes.drawer, {
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
})}
classes={{
paper: clsx({
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
}),
}}
{...rest}
>
<Brand>
{open === true ? (
<Route to="/">
<img src={desktopLogo} alt="" width={180} />
</Route>
) : (
<Route to="/">
<img src={mobileLogo} alt="" width={35} />
</Route>
)}
</Brand>
<Divider />
<List disablePadding>
<Items>
{mainRoutes.map((category, index) => (
<React.Fragment key={index}>
{category.children && category.icon ? (
<React.Fragment key={index}>
<SidebarCategory
isOpen={!openRoutes[index]}
isCollapsable={true}
name={category.id}
icon={category.icon}
button={true}
onClick={() => toggle(index)}
/>
<Collapse
in={openRoutes[index]}
timeout="auto"
unmountOnExit
>
{category.children.map((route, index) => (
<SidebarLink
key={index}
name={route.name}
to={route.path}
icon={route.icon}
badge={route.badge}
/>
))}
</Collapse>
</React.Fragment>
) : category.icon ? (
<SidebarCategory
isCollapsable={false}
name={category.id}
to={category.path}
activeClassName="active"
component={NavLink}
icon={category.icon}
exact
button
badge={category.badge}
/>
) : null}
</React.Fragment>
))}
</Items>
</List>
<Divider />
{open && <SidebarSection>Raccourcis</SidebarSection>}
<List disablePadding>
<Items>
{shortRoutes.map((category, index) => (
<React.Fragment key={index}>
{category.children && category.icon ? (
<React.Fragment key={index}>
<SidebarCategory
isOpen={!openRoutes[index]}
isCollapsable={true}
name={category.id}
icon={category.icon}
button={true}
onClick={() => toggle(index)}
/>
<Collapse
in={openRoutes[index]}
timeout="auto"
unmountOnExit
>
{category.children.map((route, index) => (
<SidebarLink
key={index}
name={route.name}
to={route.path}
icon={route.icon}
badge={route.badge}
/>
))}
</Collapse>
</React.Fragment>
) : category.icon ? (
<SidebarCategory
isCollapsable={false}
name={category.id}
to={category.path}
activeClassName="active"
component={NavLink}
icon={category.icon}
exact
button
badge={category.badge}
/>
) : null}
</React.Fragment>
))}
<SidebarCategory
icon={<CertificationIcon />}
name="Ajouter attestation"
onClick={handleDialogCertificationOpen}
/>
<SidebarCategory
icon={<LogOutIcon />}
name="Déconnexion"
onClick={handleDialogLogOutOpen}
/>
{open === true ? (
<SidebarCategory
icon={<ChevronLeftIcon />}
name="Réduire le menu"
onClick={handleDrawerClose}
/>
) : (
<SidebarCategory
icon={<ChevronRightIcon />}
name="Agrandir le menu"
onClick={handleDrawerOpen}
/>
)}
</Items>
</List>
</StyledDrawer>
</React.Fragment>
);
};
export default withRouter(Sidebar);
SidebarCategory.propTypes = {
name: PropTypes.string,
icon: PropTypes.node,
isOpen: PropTypes.bool,
isCollapsable: PropTypes.bool,
badge: PropTypes.node,
};
SidebarLink.propTypes = {
name: PropTypes.string,
to: PropTypes.string,
badge: PropTypes.node,
icon: PropTypes.node,
};
Sidebar.propTypes = {
onDrawerChange: PropTypes.func,
classes: PropTypes.node,
staticContext: PropTypes.node,
location: ReactRouterPropTypes.location,
addModal: PropTypes.func,
addLogOutModal: PropTypes.func,
};
CodePudding user response:
In order to persist some state across pages, you'll need to lift up your state to a common ancestor. So, if you're using Sidebar
on several pages, you'll need to set the open state in the component which renders your page components, and pass the state down from there.
In order to persist state between page reloads, you can use localStorage or a cookie.
CodePudding user response:
For simple data like a boolean state, you can use local storage to persist it:
const drawerOpenKey = 'drawerOpen';
const defaultOpen = localStorage.getItem(drawerOpenKey) === 'true';
const [open, setOpen] = React.useState(defaultOpen);
React.useEffect(() => {
localStorage.setItem(drawerOpenKey, open);
}, [open]);