0

I have the following code which basically collapses all the submenu and only opens them when clicked:

<ul class="menu">
  <li>
    <a href="#">One</a>
    <ul class="menu">
      <li>
        <a href="#">Alpha</a>
        <ul class="menu">
          <li><a href="#">Hello</a></li>
          <li><a href="#">World</a></li>
        ...
      <li><a href="#">Beta</a></li>
    </ul>
  </li>
  <li><a href="#">Three</a></li>
</ul>

<script type="text/javascript">
  $(".menu").children().click(function(e) {
    $(this).siblings().find("ul").hide();
    $(this).children().show();
  });
</script>

I have used recursive function to create a menu with submenu API with DRF, and have successfully managed to display a menu from that API with the following code:

class Recursive extends React.Component {
    render() {
        let childitems = null;

        if (this.props.children) {
            childitems = this.props.children.map(function(childitem) {
                return (
                    <Recursive item={childitem} children={childitem.children} />
                )
            })
        }
        return (
            <li key={this.props.item.id}>
                {this.props.item.name}
                { childitems ? <ul>{childitems}</ul> : null }
            </li>
        );
    }
}


class MenuDisplay extends React.Component {
    render() {
        let items = this.props.data.map(function(item) {
            return (
                <Recursive item={item} children={item.children} />
            )
        })
        return (
            <ul>
                {items}
            </ul>
        );
    }
}

How can I refactor my React code to include the function I have created with jQuery in the first example?

Here's the JSfiddle for the HTML.

MiniGunnR
  • 5,590
  • 8
  • 42
  • 66

3 Answers3

2

Your recursive Menu component can be reduced to a single component as simple as the following:

import React, { Component } from "react";

class Menu extends Component {
    state = {
        // initially no item is active
        active: null
    };

    // when clicked store the item id as active
    handleClick = id => this.setState({ active: id });

    render() {
        const { links } = this.props;
        const { active } = this.state;

        return (
            <ul className="menu">
                {links.map(({ id, name, children }) => (
                    <li key={id} onClick={() => this.handleClick(id)}>
                        {name}
                        {/* only render the children links when */}
                        {/* the item is the active one and the */}
                        {/* item actually has children */}
                        {id === active && children && (
                            <Menu links={children} />
                        )}
                    </li>
                ))}
            </ul>
        );
    }
}

And use it like:

const LINKS = [
    {id: 'one', name: 'One', children: [
        {id: 'alpha', name: 'Alpha', children: [
            {id: 'hello', name: 'Hello'},
            {id: 'world', name: 'World'},
        ]},
        {id: 'beta', name: 'Beta'},
    ]},
    {id: 'two', name: 'Two'},
]

ReactDOM.render(<Menu links={LINKS} />, document.getElementById('root'));

Check out the working example to see it in action:

Edit jn6vqn5jzy

The jQuery functionality is achieved by making each recursive menu component store the active link as state and only render children of that link for the active item.

Note that you need not hide elements with react. You do not render them in the first place if they shouldn't be displayed. In this example this is checked by comparing the active id with the id of the item that should be rendered. If it matches -> render the sub-links.

trixn
  • 15,761
  • 2
  • 38
  • 55
  • Thanks, this is an exact replica of the original menu. One question though. When the ID of a sub menu item is active, shouldn't the ID of that item's parent be inactive and thus not render the sub menu item too? But it doesn't do that, why? – MiniGunnR Jul 04 '18 at 04:10
  • 1
    @MiniGunnR Every level of the menu will render it's own `` component with it's own state. Clicking e.g. on a 3rd level menu item will only affect this levels state. Parent menus will not even notice there was something clicked. So if you currently see a menu 3 levels deep there are three nested `Menu` components knowing the active item of their own level. To get a better understanding of that you can install the [react-dev-tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi) and inspect the example app. – trixn Jul 04 '18 at 06:18
  • Right, I thought about this approach earlier but didn't proceed because of misunderstanding this concept. Thanks for clearing it up. You're awesome! – MiniGunnR Jul 04 '18 at 06:32
1

I drew up a working example with what I think you want in React because I have no life and nothing to do today :)

In short there are a couple ways to do this but, I think the best way is to make a separate react component that will choose to display or hide children component based on its state. It might look like a lot more code than your jQuery solution but, this may resolve some rendering issues that might come from hiding and showing stuff with CSS. Anyways here's the fiddle link and I'll paste the code below.

class Menu extends React.Component{
    constructor(props){
    super(props);
    this.state = {
        open: false
    }

    this.toggleDropDown = this.toggleDropDown.bind(this);
  }

  toggleDropDown(){
    // this toggles the open state of the menu
    this.setState({open: !this.state.open})
  }

  render(){
    // only display children if open is true
    return(
        <ul className="menu">
        <li onClick={this.toggleDropDown}>         {this.props.title}  </li>
        {(this.state.open)?this.props.children:null}
        </ul>
    )
  }
}

class Hello extends React.Component {
  render() {
    return (
    <div>
      <Menu title="one">
        <Menu title="Alpha">
          <ul>
            <li>Hello</li>
            <li>World</li>
          </ul>
        </Menu>
        <Menu title="Beta">
          <ul>
            <li>Hello</li>
            <li>World</li>
          </ul>
        </Menu>
      </Menu>  
      <Menu title="two">
      <ul>
        <li>Alpha</li>
        <li>Beta</li>
        </ul>
      </Menu>
      <ul className="menu">
      <li>three</li>
      <li>four</li>
      </ul>
    </div>
    );
  }
}

ReactDOM.render(
  <Hello name="World" />,
  document.getElementById('container')
);
Michael Sorensen
  • 1,850
  • 12
  • 20
  • Wow this is fantastic. One problem though. When I click on another item, the other items that are opened are supposed to close. What can I do about that? – MiniGunnR Jun 03 '18 at 22:02
  • Hrmmmmm, that's a bit tougher since it is kind of against the React flow to update sibling components. You can create a parent component that will handle the "open" state of the children `` by passing it down as a prop. That would probably be the best way to do it but, it's a little gross. – Michael Sorensen Jun 03 '18 at 23:53
  • Or, you can cheat a little bit by using some javascript like this https://stackoverflow.com/questions/32553158/detect-click-outside-react-component . I came up with this solution https://jsfiddle.net/6hkppdyx/ but, I would advise against this as it doesn't behave intuitively. – Michael Sorensen Jun 03 '18 at 23:55
  • Thanks man. You've done a lot. Let's see what else I can learn about React to solve this problem. – MiniGunnR Jun 04 '18 at 05:11
0

Based on Michael Sorensen's code, I lifted up the state and modified the code. The run result is almost the same as the jQuery code except that if you click on "one", only the immediate children will show up. I kind of like that, so I left it there. The code is pasted below.

function Menu(props) {
  return (
    <ul className="menu">
      <li onClick={props.onClick}> {props.title} </li>
      {props.open ? props.children : null}
    </ul>
  );
}

class Hello extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      open: Array(2).fill(false),
      open2: Array(2).fill(false)
    };

    this.toggleDropDown = this.toggleDropDown.bind(this);
    this.toggleDropDown2 = this.toggleDropDown2.bind(this);
  }

  toggleDropDown(i) {
    const open = Array(2).fill(false);
    this.setState({
      open: open.fill(true, i, i + 1),
      open2: Array(2).fill(false)
    });
  }
  toggleDropDown2(i) {
    const open2 = Array(2).fill(false);

    this.setState({ open2: open2.fill(true, i, i + 1) });
  }

  render() {
    return (
      <div>
        <Menu
          key="0"
          title="one"
          open={this.state.open[0]}
          onClick={() => this.toggleDropDown(0)}
        >
          <Menu
            key="2"
            title="Alpha"
            open={this.state.open2[0]}
            onClick={() => this.toggleDropDown2(0)}
          >
            <ul>
              <li>Hello</li>
              <li>World</li>
            </ul>
          </Menu>
          <Menu
            key="3"
            title="Beta"
            open={this.state.open2[1]}
            onClick={() => this.toggleDropDown2(1)}
          >
            <ul>
              <li>Hello</li>
              <li>World</li>
            </ul>
          </Menu>
        </Menu>
        <Menu
          key="1"
          title="two"
          open={this.state.open[1]}
          onClick={() => this.toggleDropDown(1)}
        >
          <ul>
            <li>Alpha</li>
            <li>Beta</li>
          </ul>
        </Menu>
        <ul className="menu">
          <li>three</li>
          <li>four</li>
        </ul>
      </div>
    );
  }
}

ReactDOM.render(<Hello name="World" />, document.getElementById("container"));
Hong L
  • 1