react-router+antD/ How to highlight a menu item when press back/forward button?
Asked Answered
C

8

28

I create a menu and want to highlight the item which i choose,and i did it. But when i press back/forward button,the menu item don't highlight. What should i do?

I have tried to use addEventListener but failed.

Have someone could give some advice?

class Sidebar extends React.Component {
    constructor(props) {
        super(props);
        this.state={
            test: "home"
        }
        this.menuClickHandle = this.menuClickHandle.bind(this);
    }

    componentWillMount(){
        hashHistory.listen((event)=>{
            test1 = event.pathname.split("/");
        });
        this.setState({
            test:test1[1]
        });
    }

    menuClickHandle(item) {
        this.props.clickItem(item.key);
    }

    onCollapseChange() {
        this.props.toggle();
    }

    render() {
        var {collapse} = this.props;
        return (
            <aside className="ant-layout-sider">
                <Menu mode="inline" theme="dark" defaultSelectedKeys={[this.state.test || "home"]} onClick={this.menuClickHandle.bind(this)}>
                    <Menu.Item key="home">
                        <Link to="/home">
                            <Icon type="user"/><span className="nav-text">用户管理</span>
                        </Link>
                    </Menu.Item>
                    <Menu.Item key="banner">
                        <Link to="/banner">
                            <Icon type="setting"/><span className="nav-text">Banner管理</span>
                        </Link>
                    </Menu.Item>
                </Menu>
                <div className="ant-aside-action" onClick={this.onCollapseChange.bind(this)}>
                    {collapse ? <Icon type="right"/> : <Icon type="left"/>}
                </div>
            </aside>
        )
    }
}
Chancellor answered 9/12, 2016 at 6:52 Comment(0)
C
8

Intercepts the current URL and then set selectedKeys(Note that it is not defaultSelectedKeys).

componentWillMount(){
        hashHistory.listen((event)=>{
            pathname = event.pathname.split("/");
            if(pathname != null){
                this.setState({
                    test:pathname[1]
                });
            }
        });
    }
Chancellor answered 12/12, 2016 at 3:33 Comment(2)
thanks a lot man. I wasted 3 hours thinking it had something to do with react lifecycle. It indeed is selectedKeys instead of defaultSelectedKeys. Upvote indeed.Guava
Do you have any idea how to solve the problem in Angular ?Meridel
V
52

I could come up with a solution using WithRouter

import React,{ Component } from 'react';
import { NavLink, withRouter } from 'react-router-dom';
import { Layout, Menu, Icon } from 'antd';
import PropTypes from 'prop-types';

const { Sider } = Layout;

class SideMenu extends Component{

  static propTypes = {
    location: PropTypes.object.isRequired
  }

  render() {
    const { location } = this.props;
    return (
        <Sider
          trigger={null}
          collapsible
          collapsed={this.props.collapsed}>

          <div className="logo" />
          <Menu
            theme="dark"
            mode="inline"
            defaultSelectedKeys={['/']}
            selectedKeys={[location.pathname]}>
            <Menu.Item key="/">
              <NavLink to="/">
                <Icon type="home" />
                <span>Home</span>
              </NavLink>
            </Menu.Item>
            <Menu.Item key="/other">
              <NavLink to="/other">
                <Icon type="mobile"/>
                <span>Applications</span>
              </NavLink>
            </Menu.Item>
            <Menu.Item key="/notifications">
              <NavLink to="/notifications">
                <Icon type="notification" />
                <span>Notifications</span>
              </NavLink>
            </Menu.Item>
          </Menu>
        </Sider>
    )
  }
}

export default withRouter(SideMenu);
Vittorio answered 2/2, 2018 at 5:26 Comment(0)
C
8

Intercepts the current URL and then set selectedKeys(Note that it is not defaultSelectedKeys).

componentWillMount(){
        hashHistory.listen((event)=>{
            pathname = event.pathname.split("/");
            if(pathname != null){
                this.setState({
                    test:pathname[1]
                });
            }
        });
    }
Chancellor answered 12/12, 2016 at 3:33 Comment(2)
thanks a lot man. I wasted 3 hours thinking it had something to do with react lifecycle. It indeed is selectedKeys instead of defaultSelectedKeys. Upvote indeed.Guava
Do you have any idea how to solve the problem in Angular ?Meridel
C
8

you can set the paths of the link as keys on each Menu.Item . then selectedKeys={this.props.location.pathname}

<Menu
  theme="light"
  mode='inline'
  selectedKeys={[this.props.location.pathname,]}
>
  <Menu.Item key={item.path} style={{float:'right'}}>
    <Link to={item.path}>{item.name}</Link>
  </Menu.Item>
  {menulist}
</Menu>

Item would be set active according to the current path. i added [] and trailing comma because selectedKeys accepts array while this.props.location.pathname is a String. i just code as hobby so idont know if its acceptable.

Carbarn answered 7/1, 2019 at 2:18 Comment(1)
Also work if use of react router4 hooks: let location = useLocation();Cocky
R
6

The following answer assumes you are using hooks. I know you are not in your question, but it might be useful for other people. In addition, this solution will work if you have nested paths such as /banner/this/is/nested, and it works not only when pressing back and forward buttons but also when refreshing the current page:

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

This is how the sidebar will look like: enter image description here

Robinette answered 26/5, 2020 at 20:59 Comment(1)
If you are using this solution and get an error like useLocation() may be used only in the context of a <Router> component, make sure the parent component is wrapped in a <Router></Router> tag. In my case, I tried useLocation() at the <App></App> level and it didn't work until I made my nav its own functional component and defined the logic there.Redtop
B
2

@Nadun's solution works for paths that don't contains arguments. If you're however using arguments in your routes, like me, here's a solution that should work for any route path, including /users/:id or crazy stuff like /users/:id/whatever/:otherId. It uses react-router's matchPath API, which uses the exact same logic as the Router component.

// file with routes
export const ROUTE_KEYS = {
    ROOT: "/",
    USER_DETAIL: "/users/:id",
};

export const ROUTES = {
    ROOT: {
        component: Home,
        exact: true,
        key: ROUTE_KEYS.ROOT,
        path: ROUTE_KEYS.ROOT,
    },
    USER_DETAIL: {
        component: Users,
        key: ROUTE_KEYS.USER_DETAIL,
        path: ROUTE_KEYS.USER_DETAIL,
    },
};

.

// place within the App component
<Router>
    <Layout>
        <MyMenu />
        <Layout>
            <Layout.Content>
                {Object.values(ROUTES).map((route) => (
                    <Route {...route} />
                ))}
            </Layout.Content>
        </Layout>
    </Layout>
</Router>

.

// MyMenu component
const getMatchedKey = (location) =>
    (
        Object.values(ROUTES).find((route) =>
            matchPath(location.pathname, route)
        ) || {}
    ).path;

const MyMenu = ({ location }) => {
    return (
        <Layout.Sider>
            <AntMenu mode="inline" selectedKeys={[getMatchedKey(location)]}>
                <AntMenu.SubMenu
                    title={
                        <React.Fragment>
                            <Icon type="appstore" />
                            Home
                        </React.Fragment>
                    }
                >
                    <AntMenu.Item key={ROUTE_KEYS.ROOT}>
                        <Icon type="appstore" />
                        <span className="nav-text">
                            Some subitem
                        </span>
                    </AntMenu.Item>
                </AntMenu.SubMenu>
                <AntMenu.SubMenu
                    title={
                        <React.Fragment>
                            <Icon type="user" />
                            Users
                        </React.Fragment>
                    }
                >
                    <AntMenu.Item key={ROUTE_KEYS.USER_DETAIL}>
                        <Icon type="user" />
                        <span className="nav-text">
                            User detail
                        </span>
                    </AntMenu.Item>
                </AntMenu.SubMenu>
            </AntMenu>
        </Layout.Sider>
    );
};

export default withRouter(MyMenu);
Bratton answered 18/9, 2018 at 13:57 Comment(0)
S
0

I do something like this but it doesn't seem to be reactive. Like if I navigate to a new page through a button (not from the menu items), it will not update the active link until the page refreshes.

import React from 'react';
import { StyleSheet, css } from 'aphrodite'
import { browserHistory, Link } from 'react-router';
import 'antd/lib/menu/style/css';
import 'antd/lib/icon/style/css';
import 'antd/lib/row/style/css';
import 'antd/lib/col/style/css';
import 'antd/lib/message/style/css';
import { appConfig } from '../../modules/config';
import { Menu, Icon, Row, Col, message } from 'antd';

const SubMenu = Menu.SubMenu;
const MenuItemGroup = Menu.ItemGroup;


const { appName } = appConfig;




const AppNavigation = React.createClass({
  getInitialState() {
        return {
          current: this.props.pathname
        };

  },
  handleClick(e) {
    browserHistory.push(e.key);
    this.setState({ current: e.key });
    return;  
  },
  render() {
    return (
    <Row className='landing-menu' type="flex" justify="space-around" align="middle"  style={{height: 55, zIndex: 1000, paddingLeft: 95, color: '#fff', backgroundColor: '#da5347', borderBottom: '1px solid #e9e9e9'}}>
        <Col span='19'>
            <Link to='/'>
          <h2 style={{fontSize: 21, color: '#fff'}}>
            {appName}
            <Icon type="rocket" color="#fff" style={{fontWeight: 200, fontSize: 26, marginLeft: 5 }}/>
          </h2>
        </Link>
        </Col>
        <Col span='5'>
            <Menu onClick={this.handleClick} selectedKeys={[this.state.current]} mode="horizontal" style={{height: 54, backgroundColor: '#da5347', borderBottom: '0px solid transparent'}}>
            <Menu.Item style={{height: 54, }} key="/">Home</Menu.Item>
            <Menu.Item style={{height: 54, }} key="/signup">Signup</Menu.Item>
            <Menu.Item style={{height: 54, }} key="/login">Login</Menu.Item>
          </Menu>
        </Col>

      </Row>
    );
  },
});


export const App = React.createClass({

  propTypes: {
    children: React.PropTypes.element.isRequired,
  },
  componentWillMount(){
    if (Meteor.userId()) {
      browserHistory.push('/student/home')
    }
  },
  render() {

    return (
        <div style={{position: 'relative'}}>
          <AppNavigation pathname={this.props.location.pathname}  />
            <div style={{minHeight: '100vh'}}>
             { this.props.children }
            </div>
        </div>
    );
  }





});

EDIT:

the below works pretty well. pass down the pathname from react-router and pop that as a prop into selectedKeys

import React from 'react';
import { StyleSheet, css } from 'aphrodite'
import { browserHistory, Link } from 'react-router';
import 'antd/lib/menu/style/css';
import 'antd/lib/icon/style/css';
import 'antd/lib/row/style/css';
import 'antd/lib/col/style/css';
import 'antd/lib/message/style/css';
import { appConfig } from '../../modules/config';
import { Menu, Icon, Row, Col, message } from 'antd';

const SubMenu = Menu.SubMenu;
const MenuItemGroup = Menu.ItemGroup;


const { appName } = appConfig;




const AppNavigation = React.createClass({
  getInitialState() {
        return {
          current: this.props.pathname
        };

  },
  handleClick(e) {
    browserHistory.push(e.key);
    this.setState({ current: e.key });
    return;  
  },
  render() {
    return (
    <Row className='landing-menu' type="flex" justify="space-around" align="middle"  style={{height: 55, zIndex: 1000, paddingLeft: 95, color: '#fff', backgroundColor: '#da5347', borderBottom: '1px solid #e9e9e9'}}>
        <Col span='19'>
            <Link to='/'>
          <h2 style={{fontSize: 21, color: '#fff'}}>
            {appName}
            <Icon type="rocket" color="#fff" style={{fontWeight: 200, fontSize: 26, marginLeft: 5 }}/>
          </h2>
        </Link>
        </Col>
        <Col span='5'>
            <Menu onClick={this.handleClick} selectedKeys={[this.props.pathname]} mode="horizontal" style={{height: 54, backgroundColor: '#da5347', borderBottom: '0px solid transparent'}}>
            <Menu.Item style={{height: 54, }} key="/">Home</Menu.Item>
            <Menu.Item style={{height: 54, }} key="/signup">Signup</Menu.Item>
            <Menu.Item style={{height: 54, }} key="/login">Login</Menu.Item>
          </Menu>
        </Col>

      </Row>
    );
  },
});


export const App = React.createClass({

  propTypes: {
    children: React.PropTypes.element.isRequired,
  },
  componentWillMount(){
    if (Meteor.userId()) {
      browserHistory.push('/student/home')
    }
  },
  render() {

    return (
        <div style={{position: 'relative'}}>
          <AppNavigation pathname={this.props.location.pathname}  />
            <div style={{minHeight: '100vh'}}>
             { this.props.children }
            </div>
        </div>
    );
  }


});
Subplot answered 6/1, 2017 at 15:37 Comment(0)
H
0

If you are using an array and mapping over it (as in my case) to set menu Items, They must be in the same order as they appear in the Side menu otherwise, an active bar or background will not be shown.

Homicidal answered 18/3, 2021 at 7:16 Comment(0)
G
0

Environment: React Router V5, Ant Design V4.17.0
I solved this issues by override the onClick props of Menu.Item of antd

            <Menu theme="light" mode="inline">
                {menuItems.map((item) => {
                    return (
                        <NavLink
                            to={item.navigation}
                            component={({ navigate, ...rest }) => <Menu.Item {...rest} onClick={navigate} />}
                            key={item.key}
                            activeClassName="ant-menu-item-selected"
                        >
                            {item.icons}
                            <span>{item.name}</span>
                        </NavLink>
                    )
                }
                )}
            </Menu>

The NavLink component will pass navigate prop to Menu.Item, we need to map it to onClick prop and click behaviour will work correctly.

Gathard answered 17/6, 2022 at 8:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.