I'm hoping to set up protected routes for my webapp built with React.js. I have an array 'menuOptions' that I have been looping through to add options to my navbar. However, I've now been running in to issues as I would like to add protected routes.
So for instance, I would like to have Home, Login and Register when the user is logged out, and Dashboard and Logout as the options for when the user is logged in. I'd really appreciate any help. Ideally, I'd somehow add the '!user' to the menuOptions array somehow.
My code is below (as you can see menuOptions does not do anything for the above at the moment):
import React, { useState, useContext } from "react";
import { Link, NavLink, useNavigate } from "react-router-dom";
import { useAuthState } from "react-firebase-hooks/auth";
import { auth, logout } from "../../firebase";
import { makeStyles } from "#material-ui/core/styles";
import AppBar from "#material-ui/core/AppBar";
import Toolbar from "#material-ui/core/Toolbar";
import Typography from "#material-ui/core/Typography";
import IconButton from "#material-ui/core/IconButton";
import MenuIcon from "#material-ui/icons/Menu";
import MenuItem from "#material-ui/core/MenuItem";
import Menu from "#material-ui/core/Menu";
import { useTheme } from "#material-ui/core/styles";
import useMediaQuery from "#material-ui/core/useMediaQuery";
const useStyles = makeStyles((theme) => ({
title: {
flexGrow: 1,
},
appbar: {
// background: 'none',
},
inactiveLink: {
color: "white",
padding: theme.spacing(1),
fontSize: "1.5rem",
},
activeLink: {
color: "black",
padding: theme.spacing(1),
fontSize: "1.5rem",
background: "#bfbfbf",
},
}));
function Navigation() {
const classes = useStyles();
const [anchorEl, setAnchorEl] = useState(null);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md"));
const open = Boolean(anchorEl);
const menuOptions = [
{ label: "Home", path: "/", },
{ label: "Register", path: "/register" },
{ label: "Dashboard", path: "/dashboard" },
{ label: "Logout", path: "/logout" },
];
const handleMenuSelect = (pageURL) => {
navigate(pageURL);
};
const handleMenu = (event) => {
setAnchorEl(event.currentTarget);
};
const [user] = useAuthState(auth);
const navigate = useNavigate();
return (
<>
<AppBar
className={classes.appbar}
position="fixed"
elevation={0}
color="primary"
>
<Toolbar>
<Typography variant="h4" className={classes.title}>
The Football App
</Typography>
<Typography variant="h6" className={classes.title}>
All you ever wanted to know about Football!
</Typography>
{isMobile ? (
<>
<IconButton
aria-label="menu"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={handleMenu}
color="inherit"
>
<MenuIcon />
</IconButton>
<Menu
id="menu-appbar"
anchorEl={anchorEl}
anchorOrigin={{
vertical: "top",
horizontal: "right",
}}
keepMounted
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
open={open}
onClose={() => setAnchorEl(null)}
>
{menuOptions.map((opt) => (
<MenuItem
key={opt.label}
onClick={() => handleMenuSelect(opt.path)}
>
{opt.label}
</MenuItem>
))}
</Menu>
</>
) : (
<>
<nav>
<div>
{!user ? (
<NavLink
to="/"
className={({ isActive }) =>
isActive
}
>
Home
</NavLink>
) : (
<NavLink
to="/dashboard"
className={({ isActive }) =>
isActive
}
>
Dashboard
</NavLink>
)}
</div>
{!user ? (
<div>
<Link to="/login">
<a>Login</a>
</Link>
<Link to="/register">
<a>Register</a>
</Link>
</div>
) : (
<div className="navbar-end">
<Link to="/logout">
<a>Log Out</a>
</Link>
</div>
)}
</nav>
</>
)}
</Toolbar>
</AppBar>
</>
);
}
export default Navigation;
You could update your menuOptions object to include a protected key:
const menuOptions = [
{ label: "Home", path: "/", protected: false },
{ label: "Register", path: "/register", protected: false },
{ label: "Dashboard", path: "/dashboard", protected: false },
{ label: "Logout", path: "/logout", protected: true },
];
and update the logic where your render your menu options to something like
{menuOptions.map((opt) => {
if (opt.protected && !user) return;
return (
<MenuItem
key={opt.label}
onClick={() => handleMenuSelect(opt.path)}
>
{opt.label}
</MenuItem>
);
}
)}
to only show certain options if the user exists
{menuOptions.filter((opt) => (user ? opt.protected : !opt.protected)).map((opt) => (
<MenuItem
key={opt.label}
onClick={() => handleMenuSelect(opt.path)}
>
{opt.label}
</MenuItem>
)
)}
Related
I am trying to build a hamburger menu using Material UI List on Next.js similar to Nested List example on Material UI docs. However, I am keep getting error if I use "collapse" on my code. If I remove collapse then the other parts of the code works fine.
Can anyone tell me what am I doing wrong?
screenshot of an error message
I am trying to build similar to this
import * as React from "react";
import { useState } from "react";
import IconButton from "#mui/material/IconButton";
import ListSubheader from "#mui/material/ListSubheader";
import { Drawer } from "#mui/material";
import List from "#mui/material/List";
import ListItem from "#mui/material/ListItem";
import ListItemButton from "#mui/material/ListItemButton";
import ListItemIcon from "#mui/material/ListItemIcon";
import ListItemText from "#mui/material/ListItemText";
import MenuIcon from "#mui/icons-material/Menu";
import { ExpandLess, ExpandMore } from "#mui/icons-material";
import Collapse from "#mui/material";
const myMenu = [
{
title: "about",
},
{
title: "services",
submenu: [{ title: "Digital Marketing" }, { title: "Promotion" }],
},
{
title: "blog",
},
{
title: "contact",
},
];
const ResponsiveAppBar = () => {
const [openDrawer, setOpenDrawer] = useState(false);
const [open, setOpen] = React.useState(false);
const handleClick = () => {
setOpen(!open);
};
return (
<React.Fragment>
<Drawer
anchor="right"
open={openDrawer}
onClose={() => setOpenDrawer(false)}
>
<List
sx={{ width: "100%", maxWidth: 360, bgcolor: "background.paper" }}
component="nav"
aria-labelledby="nested-list-subheader"
subheader={
<ListSubheader component="div" id="nested-list-subheader">
Navigation
</ListSubheader>
}
>
{myMenu.map((page, index) =>
page.submenu ? (
<div>
<ListItemButton key={index} onClick={handleClick}>
<ListItemText>{page.title}</ListItemText>
{open ? <ExpandLess /> : <ExpandMore />}
</ListItemButton>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{page.submenu.map((sub, index) => {
return (
<ListItemButton key={index}>
<ListItem key={index}>
<ListItemText>{sub.title}</ListItemText>
</ListItem>
</ListItemButton>
);
})}
</List>
</Collapse>
</div>
) : (
<ListItemButton key={index}>
<ListItemIcon>
<ListItemText>{page.title}</ListItemText>
</ListItemIcon>
</ListItemButton>
)
)}
</List>
</Drawer>
<IconButton
sx={{ color: "black", marginLeft: "auto" }}
onClick={() => setOpenDrawer(!openDrawer)}
>
<MenuIcon color="white" />
</IconButton>
</React.Fragment>
);
};
export default ResponsiveAppBar;
The culprit is an incorrect import of Collapse:
import Collapse from "#mui/material";
Since Collapse is a named import, you must import it in one of these two ways:
import { Collapse } from "#mui/material";
// Alternatively
import Collapse from "#mui/material/Collapse";
As you can see from the image I have a page where,
the part of the drawer and the dark mode change is found on the parent page.
Where the word Page1 and the input field appear, in the child page.
When the theme is changed, then switched to dark mode, a prop with the darkState state is passed from parent to child.
As you can see from the image if I have an input field in which I am writing, so with some text, then I switch to dark mode or I open the drawer.
The component updates everything, losing all its internal state.
I thought about using useMemo, but I don't know where I should use it.
Can you give me a hand?
Link: https://codesandbox.io/s/competent-sara-dru7w?file=/src/page/Page1.js
App.js
import React from "react";
import PropTypes from "prop-types";
import { Switch, Route, Link, useLocation } from "react-router-dom";
import {
AppBar,
CssBaseline,
Drawer,
Hidden,
IconButton,
List,
ListItem,
ListItemIcon,
ListItemText,
Toolbar,
Chip
} from "#material-ui/core";
import { GTranslate, Menu } from "#material-ui/icons";
import {
makeStyles,
useTheme,
createMuiTheme,
ThemeProvider
} from "#material-ui/core/styles";
import { blue, grey } from "#material-ui/core/colors";
import DarkModeToggle from "react-dark-mode-toggle";
import { Page1, Page2, Error } from "./page";
import "./styles/main.css";
import "./App.css";
const drawerWidth = 240;
function App(props) {
const { wind } = props;
const container = wind !== undefined ? () => wind().document.body : undefined;
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const localDark = localStorage.getItem("dark");
const isDark = localDark === null ? prefersDark : localDark === "true";
let location = useLocation();
let pathname = location.pathname.replace("/", "");
if (pathname === "") pathname = "page1";
const [state, setState] = React.useState({
mobileOpen: false,
darkState: isDark,
repo: []
});
const { mobileOpen, darkState } = state;
const useStyles = makeStyles((theme) => ({
root: {
display: "flex"
},
drawer: {
[theme.breakpoints.up("sm")]: {
width: drawerWidth,
flexShrink: 0
}
},
appBar: {
[theme.breakpoints.up("sm")]: {
width: `calc(100% - ${drawerWidth}px)`,
marginLeft: drawerWidth
}
},
menuButton: {
marginRight: theme.spacing(2),
[theme.breakpoints.up("sm")]: {
display: "none"
},
backgroundColor: darkState ? grey[900] : blue[500]
},
// necessary for content to be below app bar
toolbar: theme.mixins.toolbar,
drawerPaper: {
width: drawerWidth,
color: "#ffffff",
backgroundColor: darkState ? grey[900] : blue[500]
},
content: {
flexGrow: 1,
padding: theme.spacing(3)
}
}));
const palletType = darkState ? "dark" : "light";
const mainPrimaryColor = darkState ? grey[900] : blue[500];
const mainSecondaryColor = darkState ? grey[800] : blue[300];
const darkTheme = createMuiTheme({
palette: {
type: palletType,
primary: {
main: mainPrimaryColor
},
secondary: {
main: mainSecondaryColor
}
}
});
const classes = useStyles();
const theme = useTheme();
const handleDrawerToggle = () =>
setState((prev) => ({ ...prev, mobileOpen: !mobileOpen }));
const changePage = (page) => setState((prev) => ({ ...prev, page }));
const handleThemeChange = React.useCallback(() => {
localStorage.setItem("dark", !darkState);
setState((prev) => ({ ...prev, darkState: !prev.darkState }));
}, []);
const menu = [
{ title: "Page1", path: "page1", icon: <GTranslate /> },
{ title: "Page2", path: "page2", icon: <GTranslate /> }
];
const routeObj = [
{ path: "/", obj: <Page1 darkState={darkState} /> },
{ path: "page1", obj: <Page1 darkState={darkState} /> },
{ path: "page2", obj: <Page2 darkState={darkState} /> }
];
const drawer = (
<div className="mt-32">
<div className={classes.toolbar} />
<List>
{menu.map(({ title, path, icon, badge }, index) => (
<Link to={`/${path}`} key={title}>
<ListItem button key={title} onClick={() => changePage(path)}>
<ListItemIcon
style={{ color: path === pathname ? "#ffffff" : "#ffffff80" }}
>
{icon}
</ListItemIcon>
<ListItemText
primary={<span className="font-bold">{title}</span>}
style={{ color: path === pathname ? "#ffffff" : "#ffffff80" }}
/>
{badge && (
<Chip
label={badge}
size="small"
color="secondary"
className="font-bold"
style={{ color: "#ffffff" }}
/>
)}
</ListItem>
</Link>
))}
</List>
</div>
);
return (
<ThemeProvider theme={darkTheme}>
<div className={classes.root}>
<CssBaseline />
<AppBar
position="fixed"
className={classes.appBar}
style={{
backgroundColor: darkState ? "#303030" : grey[50],
boxShadow: "none"
}}
>
<Toolbar className={"shadow-none"}>
<IconButton
color="inherit"
aria-label="open drawer"
edge="start"
onClick={handleDrawerToggle}
className={classes.menuButton}
>
<Menu />
</IconButton>
<div className="ml-auto text-right flex">
<DarkModeToggle
onChange={handleThemeChange}
checked={darkState}
size={60}
/>
</div>
</Toolbar>
</AppBar>
<nav className={classes.drawer} aria-label="mailbox folders">
{/* The implementation can be swapped with js to avoid SEO duplication of links. */}
<Hidden smUp implementation="css">
<Drawer
container={container}
variant="temporary"
anchor={theme.direction === "rtl" ? "right" : "left"}
open={mobileOpen}
onClose={handleDrawerToggle}
classes={{
paper: classes.drawerPaper
}}
ModalProps={{
keepMounted: true // Better open performance on mobile.
}}
>
{drawer}
</Drawer>
</Hidden>
<Hidden xsDown implementation="css">
<Drawer
classes={{
paper: classes.drawerPaper
}}
variant="permanent"
open
>
{drawer}
</Drawer>
</Hidden>
</nav>
<main className={classes.content}>
<div className={classes.toolbar} />
<Switch>
{routeObj.map(({ path, obj }, key) => (
<Route exact path={`/${path}`} component={() => obj} key={key} />
))}
<Route component={() => <Error darkState={darkState} />} />
</Switch>
</main>
</div>
</ThemeProvider>
);
}
App.propTypes = {
/**
* Injected by the documentation to work in an iframe.
* You won't need it on your project.
*/
wind: PropTypes.func
};
export default App;
Page1.js
import React, { useState, useEffect } from "react";
import { TextField, makeStyles } from "#material-ui/core";
import { className } from "../function";
import "../styles/main.css";
export default function Page1({ darkState }) {
const useStyles = makeStyles((theme) => ({
title: {
color: darkState ? "#ffffff" : "#343a40",
textShadow: `3px 3px 2px ${
darkState ? "rgba(0, 0, 0, 1)" : "rgba(150, 150, 150, 1)"
}`
},
button: {
margin: theme.spacing(1)
}
}));
const classes = useStyles();
const [state, setState] = useState({
name: ""
});
const { name } = state;
useEffect(() => {
console.log(darkState, state);
}, []);
useEffect(() => {
console.log("darkState", darkState, state);
}, [darkState]);
const onChange = ({ target: { value } }, name) => {
setState((prev) => ({ ...prev, [name]: value }));
};
console.log(state);
return (
<>
<h1 className={className(classes.title, "text-6xl font-bold hp")}>
Page
<span className="text-primary">1</span>
</h1>
<div
style={{
width: "50%",
minHeight: "600px"
}}
>
<div style={{ paddingBottom: 15 }}>
<TextField
fullWidth
id="outlined-basic"
label={"Name"}
variant="outlined"
size="small"
value={name}
onChange={(value) => onChange(value, "name")}
/>
</div>
</div>
</>
);
}
I am building a reactjs application -- and I am trying to make this popup component modular -so that I could have the button look like a badge/icon combination -- activate the popup menu on hover instead of clicks.
here is a sandbox -- but I need to create popover menus for each -- at the moment its displacing the buttons.
https://codesandbox.io/s/material-demo-forked-wrn2g?file=/demo.js
Here is the component as it is currently
http://jsfiddle.net/4benm6wo/
import React from 'react';
import Button from '#material-ui/core/Button';
import ClickAwayListener from '#material-ui/core/ClickAwayListener';
import Grow from '#material-ui/core/Grow';
import Paper from '#material-ui/core/Paper';
import Popper from '#material-ui/core/Popper';
import MenuItem from '#material-ui/core/MenuItem';
import MenuList from '#material-ui/core/MenuList';
import Badge from '#material-ui/core/Badge';
import PersonIcon from '#material-ui/icons/Person';
import './PopOverMenu.scss';
export default function MenuListComposition() {
const [open, setOpen] = React.useState(false);
const anchorRef = React.useRef(null);
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
const handleClose = (event) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setOpen(false);
};
function handleListKeyDown(event) {
if (event.key === 'Tab') {
event.preventDefault();
setOpen(false);
}
}
// return focus to the button when we transitioned from !open -> open
const prevOpen = React.useRef(open);
React.useEffect(() => {
if (prevOpen.current === true && open === false) {
anchorRef.current.focus();
}
prevOpen.current = open;
}, [open]);
return (
<div className="popover-menu">
<div>
<Button
ref={anchorRef}
aria-controls={open ? 'menu-list-grow' : undefined}
aria-haspopup="true"
onClick={handleToggle}
>
<Badge badgeContent={11} color="primary">
<PersonIcon />
</Badge>
</Button>
<Popper open={open} anchorEl={anchorRef.current} role={undefined} transition disablePortal>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{ transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom' }}
>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MenuList autoFocusItem={open} id="menu-list-grow" onKeyDown={handleListKeyDown}>
<MenuItem onClick={handleClose}>Profile</MenuItem>
<MenuItem onClick={handleClose}>My account</MenuItem>
<MenuItem onClick={handleClose}>Logout</MenuItem>
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</div>
</div>
);
}
but I want to create a popperMenu component -- where I push the icon, badge count into the poppup -- so I need help implement it with props and states.
this is my current attempt
http://jsfiddle.net/4benm6wo/1/
class MenuListComposition extends Component {
constructor(props, context) {
super(props, context);
this.state = { open: false };
}
render() {
return (
<div className="popover-menu">
<div>
<Button
ref={anchorRef}
aria-controls={open ? 'menu-list-grow' : undefined}
aria-haspopup="true"
onClick={handleToggle}
>
<Badge badgeContent={11} color="primary">
<PersonIcon />
</Badge>
</Button>
<Popper open={open} anchorEl={anchorRef.current} role={undefined} transition disablePortal>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{ transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom' }}
>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MenuList autoFocusItem={open} id="menu-list-grow" onKeyDown={handleListKeyDown}>
<MenuItem onClick={handleClose}>Profile</MenuItem>
<MenuItem onClick={handleClose}>My account</MenuItem>
<MenuItem onClick={handleClose}>Logout</MenuItem>
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</div>
</div>
)
}
}
export default MenuListComposition;
so I would create a button/badge popup kind of like this from the shell
<MenuListComposition badgeCount={10} icon={<PersonIcon />} menu={[{ "label": "Profile", "link": "user/1" }, { "label": "Logout", "link": "logout" }]} />
latest code
LoginButton.js
http://jsfiddle.net/z3L89x2e/
import React, { Component } from 'react'
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import Button from '#material-ui/core/Button';
import PopOverMenu from '../_SharedGlobalComponents/PopOverMenu/PopOverMenu';
import MailIcon from '#material-ui/icons/Mail';
import NotificationsIcon from '#material-ui/icons/Notifications';
import PersonIcon from '#material-ui/icons/Person';
class LoggedInButtons extends Component {
/*
constructor(props, context) {
super(props, context);
}
*/
render() {
return (
<div className="login-badges">
<PopOverMenu
icon={<NotificationsIcon />}
badgecount="3"
menu={[
{ "label": "xxx", "value": "/" },
{ "label": "xxxx", "value": "/" },
{ "label": "xxxxx", "value": "/" },
]}
/>
<PopOverMenu
icon={<MailIcon />}
badgecount="5"
menu={[
{ "label": "xxx", "value": "/" },
{ "label": "xxxx", "value": "/" },
{ "label": "xxxxx", "value": "/" },
]}
/>
<PopOverMenu
icon={<PersonIcon />}
badgecount="2"
menu={[
{ "label": "Profile", "value": "/profile" },
{ "label": "My account", "value": "/my-account" },
{ "label": "Logout", "value": "/logout" },
]}
/>
<Button
variant="text"
color="default"
startIcon={<PersonIcon />}
href="/user/view/2"
>
User 2
</Button>
<Button variant="contained" color="secondary" href="/logout">
log out
</Button>
</div>
)
}
}
function mapStateToProps(state) {
return {
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ }, dispatch);
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(LoggedInButtons))
PopOverMenu.js
http://jsfiddle.net/z3L89x2e/2/
import React, { Component } from 'react';
import Button from '#material-ui/core/Button';
import ClickAwayListener from '#material-ui/core/ClickAwayListener';
import Grow from '#material-ui/core/Grow';
import Paper from '#material-ui/core/Paper';
import Popper from '#material-ui/core/Popper';
import MenuItem from '#material-ui/core/MenuItem';
import MenuList from '#material-ui/core/MenuList';
import Badge from '#material-ui/core/Badge';
//import './PopOverMenu.scss';
class PopOverMenu extends Component {
constructor(props, context) {
super(props, context);
this.state = { open: false };
this.anchorRef = React.createRef(null);
}
handleToggle = () => {
this.setState({open: !this.state.open});
};
handleClose = () => {
this.setState({open: false});
};
handleListKeyDown = (event) => {
if (event.key === 'Tab') {
event.preventDefault();
this.setState({open: false});
}
}
showMenuItems = () => (
this.props.menu.map((item, i) => (
<MenuItem onClick={this.handleClose}>{item.label}</MenuItem>
))
)
render() {
return (
<div style={{display:'inline', float:'left', marginLeft:'20px', marginTop:'10px'}} className="popover-menu">
<Button
ref={this.anchorRef}
aria-controls={this.state.open ? 'menu-list-grow' : null}
aria-haspopup="true"
onClick={this.handleToggle}
>
<Badge badgeContent={this.props.badgecount} color="primary">
{this.props.icon}
</Badge>
</Button>
<Popper style={{position: 'relative'}} open={this.state.open} role={undefined} transition disablePortal>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{ transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom' }}
>
<Paper>
<ClickAwayListener onClickAway={this.handleClose}>
<MenuList autoFocusItem={this.state.open} id="menu-list-grow" onKeyDown={this.handleListKeyDown}>
{this.showMenuItems()}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</div>
);
}
}
export default PopOverMenu;
this is the latest attempt - it nearly worked - but then I got errors in the ancor el
I am using material ui and the icon/badge modules. I started to get an error on hovering over the menu.
I've tried to follow something like this - of putting the ancor el into the state - but its not working.
How can I convert popover MATERIAL-UI functional component to class based component?
import React, { Component } from 'react';
import { NavLink } from 'react-router-dom';
import Button from '#material-ui/core/Button';
import ClickAwayListener from '#material-ui/core/ClickAwayListener';
import Grow from '#material-ui/core/Grow';
import Paper from '#material-ui/core/Paper';
import Popper from '#material-ui/core/Popper';
import MenuItem from '#material-ui/core/MenuItem';
import MenuList from '#material-ui/core/MenuList';
import Badge from '#material-ui/core/Badge';
import './PopOverMenu.scss';
class PopOverMenu extends Component {
constructor(props, context) {
super(props, context);
// this.state = { open: false};
// this.anchorRef = React.createRef(null);
this.state = { anchorEl: null, open: false };
}
handleToggle = (event) => {
this.setState({open: !this.state.open});
// this.state.ancherEl ? this.setState({ anchorEl: null }) : this.setState({ anchorEl: event.currentTarget });
};
handleOpen = (event) => {
this.setState({open: true});
console.log("event.currentTarget", event.currentTarget);
this.state.ancherEl ? this.setState({ anchorEl: null }) : this.setState({ anchorEl: event.currentTarget });
};
handleClose = () => {
this.setState({open: false});
//this.setState({ anchorEl: null })
};
handleListKeyDown = (event) => {
if (event.key === 'Tab') {
event.preventDefault();
this.setState({open: false});
}
}
showMenuItems = () => (
this.props.menu.map((item, i) => (
<MenuItem key={i} onClick={this.handleClose}>
<NavLink to={item.value}>{item.label}</NavLink>
</MenuItem>
))
)
render() {
//console.log("this.anchorRef", this.anchorRef)
return (
<div className="popover-menu">
<Button
//ref={this.anchorRef}
aria-controls={this.state.open ? 'menu-list-grow' : null}
aria-haspopup="true"
onClick={this.handleToggle}
onMouseOver={this.handleOpen}
onMouseLeave={this.handleClose}
>
<Badge badgeContent={this.props.badgecount} color="primary">
{this.props.icon}
</Badge>
</Button>
<Popper
className="popper-list"
//anchorEl={this.anchorRef}
open={this.state.open}
anchorEl={this.state.anchorEl}
//role={undefined}
transition
disablePortal
onMouseOver={this.handleOpen}
onMouseLeave={this.handleClose}
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{ transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom' }}
>
<Paper>
<ClickAwayListener onClickAway={this.handleClose}>
<MenuList autoFocusItem={this.state.open} id="menu-list-grow" onKeyDown={this.handleListKeyDown}>
{/*this.showMenuItems()*/}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</div>
);
}
}
export default PopOverMenu;
.popover-menu{
width: 40px;
display: inline;
float: left;
//border: 1px solid red;
//background: blue;
padding: 5px;
.popper-list{
width: 160px;
position: relative!important;
//border: 1px solid blue;
}
}
Here is a working CodeSandbox
You are using the wrong ref.
You need to assign the ref to the Popper on click (see line 89).
To be able to do this, you need to set the ref using setState and not createRef.
for hover activation: You need to toggle it using onMouseEnter and onMouseLeave on parent div
Add onClick on button to toggle the popper
https://material-ui.com/components/lists. I follow this instruction. I created a drawer using the Material UI react. I want a link is active so background change. Nested link work but single routes not working. I am so confuse
My Menu Component.
import React from 'react';
import ExpandLess from '#material-ui/icons/ExpandLess';
import ExpandMore from '#material-ui/icons/ExpandMore';
import ListItem from '#material-ui/core/ListItem';
import ListItemIcon from '#material-ui/core/ListItemIcon';
import ListItemText from '#material-ui/core/ListItemText';
import Collapse from '#material-ui/core/Collapse';
import { NavLink } from "react-router-dom";
function Menu(props) {
const objs = props.objs;
const index = props.index
const [open, setOpen] = React.useState(false);
const [selectedIndex, setSelectedIndex] = React.useState(1);
function handleListItemClick(event, index) {
setSelectedIndex(index);
console.log(index);
}
function click(){
setOpen(!open)
console.log('i ma ss')
}
function handler(obj , index) {
if (obj.children) {
// console.log(obj.children)
return (
// Sub Routing
<React.Fragment>
<ListItem key={obj.name} button onClick={ click }>
<ListItemIcon>
{obj.icon}
</ListItemIcon>
<ListItemText primary={obj.name} />
{open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
{obj.children.map((data , arr) => {
return <NavLink style={{textDecoration : 'none' , color : 'black'}} key={data.name} to={data.link}>
<ListItem selected={selectedIndex === data.name ? true : false}
onClick={event => handleListItemClick(event, data.name)}
button>
<ListItemIcon>
<div></div>
</ListItemIcon>
<ListItemText primary={data.name} />
</ListItem>
</NavLink>
})}
</Collapse>
</React.Fragment>
)
} else {
return <NavLink style={{textDecoration : 'none' , color : 'black'}} to={obj.link}>
<ListItem onClick={ click }
selected={selectedIndex === obj.name ? true : false}
onClick={event => handleListItemClick(event, obj.name)}
button>
<ListItemIcon>
{obj.icon}
</ListItemIcon>
<ListItemText primary={obj.name} />
</ListItem>
</NavLink>
}
};
return handler(objs);
};
export default Menu;
My drawer Component.
import React, { useState } from 'react';
import clsx from 'clsx';
import { BrowserRouter, Route ,Switch } from "react-router-dom";
import { makeStyles } from '#material-ui/core/styles';
import Drawer from '#material-ui/core/Drawer';
import List from '#material-ui/core/List';
import CssBaseline from '#material-ui/core/CssBaseline';
import Divider from '#material-ui/core/Divider';
import IconButton from '#material-ui/core/IconButton';
import ChevronLeftIcon from '#material-ui/icons/ChevronLeft';
import ChevronRightIcon from '#material-ui/icons/ChevronRight';
import Navbar from '../../Components/NavBarComponent/navbar.component'
import NoPageFound from '../../Components/NoPageFound/NoPageFound';
import Setup from "./../setup/setup"
import LoginPage from '../LoginPage/LoginPage';
import Menu from '../../Components/SideMenu/SideMenu.Component';
import DonutSmall from '#material-ui/icons/DonutSmall';
import FindReplace from '#material-ui/icons/PermIdentity';
import SettingsInputComponent from '#material-ui/icons/SettingsInputComponent';
import FolderOpen from '#material-ui/icons/FolderOpen';
import CollectionsBookmark from '#material-ui/icons/CollectionsBookmark';
import Dashboard from '#material-ui/icons/Dashboard';
import workflowPage from '../WorkFlow/WorkflowPage'
import Header from '../../Components/Header/Header.Component'
const drawerWidth = 240;
const useStyles = makeStyles(theme => ({
root: {
display: 'flex',
},
drawer: {
width: drawerWidth,
flexShrink: 0,
whiteSpace: 'nowrap',
},
drawerOpen: {
width: 240,
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
drawerClose: {
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) + 1,
},
},
toolbar: {
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
padding: '0 8px',
...theme.mixins.toolbar,
},
content: {
flexGrow: 1,
},
_padding: {
padding: theme.spacing(2),
}
}));
export default function Home(props) {
// Styling using jss
const classes = useStyles();
//State
const [open, setOpen] = useState(false);
// Define Routes
const routes = [
{
name : 'Dashboard' ,
link : 'Dashboard',
icon : <Dashboard />
},
{
name : 'Work Flow' ,
link : 'workflow',
icon : <DonutSmall />
},
{
name : 'Setups',
icon : <SettingsInputComponent />,
children : [
{
name : 'Organization' ,
link : '/organization',
icon : 'dropdown'
},
{
name : 'Department' ,
link : '/department',
icon : 'dropdown'
},
{
name : 'Section' ,
link : '/section',
icon : 'dropdown'
},
{
name : 'Location' ,
link : '/location',
icon : 'dropdown'
},
]
},
{
name : 'Documents',
link : 'documents',
icon : <FolderOpen />
},
{
name : 'Users',
icon : <FindReplace />,
children : [
{
name : 'Organization' ,
link : '/organization',
icon : 'dropdown'
},
{
name : 'Department' ,
link : '/department',
icon : 'dropdown'
},
{
name : 'Section' ,
link : '/section',
icon : 'dropdown'
},
{
name : 'Location' ,
link : '/location',
icon : 'dropdown'
},
]
} ,
{
name : 'Manual Record',
link : 'manualrecord',
icon : <FindReplace />
},
{
name : 'Report' ,
link : 'report',
icon : <CollectionsBookmark />
}];
//Drawer Open Close Functiion
function handleDrawer() {
setOpen(!open);
};
console.log(props)
return (
<BrowserRouter>
{true ?
<div className={classes.root}>
<CssBaseline />
{/* Drawer */}
<Drawer
variant="permanent"
className={clsx(classes.drawer, {
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
})}
classes={{
paper: clsx({
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
}),
}}
open={open}
>
<div className={classes.toolbar}>
<IconButton onClick={handleDrawer}>
{open ? <ChevronLeftIcon /> : <ChevronRightIcon />}
</IconButton>
</div>
<Divider />
<List>
{routes.map((obj , index) => {
return <Menu objs={obj} key={index} index={index} />
})}
</List>
</Drawer>
{/* Content Display */}
<main className={classes.content}>
<Navbar />
<Header heading="Dashboard" />
<div>
<Switch>
<Route path="/workflow" component={workflowPage} />
<Route component={NoPageFound} />
</Switch>
</div>
</main>
</div> : <LoginPage />}
</BrowserRouter>
);
};
I have an application has been written with React + Redux and Antdesign. My application is a dashboard app. So I used the layout in Ant design https://ant.design/components/layout/
When I click on side menus, the active menu gets bold which is fine. But I need when I refresh the page, it checks and detect the route and bold related menu item.
I have a Sidebar component which is stateful. Inside it, in componentDidMount I call a function which will dispatch an action from mapDispatchToProps. The reducer changes the state. But in HTML codes, in defaultSelectedKeys, I can not set the number of active menus.
Sidebar.js component:
import React from 'react';
import { render } from 'react-dom';
import { connect } from 'react-redux'
import { Switch, BrowserRouter, Route, Link } from 'react-router-dom';
// antd
import { Layout, Breadcrumb, Menu, Icon } from 'antd';
const { Header, Content, Footer, Sider } = Layout;
// Helpers
import { Alert } from '../helpers/notifications';
// Components
import Home from '../components/Home';
// import Header from '../components/Header';
import NotFound from '../components/NotFound';
import PostsEditor from '../components/Posts/PostsEditor';
// Actions
import { setRouteActiveFlag } from '../actions/ui.action'
class Sidebar extends React.Component {
componentDidMount () {
const routes = {
'/' : 1,
'/posts' : 2,
'/logout' : 3
}
this.props.detectActiveRoute(setRouteActiveFlag({
routes:routes,
path:window.location.pathname
}))
}
render() {
const { selectedRoute } = this.props;
console.log(selectedRoute);
return (
<div>
<Layout>
<Sider
style={{
overflow: 'auto',
height: '100vh',
position: 'fixed',
left: 0,
}}
breakpoint="lg"
collapsedWidth="0"
onBreakpoint={broken => {
console.log(broken);
}}
onCollapse={(collapsed, type) => {
console.log(collapsed, type);
}}
>
<div className="logo" >
Logo <br/><br/><br/>
</div>
<Menu theme="dark" mode="inline" style={{ lineHeight: '64px' }} defaultSelectedKeys={[selectedRoute.toString() || '1']}>
<Menu.Item key="1">
<Link to="/" style={{ color:'#fff' }}>
<Icon type="user" />
<span className="nav-text">Home</span>
</Link>
</Menu.Item>
<Menu.Item key="2">
<Link to="/posts" style={{ color:'#fff' }}>
<Icon type="user" />
<span className="nav-text">Posts</span>
</Link>
</Menu.Item>
<Menu.Item key="3">
<a href="/logout" style={{ color:'#fff' }}>
<Icon type="user" />
<span className="nav-text">Logout</span>
</a>
</Menu.Item>
</Menu>
</Sider>
<Layout style={{ marginLeft: 200 }}>
<Content style={{ margin: '24px 16px 0', overflow: 'initial'}}>
<Breadcrumb style={{ margin: '0 0 20px 0' }}>
<Breadcrumb.Item>Home</Breadcrumb.Item>
<Breadcrumb.Item>List</Breadcrumb.Item>
<Breadcrumb.Item>App</Breadcrumb.Item>
</Breadcrumb>
<div style={{ padding: 24, background: '#fff', minHeight: 360 }}>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/posts/:id?" component={PostsEditor} />
<Route component={NotFound}/>
</Switch>
<Alert stack={ { limit: 3 } } />
</div>
</Content>
<Footer style={{ textAlign: 'center' }}>Ant Design ©2018 Created by Ant UED</Footer>
</Layout>
</Layout>
</div>
);
}
}
const mapStateToProps = (state, ownProps) => {
return {
state: state,
props: ownProps,
selectedRoute:state.ui.selectedRoute || 1
}
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
detectActiveRoute: (obj) => dispatch(obj)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Sidebar)
ui.action.js
export const setRouteActiveFlag = (payload = 'global') => ({
type: actions.SET_ROUTE_ACTIVE_FLAG,
payload
});
ui.reducer.js
import { handleActions } from 'redux-actions';
import Immutable from 'seamless-immutable';
import * as actions from '../consts/action-types';
const initialState = Immutable({
requests: {},
selectedRoute:{}
});
export default handleActions({
[actions.SET_ROUTE_ACTIVE_FLAG]: (state, action) => {
if (action.payload.routes && action.payload.path && action.payload.routes[ action.payload.path ]) {
return state.set('selectedRoute', action.payload.routes[ action.payload.path ])
}else{
return state.set('selectedRoute', 1)
}
}
}, initialState);
Please help me find the best and simple practices.
There is no need to use redux, just use react-router to get current pathname and pass it to defaultSelectedKeys.
<Menu defaultSelectedKeys={[this.props.location.pathname]}>
...
.....
</Menu>
Look at this answer , if you don't know how to get pathname
The following answer assumes you are using hooks. I know that from your question you are not using hooks, but it could be useful for other people. This answer works not only when refreshing but also when pressing the back and forward buttons:
import React, { useState, useEffect } from 'react'
import { useHistory, useLocation } from 'react-router-dom'
import { Layout, Menu } from 'antd'
const { Sider } = Layout
const items = [
{ key: '1', label: 'Invoices', path: '/admin/invoices' },
{ key: '2', label: 'Service Details', path: '/admin/service-details' },
{ key: '3', label: 'Service Contract Details', path: '/admin/service-contract-details' },
{ key: '4', label: 'Cost Centers', path: '/admin/cost-centers' },
{ key: '5', label: 'Clients', path: '/admin/clients' },
{ key: '6', label: 'Vendors', path: '/admin/vendors' }
]
const Sidebar = () => {
const location = useLocation()
const history = useHistory()
const [selectedKey, setSelectedKey] = useState(items.find(_item => location.pathname.startsWith(_item.path)).key)
const onClickMenu = (item) => {
const clicked = items.find(_item => _item.key === item.key)
history.push(clicked.path)
}
useEffect(() => {
setSelectedKey(items.find(_item => location.pathname.startsWith(_item.path)).key)
}, [location])
return (
<Sider style={{ backgroundColor: 'white' }}>
<h3 style={{ paddingLeft: '1rem', paddingTop: '1rem', fontSize: '1.25rem', fontWeight: 'bold', minHeight: 64, margin: 0 }}>
Costek
</h3>
<Menu selectedKeys={[selectedKey]} mode='inline' onClick={onClickMenu}>
{items.map((item) => (
<Menu.Item key={item.key}>{item.label}</Menu.Item>
))}
</Menu>
</Sider>
)
}
export default Sidebar
Your sidebar will look as follows:
You can add any css in your menu by conditioning and adding a class just in this way.
<MenuItem className={ (this.props.location.pathname==='/yourRoute')? 'active' : '' } >
</MenuItem>
In case if you get any kind of undefined error then you can use the 'withRouter' HOC
in this way.
In your component where you want to get that location prop, you will first import
import {withRouter} from 'react-router-dom';
then you can export it in this way.
export default withRouter(YourComponent);
Final code can look somewhat similar to this
import React, {Fragment, Component} from 'react';
import {withRouter, Link } from 'react-router-dom';
class Menu extends Component {
render(){
const {pathname} = this.props.location;
return (
<Fragment>
<div id="sidebar-menu" className="sidebar-menu">
<ul>
<li className={(pathname==='/dashboard' || pathname==='/')?'active':''}>
<Link to="/dashboard">Dashboard</Link>
</li>
<li className={(pathname==='/properties')?'active':''}>
<Link to="/properties">Properties</Link>
</li>
</ul>
</div>
</Fragment>
);
}
}
export default withRouter(Menu);