3

I am making a menu and submenus using recursion function and I am in the need of help to open only the respective menu and sub menu's..

For button and collapse Reactstrap has been used..

Recursive function that did menu population:

{this.state.menuItems &&
          this.state.menuItems.map((item, index) => {
            return (
              <div key={item.id}>
                <Button onClick={this.toggle.bind(this)}> {item.name} </Button>
                <Collapse isOpen={this.state.isToggleOpen}>
                  {this.buildMenu(item.children)}
                </Collapse>
              </div>
            );
          })}

And the buildMenu function as follows,

  buildMenu(items) {
    return (
      <ul>
        {items &&
          items.map(item => (
            <li key={item.id}>
              <div>
                {this.state.isToggleOpen}
                <Button onClick={this.toggle.bind(this)}> {item.name} </Button>
                <Collapse isOpen={this.state.isToggleOpen}>
                  {item.children && item.children.length > 0
                    ? this.buildMenu(item.children)
                    : null}
                </Collapse>
              </div>
            </li>
          ))}
      </ul>
    );
  }

There is no problem with the code as of now but I am in the need of help to make menu -> submenu -> submenu step by step open and closing respective levels.

Working example: https://codesandbox.io/s/reactstrap-accordion-9epsp

You can take a look at this example that when you click on any menu the whole level of menus gets opened instead of clicked one..

Requirement

If user clicked on menu One, then the submenu (children)

-> One-One 

needs to get opened.

And then if user clicked on One-One,

 ->   One-One-One
 ->   One - one - two
 ->   One - one - three

needs to get opened.

Likewise it is nested so after click on any menu/ children their respective next level needs to get opened.

I am new in react and reactstrap way of design , So any help from expertise would be useful for me to proceed and learn how actually it needs to be done.

Daniel Widdis
  • 8,424
  • 13
  • 41
  • 63

2 Answers2

1

Instead of using one large component, consider splitting up your component into smaller once. This way you can add state to each menu item to toggle the underlying menu items.

If you want to reset al underlying menu items to their default closed position you should create a new component instance each time you open up a the underlying buttons. By having <MenuItemContainer key={timesOpened} the MenuItemContainer will be assigned a new key when you "open" the MenuItem. Assigning a new key will create a new component instance rather than updating the existing one.

For a detailed explanation I suggest reading You Probably Don't Need Derived State - Recommendation: Fully uncontrolled component with a key.

const loadMenu = () => Promise.resolve([{id:"1",name:"One",children:[{id:"1.1",name:"One - one",children:[{id:"1.1.1",name:"One - one - one"},{id:"1.1.2",name:"One - one - two"},{id:"1.1.3",name:"One - one - three"}]}]},{id:"2",name:"Two",children:[{id:"2.1",name:"Two - one"}]},{id:"3",name:"Three",children:[{id:"3.1",name:"Three - one",children:[{id:"3.1.1",name:"Three - one - one",children:[{id:"3.1.1.1",name:"Three - one - one - one",children:[{id:"3.1.1.1.1",name:"Three - one - one - one - one"}]}]}]}]},{id:"4",name:"Four"},{id:"5",name:"Five",children:[{id:"5.1",name:"Five - one"},{id:"5.2",name:"Five - two"},{id:"5.3",name:"Five - three"},{id:"5.4",name:"Five - four"}]},{id:"6",name:"Six"}]);

const {Component, Fragment} = React;
const {Button, Collapse} = Reactstrap;

class Menu extends Component {
  constructor(props) {
    super(props);
    this.state = {menuItems: []};
  }

  render() {
    const {menuItems} = this.state;
    return <MenuItemContainer menuItems={menuItems} />;
  }

  componentDidMount() {
    loadMenu().then(menuItems => this.setState({menuItems}));
  }
}

class MenuItemContainer extends Component {
  render() {
    const {menuItems} = this.props;
    if (!menuItems.length) return null;
    return <ul>{menuItems.map(this.renderMenuItem)}</ul>;
  }
  
  renderMenuItem(menuItem) {
    const {id} = menuItem;
    return <li key={id}><MenuItem {...menuItem} /></li>;
  }
}
MenuItemContainer.defaultProps = {menuItems: []};

class MenuItem extends Component {
  constructor(props) {
    super(props);
    this.state = {isOpen: false, timesOpened: 0};
    this.open = this.open.bind(this);
    this.close = this.close.bind(this);
  }

  render() {
    const {name, children} = this.props;
    const {isOpen, timesOpened} = this.state;
    return (
      <Fragment>
        <Button onClick={isOpen ? this.close : this.open}>{name}</Button>
        <Collapse isOpen={isOpen}>
          <MenuItemContainer key={timesOpened} menuItems={children} />
        </Collapse>
      </Fragment>
    );
  }

  open() {
    this.setState(({timesOpened}) => ({
      isOpen: true,
      timesOpened: timesOpened + 1,
    }));
  }
  
  close() {
    this.setState({isOpen: false});
  }
}

ReactDOM.render(<Menu />, document.getElementById("root"));
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reactstrap/8.4.1/reactstrap.min.js"></script>

<div id="root"></div>
3limin4t0r
  • 19,353
  • 2
  • 31
  • 52
  • I am making it as accepted but I am also having an issue here as like I am mentioned in the other solution.. If I open upto last level menu opening wise it is working fine first time.. Then if I close it at say (second level), and again if I click on the seond level, the next level only should open but it opens until the last level.. So closing the menus in one level and again opening showing all the levels instead of next level.. Please look into it and update your answer –  Mar 10 '20 at 02:43
  • @TestUser I've updated the answer based upon the requirements. I do feel like the ref solution is not the best way to go, but it's late and this was the easy way to go. I'll rewatch the problem tomorrow to see if I can find something better. – 3limin4t0r Mar 10 '20 at 04:20
  • Yes please update the solution if possible with the best method.. Thanks in advance.. –  Mar 10 '20 at 05:07
  • Can you help me in this question please?? https://stackoverflow.com/questions/60616336/open-the-collapsible-menu-by-default-based-on-the-id –  Mar 10 '20 at 12:29
  • @TestUser I've refactored the solution to use a "Fully uncontrolled component with a key" solution. Check out the link provided in the answer. I'll have a look at the new question later today. – 3limin4t0r Mar 10 '20 at 14:20
  • Okay I will look into this, kindly look into the new question once you are free to do.. –  Mar 10 '20 at 16:19
0

You will want to create an inner component to manage the state at each level.

For example, consider the following functional component (I'll leave it to you to convert to class component):

const MenuButton = ({ name, children }) => {
  const [open, setOpen] = useState(false);
  const toggle = useCallback(() => setOpen(o => !o), [setOpen]);
  return (
    <>
      <Button onClick={toggle}>{name}</Button>
      <Collapse open={open}>{children}</Collapse>
    </>
  );
};

This component will manage whether to display its children or not. Use it in place of all of your <div><Button/><Collapse/></div> sections, and it will manage the open state for each level.

Keep shared state up at the top, but if you don't need to know whether something is expanded for other logic, keep it localized.

Also, if you do need that info in your parent component, use the predefined object you already have and add an 'open' field to it which defaults to false. Upon clicking, setState on that object to correctly mark the appropriate object to have the parameter of true on open.

Localized state is much cleaner though.

Expanded Example

import React, { Component, useState, useCallback, Fragment } from "react";
import { Collapse, Button } from "reactstrap";
import { loadMenu } from "./service";


const MenuButton = ({ name, children }) => {
  const [open, setOpen] = React.useState(false);
  const toggle = useCallback(() => setOpen(o => !o), [setOpen]);
  return (
    <Fragment>
      <Button onClick={toggle}>{name}</Button>
      <Collapse open={open}>{children}</Collapse>
    </Fragment>
  );
};

class Hello extends Component {
  constructor(props) {
    super(props);
    this.state = {
      currentSelection: "",
      menuItems: [],
    };
  }

  componentDidMount() {
    loadMenu().then(items => this.setState({ menuItems: items }));
  }

  buildMenu(items) {
    return (
      <ul>
        {items &&
          items.map(item => (
            <li key={item.id}>
              <MenuButton name={item.name}>
              {item.children && item.children.length > 0
                    ? this.buildMenu(item.children)
                    : null}
              </MenuButton>
            </li>
          ))}
      </ul>
    );
  }

  render() {
    return (
      <div>
        <h2>Click any of the below option</h2>
        {this.state.menuItems &&
          this.state.menuItems.map((item, index) => {
            return (
              <MenuButton name={item.name}>
                {this.buildMenu(item.children)}
              </MenuButton>
            );
          })}
      </div>
    );
  }
}

export default Hello;

Asher Gunsay
  • 259
  • 1
  • 5
  • Thanks for your suggestion.. Could you please fork my code sandbox and give me right solution which would be really help me to understand better.. –  Mar 09 '20 at 15:13
  • I added an expanded example in my answer. – Asher Gunsay Mar 09 '20 at 15:21
  • Thanks for your help will implement this in my real application.. But for what I have shown here in codesandbox you have made it working so your work is much appreciated and I am going to upvote and accept it.. As you have helpmed me here I think you might also be able solve my another question here https://stackoverflow.com/questions/60600098/recursive-function-in-reactjs .. At the last level of menus I am in the need of checkbox inline with the last level menu items.. –  Mar 09 '20 at 15:52
  • Also I have found a flaw here in your solution, if I open upto last level menu opening wise it is working fine first time.. Then if I close it at say (second level), and again if I click on the seond level, the next level only should open but it opens until the last level.. So closing the menus in one level and again opening showing all the levels instead of next level.. Please look into it and update your answer.. –  Mar 09 '20 at 16:05