Material-ui adding Link component from react-router - javascript

I'm struggling to add <Link/> component to my material-ui AppBar
This is my navigation class:
class Navigation extends Component {
constructor(props) {
super(props)
}
render() {
var styles = {
appBar: {
flexWrap: 'wrap'
},
tabs: {
width: '100%'
}
}
return (
<AppBar showMenuIconButton={false} style={styles.appBar}>
<Tabs style={styles.tabs}>
<Tab label='Most popular ideas'/>
<Tab label='Latest ideas' />
<Tab label='My ideas' />
</Tabs>
</AppBar>
)
}
}
Which looks okay:
Tabs are clickable, have fluid animations, that's cool. But how do I wire them up together with react-router and its' <Link/> component?
I've tried adding onChange listener like that:
<Tab
label='My ideas'
onChange={<Link to='/myPath'></Link>}
/>
However I'm getting following error:
Uncaught Invariant Violation: Expected onChange listener to be a function, instead got type object
If I try to wrap <Tab/> component into <Link/> component, I'm getting error that <Tabs/> component accepts only <Tab/> component.
This doesn't work either (no error is being produced, but clicking on Tab does not bring me to the path):
<Tab label='Most popular ideas'>
<Link to='/popular'/>
</Tab>
How do I make <Link/> component work together with <Tabs> and <AppBar>? If that's not possible, I can use any other component from material-ui library to form a proper menu.

For Material UI 1.0 with Typescript: see this post by #ogglas below.
For Material-UI 1.0 with plain JS:
<Tabs value={value} onChange={this.handleChange}>
{
this.props.tabs.map(
({label, path})=><Tab key={label}
label={label}
className={classes.tabLink}
component={Link}
to={path} />
)
}
</Tabs>
And classes.tabLink is defined as:
tabLink : {
display:"flex",
alignItems:"center",
justifyContent:"center"
}
How this works?
All the mui 1.0 components inheriting from ButtonBase, support a component prop, see ButtonBase. The idea is to allow you to control what the component renders as its wrapper/root element. Tab also has this feature although at the time of writing this answer this prop is not documented explicitly, but as Tab inherits from ButtonBase, all its props carry over (and the documentation does cover this).
Another feature of ButtonBase is that all the extra props, not in use by ButtonBase or inherited component, are spread over the specified component. We have used this behavior to send the to prop used by Link by giving it to Tab control. You can send any additional props in the same way. Note that this is documented explicitly for both ButtonBase and Tab.
Thanks #josh-l for asking this to be added.

here's how you can do it now:
<Tabs onChange={this.changeTab} value={value}>
<Tab value={0} label="first" containerElement={<Link to="/first"/>} />
<Tab value={1} label="second" containerElement={<Link to="/second"/>}/>
<Tab value={2} label="third" containerElement={<Link to="/third"/>} />
</Tabs>

You can try this simple method
<Tab label='Most popular ideas' to='/myPath' component={Link} />

This is solved using the <Link /> from material-ui instead of directly using the <Link /> or <NavLink /> from react-router. The example for the same can be found in the documentation here.
https://material-ui.com/components/links/
Also <Button /> tag has a component prop to achieve this
<Button color="inherit" component={Link} to={"/logout"}>Logout</Button>
An extensive discussion on this can be found here
https://github.com/mui-org/material-ui/issues/850

Since we are using TypeScript I could not use #hazardous solutions. This is how we implemented routing for material-ui v1.0.0-beta.16 and react-router 4.2.0. The reason why we are splitting this.props.history.location.pathname is because we need to access /renewals/123 for example. If we did not do this we would get the following warning and no tab would be displayed as active: Warning: Material-UI: the value provided '/renewals/123' is invalid
Complete code with imports:
import * as React from "react";
import * as ReactDOM from "react-dom";
import * as ReactRouter from "react-router";
import * as PropTypes from "prop-types";
import { Switch, Route, Redirect, Link } from "react-router-dom";
import { Cases } from './../Cases';
import { SidePane } from './../SidePane';
import { withStyles, WithStyles } from 'material-ui/styles';
import Paper from 'material-ui/Paper';
import Tabs, { Tab } from 'material-ui/Tabs';
import { withRouter } from "react-router-dom";
import Badge from 'material-ui/Badge';
import Grid from 'material-ui/Grid';
import { Theme } from 'material-ui/styles';
import SimpleLineIcons from '../../Shared/SimpleLineIcons'
interface IState {
userName: string;
}
interface IProps {
history?: any
}
const styles = (theme: Theme) => ({
root: theme.typography.display1,
badge: {
right: '-28px',
color: theme.palette.common.white,
},
imageStyle:{
float: 'left',
height: '40px',
paddingTop: '10px'
},
myAccount: {
float: 'right'
},
topMenuAccount: {
marginLeft: '0.5em',
cursor: 'pointer'
}
});
type WithStyleProps = 'root' | 'badge' | 'imageStyle' | 'myAccount' | 'topMenuAccount';
class Menu extends React.Component<IProps & WithStyles<WithStyleProps>, IState> {
constructor(props: IProps & WithStyles<WithStyleProps>) {
super(props);
this.state = {
userName: localStorage.userName ? 'userName ' + localStorage.userName : ""
}
}
componentDidMount() {
this.setState({ userName: localStorage.userName ? localStorage.userName : "" })
}
logout(event: any) {
localStorage.removeItem('token');
window.location.href = "/"
}
handleChange = (event: any, value: any) => {
this.props.history.push(value);
};
render() {
const classes = this.props.classes;
let route = '/' + this.props.history.location.pathname.split('/')[1];
return (
<div>
<Grid container spacing={24}>
<Grid item xs={12} className={classes.root}>
<img src="/Features/Client/Menu/logo.png" alt="Logo" className={classes.imageStyle} />
<div className={this.props.classes.myAccount}>
<span><span className={this.props.classes.topMenuAccount}>MY ACCOUNT</span><span className={classes.topMenuAccount}><SimpleLineIcons iconName={'user'} />▾</span></span>
<span onClick={this.logout} className={classes.topMenuAccount}><SimpleLineIcons iconName={'logout'} /></span>
</div>
</Grid>
<Grid item xs={12} >
<div className="route-list">
<Tabs
value={route}
onChange={this.handleChange}
indicatorColor="primary"
textColor="primary"
>
<Tab label="Overview" value="/" />
<Tab label={<Badge classes={{ badge: classes.badge }} badgeContent={this.props.caseRenewalCount} color="primary">
Renewals
</Badge>} value="/renewals" />
</Tabs>
</div>
</Grid>
</Grid>
</div>
);
}
}
export default withStyles(styles)(withRouter(Menu))

TypeScript implementation of the router-driven tabs.
For those who look for the TypeScript implementation. Easy configurable. Driven by tabs configuration.
interface ITabsPageProps {
match: match<{page: string}>;
history: History;
}
const tabs = [{
label: 'Fist Tab',
link: 'fist-tab',
component: <FirstTabContent/>
}, {
label: 'Second Tab',
link: 'second-tab',
component: <SecondTabContent/>
}, {
label: 'Third Tab',
link: 'third-tab',
component: <ThirdTabContent/>
}];
export class TabsPage extends React.Component<ITabsPageProps> {
handleChange(tabLink: string) {
this.props.history.push(`/tabs-page/${tabLink}`);
}
render() {
const currentTab = this.props.match.params.page;
const selectedTab = tabs.find(tab => currentTab === tab.link);
return (
<Fragment>
<Tabs
value={currentTab}
onChange={(event, value) => this.handleChange(value)}
>
{tabs.map(tab => (
<Tab
key={tab.link}
value={tab.link}
label={tab.label}
/>
))}
</Tabs>
{selectedTab && selectedTab.component}
</Fragment>
);
}
}

Here's another implementation of React with hooks, Material-UI with tabs, React Router with Link, and TypeScript.
import * as React from "react";
import { BrowserRouter as Router, Route, Redirect, Switch, Link, LinkProps } from 'react-router-dom';
import AppBar from '#material-ui/core/AppBar';
import Tabs from '#material-ui/core/Tabs';
import { default as Tab, TabProps } from '#material-ui/core/Tab';
import Home from './Home';
import ProductManagement from './ProductManagement';
import Development from './Development';
import HomeIcon from '#material-ui/icons/Home';
import CodeIcon from '#material-ui/icons/Code';
import TimelineIcon from '#material-ui/icons/Timeline';
const LinkTab: React.ComponentType<TabProps & LinkProps> = Tab as React.ComponentType<TabProps & LinkProps>;
function NavBar() {
const [value, setValue] = React.useState(0);
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
setValue(newValue);
};
return (
<div >
<AppBar position="static" >
<Tabs value={value} onChange={handleChange} centered>
<LinkTab label='Home' icon={ <HomeIcon />} component={Link} to="/" />
<LinkTab label='Development' icon={<CodeIcon />} component={Link} to="/dev" />
<LinkTab label='Product Management' icon={<TimelineIcon />} component={Link} to="/pm" />
</Tabs>
</AppBar>
</div>
)
};
export default function App() {
return (
<Router>
<div>
<NavBar />
<Switch>
<Route exact path="/" component={ Home } />
<Route exact path="/dev" component={ Development } />
<Route exact path="/pm" component={ ProductManagement } />
<Redirect from="/" to="/" />
</Switch>
</div>
</Router>
)
}

So my work-around for this solution has been quite reliable, though it may be more manual of a solution than what you're looking to do.
The strategy that I've been using is to actually not even use the Link Component. Instead, you'll utilize the Tabs onChange property as a callback that can respond to Tab clicks, and track location manually with Props on the Parent.
You can import a utility called History from react-router that will allow you to manually push locations. While using React-Router, your component tree will have access to Location prop that has a pathname key with the string of your current location.
We will manually parse this string into the components that make up your current URL, then use a Switch statement to decide both which tab is currently selected and also where to link to when a tab is clicked. (This gives you a fair amount of control over navigation)
( e.g. ['', 'latest'] )
Here is a mock up of what your component MAY look like after integrating this solution.
import React from 'react';
import {History} from 'react-router';
function parseLocation(location) {
if (String(location)) {
var locationArray = location.split('/');
return locationArray;
} else {
return false;
}
};
function filterPath(path) {
let locationArray = parseLocation(path);
return locationArray[locationArray.length - 1];
};
var Navigation = React.createClass({
mixins: [History],
getPage() {
if (this.props.location.pathname) {
let pathname = this.props.location.pathname;
let pageName = filterPath(pathname);
return pageName;
} else {
return false;
}
},
decideContent() {
let page = this.getPage();
let content;
switch(page) {
case 'popular':
content = 0;
case 'latest':
content = 1;
case 'myideas':
content = 2;
default:
content = 0;
}
return content;
},
handleTabChange(value) {
let location = false;
switch (value) {
case 0:
location = 'popular';
break;
case 1:
location = 'latest';
break;
case 2:
location = 'myideas';
break;
}
if (location && location !== this.getPage()) {
this.history.pushState(null, '/'+location);
}
},
render() {
var styles = {
appBar: {
flexWrap: 'wrap'
},
tabs: {
width: '100%'
}
};
let content = this.decideContent();
let tabs = <Tabs
onChange={this.handleTabChange}
value={content}
>
<Tab label="Most Popular Ideas" value={0} />
<Tab label="Latest Ideas" value={1} />
<Tab label="My Ideas" value={2} />
</Tabs>;
return (
<AppBar showMenuIconButton={false} style={styles.appBar}>
{tabs}
</AppBar>
);
}
});

Check this link, I implemented the solution and worked for me
Composition in material UI

If you use NextJs, you can do it like that, and create your own component.
*i didn`t wrap the Tab with 'a' tag, because it added automatically
const WrapTab = (props) => {
const { href } = props
return (
<Link href={href} style={{ width: "100%" }}>
<Tab {...props} />
</Link>
)}
and then this is your return
<Tabs
value={value}
indicatorColor="primary"
textColor="primary"
onChange={handleChange}
variant="fullWidth"
>
<WrapTab href="/testPage/?tab=0" icon={<MenuIcon />} />
<WrapTab href="/testPage/?tab=1" icon={<StampIcon2 />} />
<WrapTab href="/testPage/?tab=2" icon={<ShopIcon />} />
<WrapTab href="/testPage/?tab=3" icon={<PenIcon />} />
<WrapTab href="/testPage/?tab=4" icon={<ProfileIcon />} />
</Tabs>
link to material-ui docs:
https://material-ui.com/guides/composition/

This seems to work for me
import { Link as RouterLink } from 'react-router-dom';
import Link from '#mui/material/Link';
<Link to={menuItem.url} component={RouterLink} aria-current="page">
{menuItem.label}
</Link>

For anyone looking to wrap Material-ui Link with Next.js Link
import Link from "next/link"
import MuiLink from "#material-ui/core/Link"
const CustomNextLink = ({href, alt}) => ({children, ...rest}) => (
<Link href={href} alt={alt}>
<MuiLink {...rest}>
{children}
</MuiLink>
</Link>)
Then pass it to you Tab component
<Tab
key={...}
label={title}
icon={icon}
component={CustomNextLink({href: to, alt: title})}
style={...}
className={...}
classes={{selected: ...}}
{...a11yProps(index)}
/>

Use the href="" option as shown below:
<Tab
href="/en/getting-started"
label="Contact US"
style={{ color: "white", textDecoration: "none" }}
/>
To remove the ripple effect on clicking, use the option disableRipple

Related

Warning: Each child in a list should have a unique "key" prop. how to fix this?

Ive been using this project with out a problem and now all of a sudden I keep getting this error and it won't show my notes when I click on the my notes section. What do I have to do for it to go away. The backend is up and running and I can see the static data but it wont show on the app
import { makeStyles } from '#mui/styles'
import React from 'react'
import { Drawer } from '#mui/material'
import { Typography } from '#mui/material'
import List from '#mui/material/List'
import ListItem from '#mui/material/ListItem'
import ListItemIcon from '#mui/material/ListItemIcon'
import ListItemText from '#mui/material/ListItemText'
import { AddCircleOutlineOutlined, SubjectOutlined } from '#mui/icons-material'
import { useHistory, useLocation } from 'react-router-dom'
import AppBar from '#mui/material/AppBar'
import Toolbar from '#mui/material/Toolbar'
import { format } from 'date-fns'
import { red } from '#mui/material/colors'
const drawerWidth = 240 // 500 - subtract this number from
const useStyles = makeStyles((theme) => {
return{
page: {
background: '#E5E4E2',
width: '100%',
padding: theme.spacing(3)
},
drawer: {
width: drawerWidth
},
drawerPaper: {
width: drawerWidth
},
root: {
display: 'flex' //places the drawer side by side with the page content
},
active: {
background: '#E5E4E2'
},
// title:{
// padding: theme.spacing(13),
// alignItems: 'center'
// },
}})
export default function Layout({ children }) {
const classes = useStyles()
const history = useHistory()
const location = useLocation()
const menuItems = [
{
text: 'My Projects',
icon: <SubjectOutlined color="secondary" />,
path: '/'
},
{
text: 'Create Project',
icon: <AddCircleOutlineOutlined color="secondary" />,
path: '/create'
}
]
return (
<div className={classes.root}>
{/* side drawer */}
<Drawer
className={classes.drawer}
variant='permanent' //Lets MUI know we want it on the page permanently
anchor="left" // position of drawer
classes={{ paper: classes.drawerPaper}}
>
<div>
<Typography variant="h5" sx={{textAlign: 'center'}}>
Projects
</Typography>
</div>
{/* List / Links */}
<List>
{menuItems.map(item => (
<div className={location.pathname == item.path ? classes.active : null}>
<ListItem key={item.text} button onClick={() => history.push(item.path)}>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.text} />
</ListItem>
</div>
))}
</List>
</Drawer>
<div className={classes.page}>
<div className={classes.toolbar}></div>
{children}
</div>
</div>
)
}
enter image description here
Updated
I'm sorry, of course, you should just move key to the parent div. I didn't notice it. Chris who answered in the comments is right and my answer was not needed. I rewrote the answer.
To have an unique key use index in map or like you did item.text if text is unique for each element in map.
menuItems.map((item,index) =>
The idea is that map has to contain unique key for each element.
In result we have:
<div key={item.text} className={location.pathname == item.path ? classes.active : null}>
or
<div key={index} className={location.pathname == item.path ? classes.active : null}>
And you need to remove key from the List.
Hope this helps! Regards,

Problem passing parameters down in React native

I am having a hard time using React StackNavigator, and passing parameters down to a screen. I am just in the stages of learning how React Native works, so maybe this is not the best practice so I am open to other suggestions if there is a better way.
function SetupsStack(props) {
console.log(props.route.params.Setup,"route is") // This has what I want
return (
<Stack.Navigator
initialRouteName="IndivdualSetup"
mode="card"
headerMode="screen"
>
<Stack.Screen
name="IndivdualSetup"
component={IndivualScreen}
//component={<IndividualScreen individual={props.route.params.Setup} />} thought this was it but its not
options={{
header: ({ navigation, scene }) => (
<Header
title="IndivdualSetup"
tabs
tabTitleSizeRight={10}
tabRightIcon={"shape-star"}
scene={scene}
navigation={navigation}
/>
),
}}
/>
</Stack.Navigator>
);
}
The component:
import React from "react";
import { ScrollView, StyleSheet, Dimensions } from "react-native";
import { Block, Text, theme } from "galio-framework";
const { width } = Dimensions.get("screen");
const thumbMeasure = (width - 48 - 32) / 3;
export default class IndivualScreen extends React.Component {
render() {
// const {
// navigation,
// route,
// } = this.props;
// const { product } = route.params;
console.log(this.props,"props are")
return (
<Block flex center>
<ScrollView
style={styles.components}
showsVerticalScrollIndicator={false}
>
<Block flex>
<Text bold size={30} style={styles.title}>
Text here
</Text>
</Block>
</ScrollView>
</Block>
);
}
}
The console log in this component does not have route as a parameter just navigation, but this only has has setParams (looks like the removed getParams in v5 and newer. However if I do:
component={<IndividualScreen individual={props.route.params.Setup} />}
I get:
Blockquote Error: Got an invalid value for 'component' prop for the screen 'IndivdualSetup'. It must be a valid React Component.
My syntax looks correct everywhere so not exactly sure what my problem is, or if it isn't working cause there is a better practice I should be following.
Thanks in advance!
instead of
component={<IndividualScreen individual={props.route.params.Setup} />}
you have to do this:
component={(props)=> <IndividualScreen individual={props.route.params.Setup} />}

React/JS/TS - Adding prop to component by location

So I have a custom component for my sidebar navigation that isn't completely functional. I'm trying to figure out how to dynamically add isSelected to the CustomItem component by location.
Edit: I'm assuming that useLocation from react-router-dom is my best bet for the first part.
Here's the code for the Dashboard:
/** #jsx jsx */
import React, { forwardRef } from 'react';
import {
Main,
Content,
LeftSidebar,
} from '#atlaskit/page-layout';
import {
Link,
Route,
Switch,
useParams,
useRouteMatch } from 'react-router-dom';
import { css, jsx } from '#emotion/core';
import { Section } from '#atlaskit/menu';
import {
Header,
NavigationHeader,
NestableNavigationContent,
SideNavigation,
CustomItem,
CustomItemComponentProps
} from '#atlaskit/side-navigation';
type CustomProps = CustomItemComponentProps & {
href: string;
};
const CustomLink = forwardRef<HTMLAnchorElement, CustomProps>(
(props: CustomProps, ref) => {
const { href, children, ...rest } = props;
return (
<Link to={href} ref={ref} {...rest}>
{children}
</Link>
);
},
);
function SideNavigationWrapper(props: { children: React.ReactNode }) {
return (
<div
css={css`
height: 100%;
& nav {
min-width: 20px;
overflow-x: hidden;
}
`}
>
{props.children}
</div>
);
}
function Components() {
let { component } = useParams();
if (component === 'assets') {
return (
<Assets />
);
}
if (component === 'orders') {
return (
<Orders />
);
}
return (
<div></div>
);
}
export default function Dashboard() {
let { path } = useRouteMatch();
return (
<Content>
<LeftSidebar
isFixed={true}
id="dash-navigation"
>
<SideNavigationWrapper>
<SideNavigationContent />
</SideNavigationWrapper>
</LeftSidebar>
<Main>
<div
style={{
marginLeft: 40,
marginRight: 40,
marginTop: 20,
}}
>
<Switch>
<Route exact path={path}>
<Overview />
</Route>
<Route path={`${path}/:component`}>
<Components />
</Route>
</Switch>
</div>
</Main>
</Content>
);
}
const SideNavigationContent = () => {
return (
<SideNavigation label="DashboardNav">
<NavigationHeader>
<Header>Dashboard</Header>
</NavigationHeader>
<NestableNavigationContent initialStack={[]}>
<Section>
<CustomItem
isSelected
component={CustomLink}
href="/dashboard"
iconBefore={<PortfolioIcon label="" />}
>
Portfolio
</CustomItem>
<CustomItem
component={CustomLink}
href="/dashboard/assets"
iconBefore={<SuitcaseIcon label="" />}
>
Assets
</CustomItem>
<CustomItem
component={CustomLink}
href="/dashboard/orders"
iconBefore={<RoadmapIcon label="" />}
>
Orders
</CustomItem>
</Section>
</NestableNavigationContent>
</SideNavigation>
);
};
It's also worth nothing that my approach has slightly messed up the styling as shown below:
Which should appear like this naturally without me having to hover over it.
Edit: I'm also assuming that I would have to override the styling with cssFn as per the documentation.
Any guidance on either two issues would be helpful. I've gave myself a headache trying to redo the entire apps routing. Figured I'd try my best to wrap this up tonight so I can move onto greater issues
You should assign boolean instead of putting props
const SideNavigationContent = props => {
//Add logic to control variable
//isPortfolioSelected, isAssetsSelect, isOrdersSelected, then assign to each item,
//Ex: isPortfolioSelected = props.selected['Portfolio']
return (
<SideNavigation label="DashboardNav">
<NavigationHeader>
<Header>Dashboard</Header>
</NavigationHeader>
<NestableNavigationContent initialStack={[]}>
<Section>
<CustomItem
isSelected={isPortfolioSelected}
component={CustomLink}
href="/dashboard"
iconBefore={<PortfolioIcon label="" />}
>
Portfolio
</CustomItem>
<CustomItem
isSelected={isAssetsSelect}
component={CustomLink}
href="/dashboard/assets"
iconBefore={<SuitcaseIcon label="" />}
>
Assets
</CustomItem>
<CustomItem
isSelecled={isOrdersSelected}
component={CustomLink}
href="/dashboard/orders"
iconBefore={<RoadmapIcon label="" />}
>
Orders
</CustomItem>
</Section>
</NestableNavigationContent>
</SideNavigation>
);
};

Nested MaterialUI Tabs throws an error when opening second tabs level

I am trying to build a nested horizontal tabs in MaterialUI, I mean, a first tabs level that, when you click on it, open a second tabs level.
Here is a link to the working replicable code example: https://codesandbox.io/s/sweet-pasteur-x4m8z?file=/src/App.js
The problem is: When I click on first level, second level is opened, when I click on a item from second level, I get this error
Material-UI: the value provided to the Tabs component is invalid.
None of the Tabs' children match with "value21".
You can provide one of the following values: value11
For replicate the error, you could do next steps:
Click in "Label 1"
Click in "Label 1.1"
Error is thrown
I do not understand why that error, if I am splitting values of each tab in different states and, supposedly, it is all Ok. Maybe the way I use for implementing the nested tab is wrong, any idea what could be happening?
Thank you.
I created three tab components, one parent and two children. Then I Imported the children tab components into the parent. You can use vertical tabs for the child tab components to help with the layout. check out the how the parent tab looks like. Note all these are tab components
// Parent tab component
import React from 'react';
import PropTypes from 'prop-types';
import SwipeableViews from 'react-swipeable-views';
import { makeStyles, useTheme } from '#material-ui/core/styles';
import AppBar from '#material-ui/core/AppBar';
import Tabs from '#material-ui/core/Tabs';
import Tab from '#material-ui/core/Tab';
import Typography from '#material-ui/core/Typography';
import Box from '#material-ui/core/Box';
function TabPanel(props) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`full-width-tabpanel-${index}`}
aria-labelledby={`full-width-tab-${index}`}
{...other}
>
{value === index && (
<Box p={3}>
<Typography>{children}</Typography>
</Box>
)}
</div>
);
}
TabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.any.isRequired,
value: PropTypes.any.isRequired,
};
function a11yProps(index) {
return {
id: `full-width-tab-${index}`,
'aria-controls': `full-width-tabpanel-${index}`,
};
}
const useStyles = makeStyles((theme) => ({
root: {
backgroundColor: theme.palette.background.paper,
width: 500,
},
}));
export default function FullWidthTabs() {
const classes = useStyles();
const theme = useTheme();
const [value, setValue] = React.useState(0);
const handleChange = (event, newValue) => {
setValue(newValue);
};
const handleChangeIndex = (index) => {
setValue(index);
};
return (
<div className={classes.root}>
<AppBar position="static" color="default">
<Tabs
value={value}
onChange={handleChange}
indicatorColor="primary"
textColor="primary"
variant="fullWidth"
aria-label="full width tabs example"
>
<Tab label="Item One" {...a11yProps(0)} />
<Tab label="Item Two" {...a11yProps(1)} />
</Tabs>
</AppBar>
<SwipeableViews
axis={theme.direction === 'rtl' ? 'x-reverse' : 'x'}
index={value}
onChangeIndex={handleChangeIndex}
>
<TabPanel value={value} index={0} dir={theme.direction}>
<ChildTabOne/>
</TabPanel>
<TabPanel value={value} index={1} dir={theme.direction}>
<ChildTabTwo/>
</TabPanel>
</SwipeableViews>
</div>
);
}

React Router with - Ant Design Sider: how to populate content section with components for relevant menu item

I'm trying to use AntD menu sider like a tab panel.
I want to put components inside the content so that the content panel renders the related component when a menu item is clicked.
How do I get this structure to take components as the content for each menu item?
import React from 'react';
import { compose } from 'recompose';
import { Divider, Layout, Card, Tabs, Typography, Menu, Breadcrumb, Icon } from 'antd';
import { PasswordForgetForm } from '../Auth/Password/Forgot';
const { Title } = Typography
const { TabPane } = Tabs;
const { Header, Content, Footer, Sider } = Layout;
const { SubMenu } = Menu;
class Dashboard extends React.Component {
state = {
collapsed: false,
};
onCollapse = collapsed => {
console.log(collapsed);
this.setState({ collapsed });
};
render() {
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider collapsible collapsed={this.state.collapsed} onCollapse={this.onCollapse}>
<div />
<Menu theme="light" defaultSelectedKeys={['1']} mode="inline" >
<Menu.Item key="1">
<Icon type="fire" />
<span>Next item</span>
<PasswordForgetForm />
</Menu.Item>
<Menu.Item key="2">
<Icon type="fire" />
<span>Next item</span>
<Another component to render in the content div />
</Menu.Item>
</Menu>
</Sider>
<Layout>
<Header style={{ background: '#fff', padding: 0 }} />
<Content style={{ margin: '0 16px', background: '#fff' }}>
<div style={{ padding: 24, background: '#fff', minHeight: 360 }}>
RENDER RELEVANT COMPONENT HERE BASED ON MENU ITEM SELECTED
</div>
</Content>
</Layout>
</Layout>
);
}
}
export default Dashboard;
When I try to render this, no errors are thrown, but the content div does not update with the PasswordForgetForm that I specified as the content for the menu item.
I tried Chris' suggestion below - which works fine to render the component for each of the different menu item content divs in the layout segment, however - the downside of this approach is that with every page refresh, the path goes to the content component for that particular menu item, instead of the test page which has the menu on it -- and the content component with that relevant content. If this approach is endorsed as sensible, is there a way to keep the page refresh on the original page, rather than trying to go to a subset menu item url?
Update
I found this literature. I think this might be what I need to know, but the language is too technical for me to understand. Can anyone help with a plain english version of this subject matter?
I also found this tutorial which uses Pose to help with rendering. While this structure is what I'm trying to achieve, it looks like Pose has been deprecated. Does anyone know if its necessary to go beyond react-router to get this outcome, or is there a solution within react that I can look to implement?
This example is similar to what I want, but I can't find anything that defines sidebar (which seems to be an argument that is necessary to make this work).
I have also seen this post. While some of the answers are a copy and paste of the docs, I wonder if the answer by Adam Gering is closer to what I need. I'm trying to keep the Sider menu on the /dash route at all times. That url should not change - regardless of which menu item in the sider the user clicks BUT the content div in the /dash component should be updated to render the component at the route path I've specified.
APPLYING CHRIS' SUGGESTION
Chris has kindly offered a suggestion below. I tried it, but the circumstance in which it does not perform as desired is when the menu item is clicked (and the relevant component correctly loads in the content div, but if I refresh the page, the refresh tries to load a page with a url that is for the component that is supposed to be inside the content div on the dashboard page.
import React from 'react';
import {
BrowserRouter as Router,
Route,
Link,
Switch,
} from 'react-router-dom';
import * as ROUTES from '../../constants/Routes';
import { compose } from 'recompose';
import { Divider, Layout, Card, Tabs, Typography, Menu, Breadcrumb, Icon } from 'antd';
import { withFirebase } from '../Firebase/Index';
import { AuthUserContext, withAuthorization, withEmailVerification } from '../Session/Index';
// import UserName from '../Users/UserName';
import Account from '../Account/Index';
import Test from '../Test/Index';
const { Title } = Typography
const { TabPane } = Tabs;
const { Header, Content, Footer, Sider } = Layout;
const { SubMenu } = Menu;
class Dashboard extends React.Component {
state = {
collapsed: false,
};
onCollapse = collapsed => {
console.log(collapsed);
this.setState({ collapsed });
};
render() {
const { loading } = this.state;
// const dbUser = this.props.firebase.app.snapshot.data();
// const user = Firebase.auth().currentUser;
return (
<AuthUserContext.Consumer>
{authUser => (
<div>
{authUser.email}
<Router>
<Layout style={{ minHeight: '100vh' }}>
<Sider collapsible collapsed={this.state.collapsed} onCollapse={this.onCollapse}>
<div />
<Menu theme="light" defaultSelectedKeys={['1']} mode="inline" >
<SubMenu
key="sub1"
title={
<span>
<Icon type="user" />
<span>Profile</span>
</span>
}
>
<Menu.Item key="2"><Link to={ROUTES.ACCOUNT}>Account Settings</Link></Menu.Item>
<Menu.Item key="3"><Link to={ROUTES.TEST}>2nd content component</Link></Menu.Item>
</SubMenu>
</Menu>
</Sider>
<Layout>
<Header> </Header>
<Content style={{ margin: '0 16px', background: '#fff' }}>
<div style={{ padding: 24, background: '#fff', minHeight: 360 }}>
<Switch>
<Route path={ROUTES.ACCOUNT}>
<Account />
</Route>
<Route path={ROUTES.TEST}>
< Test />
</Route>
</Switch>
</div>
</Content>
<Footer style={{ textAlign: 'center' }}>
test footer
</Footer>
</Layout>
</Layout>
</Router>
</div>
)}
</AuthUserContext.Consumer>
);
}
}
export default Dashboard;
In my routes file, I have:
export const ACCOUNT = '/account';
I've also found this tutorial - which uses the same approach outlined above - and doesn't get redirected in the same way that my code does on page refresh. The tutorial uses Route instead of BrowserRouter - but otherwise I can't see any differences.
NEXT ATTEMPT
I saw this post (thanks Matt). I have tried to follow the suggestions in that post.
I removed the outer Router wrapper from the Dashboard page (there is a Router around App.js, which is where the route to Dashboard is setup).
Then, in my Dashboard, I changed the Content div to:
I added:
let match = useRouteMatch();
to the render method inside my Dashboard component (as shown here).
I added useRouteMatch to my import statement from react-router-dom.
This produces an error that says:
Error: Invalid hook call. Hooks can only be called inside of the body
of a function component. This could happen for one of the following
reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
There is more message in the error above but stack overflow won't allow me to post it because it has a short link
I don't know what I've done wrong in these steps, but if I comment out the let statement, the page loads.
When I go to /Dashboard and click "account", Im expecting the account component to render inside the content div I have in the layout on the Dashboard page. Instead, it redirects directly to a page at localhost3000/account (for which there is not page reference - it's just a component to render inside the Dashboard page).
So this is worse than the problem I started with - because at least at the start, the redirect only happened on page refresh. Now it happens immediately.
I found this repo that has examples of each kind of route. I can't see what it is doing that I am not.
This post seems to have had the same problem as me, and resolved it using the same approach as I have tried. It's a post from 2018 so may the passage of time makes the approach outdated - but I can't see any difference between what that post solution implements and my original attempt.
NEXT ATTEMPT
I had thought I had found an approach that works in Lyubomir's answer on this post.
I have:
<Menu.Item key="2">
<Link to=
{${this.props.match.url}/account}>
Account Settings
Then in the div on the dash component where I want to display this component I have:
<div>
<Switch>
<Route exact path={`${this.props.match.url}/:account`} component={Account} />
This works. On page refresh, I can keep things working as they should.
HOWEVER, when I add a second menu item with:
<Menu.Item key="16">
<Link to={`${this.props.match.url}/learn`}>
<Icon type="solution" />
<span>Lern</span>
</Link>
</Menu.Item>
Instead of rendering the Learning component when menu item for /learn is clicked (and the url bar changes to learn), the Account component is rendered. If i delete the account display from the switch statement, then I can have the correct Learning component displayed.
With this attempt, I have the menu items working to match a Dashboard url extension (ie localhost/dash/account or localhost/dash/learn) for ONE menu item only. If I add a second menu item, the only way I can correctly render the 2nd component, is by deleting the first menu item. I am using switch with exact paths.
I want this solution to work with multiple menu items.
I have tried alternating path for url (eg:
<Link to={`${this.props.match.url}/learn`}>
is the same as:
<Link to={`${this.props.match.path}/learn`}>
I have read the explanation in this blog and while I don't entirely understand these options. I have read this. It suggests the match statement is now a legacy method for rendering components (now that hooks are available). I can't find any training materials that show how to use hooks to achieve the expected outcome.
The better solution is using React Router <Link> to make each menu item link to a specific path, and then in the content, using <Switch> to render the corresponding component. Here's the doc: React router
Render With React Router
<Router>
<Layout style={{ minHeight: "100vh" }}>
<Sider
collapsible
collapsed={this.state.collapsed}
onCollapse={this.onCollapse}
>
<div />
<Menu theme="light" defaultSelectedKeys={["1"]} mode="inline">
<Menu.Item key="1">
// Add a react router link here
<Link to="/password-forget-form">
<Icon type="fire" />
<span>Next item</span>
</Link>
</Menu.Item>
<Menu.Item key="2">
// Add another react router link
<Link to="/next-item">
<Icon type="fire" />
<span>Next item</span>
</Link>
</Menu.Item>
</Menu>
</Sider>
<Layout>
<Header style={{ background: "#fff", padding: 0 }} />
<Content style={{ margin: "0 16px", background: "#fff" }}>
<div style={{ padding: 24, background: "#fff", minHeight: 360 }}>
// Render different components based on the path
<Switch>
<Route path="/password-forget-form">
<PasswordForgetForm />
</Route>
<Route path="/next-item">
<Another component to render in the content div />
</Route>
</Switch>
</div>
</Content>
</Layout>
</Layout>
</Router>;
Render With Menu Keys
App.js
import React, { useState } from "react";
import { Layout } from "antd";
import Sider from "./Sider";
import "./styles.css";
const { Content } = Layout;
export default function App() {
const style = {
fontSize: "30px",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center"
};
const components = {
1: <div style={style}>Option 1</div>,
2: <div style={style}>Option 2</div>,
3: <div style={style}>Option 3</div>,
4: <div style={style}>Option 4</div>
};
const [render, updateRender] = useState(1);
const handleMenuClick = menu => {
updateRender(menu.key);
};
return (
<div className="App">
<Layout style={{ minHeight: "100vh" }}>
<Sider handleClick={handleMenuClick} />
<Layout>
<Content>{components[render]}</Content>
</Layout>
</Layout>
</div>
);
}
Sider.js
import React from "react";
import { Menu, Layout, Icon } from "antd";
const { SubMenu } = Menu;
export default function Sider(props) {
const { handleClick } = props;
return (
<Layout.Sider>
<Menu theme="dark" mode="inline" openKeys={"sub1"}>
<SubMenu
key="sub1"
title={
<span>
<Icon type="mail" />
<span>Navigation One</span>
</span>
}
>
<Menu.Item key="1" onClick={handleClick}>
Option 1
</Menu.Item>
<Menu.Item key="2" onClick={handleClick}>
Option 2
</Menu.Item>
<Menu.Item key="3" onClick={handleClick}>
Option 3
</Menu.Item>
<Menu.Item key="4" onClick={handleClick}>
Option 4
</Menu.Item>
</SubMenu>
</Menu>
</Layout.Sider>
);
}
I found Lyubomir's answer on this post. It works. Nested routes with react router v4 / v5
The menu item link is:
<Menu.Item key="2">
<Link to=
{`${this.props.match.url}/account`}>
Account
</Link>
</Menu.Item>
The display path is:
<Route exact path={`${this.props.match.path}/:account`} component={Account} />
There is a colon before the name of the component. Not sure why. If anyone knows what this approach is called - I'd be grateful for a reference so that i can try to understand it.
I am using antd >=4.20.0, as a side effect Menu.Item is deprecated. While above solution works, It does warn about discontinuing support. So thats not a long term solution.
Check here for details on 4.20.0 changes.
Long term solution is that instead of Menu.Item we need to use items={[]}. But with that <Link> doesn't work as API doesn't have support and we cant pass it as child component.
I looked around, after much search and combining information from various sources, what worked was,
use useNavigate() to change pages using onClick in Menu items[].
use useLocation().pathname to get path and pass it up as selectedKey
Define routes in <Content>.
Here's working example [ stripped to skeleton code to illustrate concept]
#index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './index.css';
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
#App.js
import React, {useState} from 'react';
import { Layout, Menu, Tooltip } from 'antd'
import {
MenuUnfoldOutlined,
MenuFoldOutlined,
TeamOutlined,
UserOutlined,
} from '#ant-design/icons'
import { useLocation, useNavigate, Route, Routes } from 'react-router-dom'
import './App.less';
import 'antd/dist/antd.min.css';
const { Content, Sider } = Layout;
const Page1 = () => {
return <h4> Page 1</h4>
}
const Page2 = () => {
return <h4> Page 2</h4>
}
const App = () => {
const [collapsed, setCollapsed] = useState(true);
const toggleCollapsed = () => {
setCollapsed(!collapsed);
};
let navigate = useNavigate();
const selectedKey = useLocation().pathname
const highlight = () => {
if (selectedKey === '/'){
return ['1']
} else if (selectedKey === '/page2'){
return ['2']
}
}
return (
<Layout className="site-layout">
<Sider trigger={null} collapsible collapsed={collapsed}>
<div className="logo">
<Tooltip placement="right" arrowPointAtCenter title="Expand / Shrink Menu" >
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
onClick: toggleCollapsed,
})}
</Tooltip>
</div>
<Menu
mode="inline"
theme="light"
defaultSelectedKeys={['1']}
selectedKeys={highlight()}
style={{ height: '100%', borderRight:0 }}
items={[
{
key: '1',
icon: <UserOutlined />,
label: "Page 1",
onClick: () => { navigate('/')}
},
{
key: '2',
icon: <TeamOutlined />,
label: "Page 2",
onClick: () => { navigate('/page2')}
}
]}
/>
</Sider>
<Content>
<Routes>
<Route exact path="/" element={<Page1 />} />
<Route path="/page2" element={<Page2 />} />
</Routes>
</Content>
</Layout>
)
}
export default App;
#Resulting Page
Just an update to wei-su's answer above:
antd has declared the deprecation of using Menu.item components after version 4.20.0. A quick and dirty hack could be nesting the react router <Link> inside label of the antdesign menu:
import { Icon, Menu } from "antd";
import { Link } from "react-router-dom";
// and the other imports ...
const items = [
{
path: "/some-path",
label: "a menu item",
icon: <Icon type="fire" />,
},
{
path: "/another-path",
label: "another menu item",
icon: <Icon type="fire" />,
},
].map((item, index) => {
return {
key: index,
label: <Link to={item.path}>{item.label}</Link>,
icon: item.icon,
};
});
and then when rendering Menu:
<Menu
mode="inline"
items={items}
defaultSelectedKeys={["0"]}
/>

Categories

Resources