Chris G.
Chris G.

Reputation: 25954

react-bootstrap how to collapse menu when item is selected

How do you make the menu collapse after item is selected?

I dont know how to make it work on fiddle, but this is what I would do? https://jsfiddle.net/vjeux/kb3gN/

import React from 'react';
import {Navbar, Nav, NavItem, NavDropdown, DropdownButton, MenuItem, CollapsibleNav} from 'react-bootstrap';

export default class App extends React.Component {

    constructor(props) {
      super(props);
      this.onSelect = this.onSelect.bind(this);
      this.toggleNav = this.toggleNav.bind(this);
      // this.state = {navExpanded: false};
    }

    onSelect(e){
        console.log('OnSelect')
        // console.log(this.state.navExpanded);
        // this.setState({navExpanded: false});
    }

    toggleNav(){console.log('toggle...')};

    // <Navbar inverse fixedTop toggleNavKey={0} navExpanded={this.state.navExpanded} onToggle={() => this.toggleNav()}>
    // <Navbar inverse fixedTop toggleNavKey={0} navExpanded={this.state.navExpanded} >

    render() {
        return (

          <Navbar inverse fixedTop toggleNavKey={0} >
            <Nav right eventKey={0} onSelect={this.onSelect} > {/* This is the eventKey referenced */}
              <NavItem eventKey={1} href="#">Link</NavItem>
              <NavItem eventKey={2} href="#">Link</NavItem>
            </Nav>
          </Navbar>     

      )

    }

    componentDidMount() {

    }
}

React.render(<App />, document.getElementById('example'));

Upvotes: 32

Views: 57329

Answers (16)

Josep Vidal
Josep Vidal

Reputation: 2661

For anyone coming here in 2020 and using Hooks, maybe you are using react-router, and as result, instead of the Nav.Link that are the default component for the Navbar you use Link from react-router.

And what did you find? That in result the mobile menu is not working as expected and not closing after clicking on a link, and nothing seems to work.

Here is my "simple" solution (Using hooks) to that problem:

First we set up a hook:

const [expanded, setExpanded] = useState(false);

Second in the Navbar we add this prop:

<Navbar expanded={expanded}>

Now we have control over the visibilty of the menu, in the "first" load it will be hidden.

Third we add an onClick event to the toggle handler that changes the menu visibility status:

<Navbar.Toggle onClick={() => setExpanded(!expanded)} />

Fourth we add the prop onClick={() => setExpanded(false)} to all our Link components from react-router inside our Navbar.

Profit! I swear that after more than 1 hour wandering for the internet is the easiest and cleanest solution I found.

Upvotes: 83

Mahdi Hasanzadeh
Mahdi Hasanzadeh

Reputation: 1

It is very easy to solve this problem.In React,if you use a Link,add an onClick event on each link and then inside the the function just write this code: doucument.getElementById("nav").classList.remove("show"); "nav":Id of the the element that collapse the navbar.

Upvotes: 0

A modification of Ricky's solution is to add collapseOnSelect on the Navbar element

<Navbar collapseOnSelect ...>

AND add the eventKey={key} prop to each Nav.Link.

<Nav.Link as={Link} to="/page1" eventKey={1}>Page 1</Nav.Link>

Answer as per React-Bootstrap documentation on responsive Navbar

Upvotes: 0

Jose Arturo Paz
Jose Arturo Paz

Reputation: 1

Into Navbar Component initialize an new State:

const [expanded, setExpanded] = useState('');

Add expanded state in Navbar component:

<Navbar expanded={expanded} ... >

After create click event in <Navbar.Toggle /> component:

<NavbarB.Toggle aria-controls='basic-navbar-nav' onClick={() => setExpanded(prev => prev === '' ? 'expanded' : '')} />

you should to use a useLocation hook of react-router-dom into an useEffect hook so that when a route change is detected, the Navbar menu is hidden.

Full example Here:

import { useEffect, useState } from 'react'
import {Navbar} from 'react-bootstrap'
import {Link, useLocation} from 'react-router-dom'

const NavbarComponent = () => {
   const [expanded, setExpanded] = useState('');
   const {pathname} = useLocation();

   useEffect(() => {
      // expanded closed when pathname is changed
      setExpanded('')
   }, [pathname])

   return (
      <Navbar expanded={expanded}>
         <Navbar.Brand as={Link} to='/'>
            <img src={Logo} alt='Logo' />
        </Navbar.Brand>
        <Navbar.Toggle
            aria-controls='basic-navbar-nav'
            onClick={() => setExpanded(prev => prev === '' ? 'expanded' : '')}
        />
        <Navbar.Collapse id='basic-navbar-nav'>
            <Nav className='me-auto'>
               <Nav.Link as={Link} to={'/home'}>Home</Nav.Link>
            </Nav>
         </Navbar.Collapse>
      </Navbar>
   )
}
 

Upvotes: 0

jayweezy
jayweezy

Reputation: 121

@Josep Vidal's solution works almost perfectly for me. But, what happens when the user changes their mind after already opening the navigation menu and doesn't want to select a link anymore? Clicking outside of the Navbar menu doesn't work anymore.

Solution: After using @Josep Vidal's solution, you will need to also wrap the whole Navbar element in a div with following:

<div onClick={() => (expanded ? setExpanded(false) : false) }> {/*Navbar element*/} </div>

Upvotes: 0

Ricky
Ricky

Reputation: 7879

There are many answers on here, but the fix as of today is very simple. There is no need to use state or click events. This is assuming you're rendering as Link for each Nav.Link.

Add the collapseOnSelect prop here.

<Navbar collapseOnSelect expand="false">

AND add the eventKey prop to each Nav.Link.

<Nav.Link as={Link} to="/page1" eventKey="1">Page 1</Nav.Link>
<Nav.Link as={Link} to="/page2" eventKey="2">Page 2</Nav.Link>

Upvotes: 14

mr.saraan
mr.saraan

Reputation: 65

This is an old question and I don't know that this option was available at that time or not, but use collapseOnSelect in your navbar component. This is the Right and very quick solution!

<Navbar **collapseOnSelect** className="header" bg="light" expand="lg">
      <Nav.Link className="navbar-link navbar-link-header" as={NavLink} to="/">
        <Navbar.Brand>
         
        </Navbar.Brand>
      </Nav.Link>

      <Navbar.Toggle aria-controls="basic-navbar-nav" />

      <Navbar.Collapse id="basic-navbar-nav">
        <Nav className="me-auto">
          <Nav.Link className="navbar-link" as={NavLink} to="/resume">
        
          </Nav.Link>
          <Nav.Link className="navbar-link" as={NavLink} to="/portfolio">
         
          </Nav.Link>
          <Nav.Link className="navbar-link" as={NavLink} to="/blogs">
         
          </Nav.Link>
          <Nav.Link className="navbar-link" as={NavLink} to="/contact">
        
          </Nav.Link>
        </Nav>
      </Navbar.Collapse>

    </Navbar>

Upvotes: 0

pr0mpT_07
pr0mpT_07

Reputation: 184

Depending on above answers and some best practices online I came up with some great code and sharing the same here. Bellow is just a Navbar component one can easily pick this one and use as it is it is responsive media set to a width of 500px

import './navbar.css'
import { useState, useEffect } from 'react'
import { Route, BrowserRouter as Router, Switch, Link} from 'react-router-dom'
import Home from './Pages/Home'
import About from './Pages/About'
import Contact from './Pages/Contact'
import Product from './Pages/Product'

const Navbar = () => {
    const [screenWidth, setScreenWidth] = useState(window.innerWidth)
    const [toggleMenu, setToggleMenu] = useState(false)

    function toggle(){
        setToggleMenu( preValue => (!preValue))
    }
     
    useEffect(() => {

        const changeWidth = () => {
            setScreenWidth(window.innerWidth);
          }
          window.addEventListener('resize', changeWidth)

        return () => {
            window.removeEventListener('resize', changeWidth)
        }

    },[])

    return (
        <Router>
            <nav>
                {// better to use Link component instead of href as Link will keep entire operation at client side and not reeload the page
                }
            {
                (toggleMenu || screenWidth >= 500 ) && (
                <ul className='list'>
                <li className='items'><Link onClick={() => setToggleMenu(false)} to='/'>Home</Link></li>
                <li className='items'><Link onClick={() => setToggleMenu(false)} to='/about'>About Us</Link></li>
                <li className='items'><Link onClick={() => setToggleMenu(false)} to='/product'>Product</Link></li>
                <li className='items'><Link onClick={() => setToggleMenu(false)} to='/contact'>Contact Us</Link></li>
            </ul>
                )

            }
            <button className='btn'
            onClick={toggle}>Btn</button>
        </nav>

        <Switch>
            <Route path='/contact'><Contact /></Route>
            <Route path='/product'><Product /></Route>
            <Route path='/about'><About /></Route>
            <Route path='/'><Home /></Route>
        </Switch>
        </Router>
    )
}

export default Navbar

Upvotes: 1

Tony
Tony

Reputation: 289

For those who use react hooks , base on @Alongkorn Chetasumon above:

  const wrapperRef = useRef(null);
  const [navExpanded, setNavExpanded] = useState();
  const closeNav =()=> {
    setNavExpanded(false);
  }
  useEffect(() => {
    /**
     * Alert if clicked on outside of element
     */
    function handleClickOutside(event) {
      if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
        //alert("You clicked outside of me!");
        closeNav();
      }
    }

    // Bind the event listener
    document.addEventListener("mousedown", handleClickOutside);
    return () => {
        // Unbind the event listener on clean up
        document.removeEventListener("mousedown", handleClickOutside);
    };
  }, [wrapperRef]);

Upvotes: 1

user3004118
user3004118

Reputation: 125

Here is my solution, which is using native Bootstrap menu functionality. The reason is that there is more related behaviour when collapsing a menu, for instance changing the hamburger menu icon as that has a toggable class "collapsed" and a boolean also for attribute area-expanded, for that reason.

So... we let the default Bootstrap functionality handle that part, and we just need to add functionality to collapse the menu using the same functionality, but only it should only trigger when the menu is expanded (otherwise we have an unwanted conflicting behaviour.)

We need a const "expanded" to determine if the status of the collapsable/ expandable menu, to prevent it from expanding when the site is viewed on a desktop device and the full menu is already visible.

const [expanded, setExpanded] = useState("")

This is the only const we set with a click event on the navbar-toggler button. It's either expanded or not expanded (we handle this) and collapsed or not collapsed (Bootstrap handles this.)

<button
    className={"navbar-toggler collapsed" + expanded}
    type="button"
    data-bs-toggle="collapse"
    data-bs-target="#navbar"
    aria-controls="navbar"
    aria-expanded="false"
    aria-label="Toggle navigation"
    onClick={() => setExpanded(expanded ? "" : " expanded")}
>

We now have a state of expanded only when the menu is really expanded, and since the navbar-toggler button only shows up on mobile devices (depending on your configuration) there will be no conflicts on a desktop device.

Now we ony need to use default Bootstrap functionality to toggle the menu, so on each item you want to behave as a toggler you have to add the functionality. This will handle all Bootstrap behaviour so we don't need additional hooks.

<li className="nav-item" data-bs-toggle={expanded && "collapse"} data-bs-target="#navbar" aria-controls="navbar" onClick={() => setExpanded("")}>

What this does is triggering the toggle-functionality only when the menu is expanded, thus only on mobile devices. No conflicts whatsoever.

I would not recommend adding this to the container div because that will also trigger the toggle if the menu contains a menu with sub-items or maybe a search form.

Looking forward to comments and improvements.

Upvotes: 1

Shalom Dahan
Shalom Dahan

Reputation: 384

import React, { Component } from "react";
import { Navbar, Nav, Container } from 'react-bootstrap';
import logo from '../assets/logo.png';
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";

export default class Menu extends Component {
  constructor(props) {
    super(props);

    this.state = {
      navExpanded: false
    };
  }

  setNavExpanded = (expanded) => {
    this.setState({ navExpanded: expanded });
  }

  setNavClose = () => {
    this.setState({ navExpanded: false });
  }

  render() {
    return (
      <Navbar bg="dark" variant="dark" expand="lg" onToggle={this.setNavExpanded} expanded={this.state.navExpanded}>
        <Container>
          <Navbar.Brand>
              <Link to={"/"} className="navbar-brand"><img src={logo} height="30"></img></Link>
          </Navbar.Brand>          
          <Navbar.Toggle aria-controls="basic-navbar-nav" />
          <Navbar.Collapse id="basic-navbar-nav">
            <Nav className="mr-auto" onClick={this.setNavClose}>
              <Link to={"/"} className="nav-link">Home</Link>
              <Link to={"/"} className="nav-link">Contact</Link>
            </Nav>          
          </Navbar.Collapse>
        </Container>
      </Navbar>
    );
  }
}

Upvotes: 5

Filip Filipovic
Filip Filipovic

Reputation: 151

I like the post from @Josep Vidal, because I had the same problem, however, I have a small change for his answer. While his approach is really the best one at this point, I believe that the function would be cleaner if instead of

<Navbar.Toggle onClick={() => setExpanded(expanded ? false : "expanded")} />

we did

<Navbar.Toggle onClick={() => setExpanded((prevExpanded) => (prevExpanded = !prevExpanded)) />

also, no need to put the onClick={() => setExpanded(false)} to all our Link components, we can simply put it once on the parent Nav

Upvotes: 4

Vadim Bocharov
Vadim Bocharov

Reputation: 1

For me works the decision of Josef I implemented all four points. But I have some other makeup:

            <Navbar expanded={expanded} bg="light" expand="lg" className="p-3">
            <Navbar.Brand href="/"><img src={CarLogo} alt="Tim-Tim auto" title="Tim-Tim auto" /></Navbar.Brand>
            <Navbar.Toggle onClick={() => setExpanded(expanded ? false : "expanded")} aria-controls="basic-navbar-nav" />
                <Navbar.Collapse id="basic-navbar-nav" className="justify-content-end">
                    <Nav> 
                        <Nav.Item><NavLink onClick={() => setExpanded(false)} exact to='/'>Home</NavLink></Nav.Item>
                        <Nav.Item><NavLink onClick={() => setExpanded(false)} to='/Cars'>Cars</NavLink></Nav.Item>
                        <Nav.Item><NavLink onClick={() => setExpanded(false)} to='/about'>About</NavLink></Nav.Item>
                        <Nav.Item><NavLink onClick={() => setExpanded(false)} to='/news'>News</NavLink></Nav.Item>
                        <Nav.Item><NavLink onClick={() => setExpanded(false)} to='/contacts'>Contacts</NavLink></Nav.Item>
                    </Nav>
                </Navbar.Collapse>
            </Navbar>

Upvotes: 0

KARPOLAN
KARPOLAN

Reputation: 3595

<Navbar collapseOnSelect ... > should do the job, but it works unstable :(

Upvotes: 5

madhur
madhur

Reputation: 1001

Slightly related to issue, might be helpful for someone This is what I did for closing navbar when clicked outside the menu

class Menu extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isNavExpanded: false
    };
  
    this.setIsNavExpanded = (isNavExpanded) => {
      this.setState({ isNavExpanded: isNavExpanded });
    }
  
    this.handleClick = (e) => {
      if (this.node.contains(e.target)) {
        // if clicked inside menu do something
      } else {
        // If clicked outside menu, close the navbar.
        this.setState({ isNavExpanded: false });
      }
    }
  }
  componentDidMount() {
    document.addEventListener('click', this.handleClick, false);      
  }

  componentWillUnmount() {
    document.removeEventListener('click', this.handleClick, false);
  }

  render() {
    return (
      <div ref={node => this.node = node}
        <Navbar collapseOnSelect
           onToggle={this.setIsNavExpanded}
           expanded={this.state.isNavExpanded}
           >
          <Navbar.Collapse>
            // Nav Items
          </Navbar.Collapse>
        </Navbar>
      </div>
    )
  }

Upvotes: 7

Alongkorn
Alongkorn

Reputation: 4197

i have found the solution from this link https://github.com/react-bootstrap/react-bootstrap/issues/1301

i will put sample code of the link above here

const Menu = React.createClass ({
  getInitialState () {
    return {
      navExpanded: false
    }
  },

  setNavExpanded(expanded) {
    this.setState({ navExpanded: expanded });
  },

  closeNav() {
    this.setState({ navExpanded: false });
  },

  render() {
    return (
      <div>
        <Navbar onToggle={this.setNavExpanded}
                expanded={this.state.navExpanded}>
          <Navbar.Header>
            <Navbar.Brand>
              <Link to="some url">Main</Link>
            </Navbar.Brand>
            <Navbar.Toggle />
          </Navbar.Header>
          <Navbar.Collapse>
            <Nav onSelect={this.closeNav}>
              { this.renderMenuItem() }
            </Nav>
          </Navbar.Collapse>
        </Navbar>
      </div>
    )
  }

Upvotes: 20

Related Questions