默认情况下,根据ID打开可折叠菜单 [英] Open the collapsible menu by default based on the id

查看:75
本文介绍了默认情况下,根据ID打开可折叠菜单的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在制作一个嵌套菜单和子菜单,到目前为止,所有操作都已完成。.现在,我需要使此可折叠菜单默认根据给定的ID打开。.

I am making a nested menu and submenus and everything has been done as of now.. I am now in the need to make this collapsible menu to get opened by default based on the id given..

您还可以查看下面的完整工作代码段,

You could also take a look at the complete working code snippet below,

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 openMenuId = "3.1.1.1";

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

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

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

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

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

class MenuItem extends Component {
  constructor(props) {
    super(props);
    this.state = {isOpen: false};
    this.toggle = this.toggle.bind(this);
  }

  render() {
    let isLastChild = this.props.children ? false : true;
    return (
      <Fragment>
        <Button onClick={this.toggle}>{this.props.name}</Button>
        <Fragment>
          {isLastChild ? <Input type="checkbox" value={this.props.id} /> : ''}
        </Fragment>
        <Collapse isOpen={this.state.isOpen}>
          <MenuItemContainer menuItems={this.props.children} />
        </Collapse>
      </Fragment>
    );
  }

  toggle() {
    this.setState(({isOpen}) => ({isOpen: !isOpen}));
  }
}

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>

要求:

我的ID值存储在父组件的 const openMenuId = 3.1.1.1.1; 中(您可以看一下 loadMenu 数组变量下面的变量。)

I am having an id value stored in const openMenuId = "3.1.1.1.1"; in parent component (you can look this variable below loadMenu array variable)..

即使有多个子菜单,此ID也只会是g到最后一级的孩子id,因此肯定会有一个复选框,以便需要选中此复选框,并且需要打开父级菜单。

Even though there are multiple submenus, this id will only belong to last level children id and hence would have a checkbox for sure so that checkbox needs to be checked and the menus up to parent level need to get opened.

例如..

由于openMenuId为 3.1.1.1.1 ,因此很显然,菜单三个的最后一个子级别必须为三-一-一-一对作为 openMenuId 检查,并且复选框值在此处具有匹配项。然后,需要将相应的菜单和子菜单扩展到最后一级。

As the openMenuId is "3.1.1.1.1" and hence it is clear that last child level of menu three which is Three - one - one - one - one needs to be checked as the openMenuId and checkbox value has a match here.. Then the respective menus and submenus need to be expanded up to the last level.

这仅用于所访问页面上的默认行为,因此该用户可以退回并能够选中任何其他菜单中的任何其他复选框。。但是在访问该页面时,我将需要一个特定的ID要默认打开,也需要在复选框中选中。.

This is only for default behavior on the page visited so after that user can collapse back and able to check any other checkboxes in any other menus.. But while visiting the page I will have a particular id that needs to get opened by default and also needs to be checked in the checkbox..

请帮助我通过比较传递给以下用户的ID来获得打开相应菜单的结果道具和制作

Kindly help me to achieve the result of opening the respective menu by comparing the id passed down as props and make the respective menu checked..

苦苦挣扎了很久,所以请帮助我。.提前谢谢。.

Struggling for a long time, So please help me.. A big thanks in advance..

推荐答案

真是个好问题!我真的很喜欢为此提供解决方案。

What a great question! I really enjoyed coming up with a solution for this one.

由于您想同时为菜单状态和复选框状态赋予初始状态,因此我认为控制在< Menu> 级别上(甚至更高!)的两个状态都是一个好主意。这样不仅可以轻松地从父级定义初始状态,而且还可以在将来需要更复杂的菜单或复选框行为时为您提供更大的灵活性。

As you wanted to give an initial state to both the menu state and the checkbox state, I think that controlling the state of both on the <Menu> level (or even higher!) is a good idea. This not only makes it easy to define an initial state from a parent, but it also grants you more flexibility if you need some more complicated menu or checkbox behavior in the future.

由于菜单的结构是递归的,所以我认为为菜单状态设置递归结构效果很好。在我进入代码之前,这里有一个简短的GIF,我希望可以帮助解释状态:

Since the structure of the menus is recursive, I think that having a recursive structure for the menu state works pretty well. Before I go into the code, here's a short GIF which, I hope, helps explain what the state looks like:

这是游乐场代码段:

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, Input } = Reactstrap;

const replaceNode = (replacer, node, idPath, i) => {
  if (i <= idPath.length && !node) {
    // Not at target node yet, create nodes in between
    node = {};
  }
  if (i > idPath.length) {
    // Reached target node
    return replacer(node);
  }

  // Construct ID that matches this depth - depth meaning
  // the amount of dots in between the ID
  const id = idPath.slice(0, i).join(".");
  return {
    ...node,
    // Recurse
    [id]: replaceNode(replacer, node[id], idPath, i + 1)
  };
};

const replaceNodeById = (node, id, visitor) => {
  // Pass array of the id's parts instead of working on the string
  // directly - easy way to handle multi-number ID parts e.g. 3.1.15.32
  return replaceNode(visitor, node, id.split("."), 1);
};

const expandedNode = () => ({});
const unexpandedNode = () => undefined;

const toggleNodeById = (node, id) =>
  replaceNodeById(node, id, oldNode =>
    oldNode ? unexpandedNode() : expandedNode()
  );
const expandNodeById = (node, id) =>
  replaceNodeById(node, id, oldNode => expandedNode());

class Menu extends Component {
  constructor(props) {
    super(props);
    this.state = {
      menuItems: [],
      openMenus: {},
      checkedMenus: {}
    };
    this.handleMenuToggle = this.handleMenuToggle.bind(this);
    this.handleChecked = this.handleChecked.bind(this);
  }

  render() {
    const { menuItems, openMenus, checkedMenus } = this.state;

    return (
      <div
        style={{
          display: "flex",
          flexDirection: "row",
          columnCount: 3,
          justifyContent: "space-between"
        }}
      >
        <div style={{ paddingTop: "10px" }}>
          <MenuItemContainer
            openMenus={openMenus}
            menuItems={menuItems}
            onMenuToggle={this.handleMenuToggle}
            checkedMenus={checkedMenus}
            onChecked={this.handleChecked}
          />
        </div>
        <div style={{ padding: "10px", marginLeft: "auto" }}>
          <p>Menu state</p>
          <pre>{JSON.stringify(openMenus, null, 2)}</pre>
        </div>
        <div style={{ padding: "10px", width: "177px" }}>
          <p>Checkbox state</p>
          <pre>{JSON.stringify(checkedMenus, null, 2)}</pre>
        </div>
      </div>
    );
  }

  componentDidMount() {
    const { initialOpenMenuId, initialCheckedMenuIds } = this.props;

    loadMenu().then(menuItems => {
      const initialMenuState = {};
      this.setState({
        menuItems,
        openMenus: expandNodeById(initialMenuState, initialOpenMenuId),
        checkedMenus: initialCheckedMenuIds.reduce(
          (acc, val) => ({ ...acc, [val]: true }),
          {}
        )
      });
    });
  }

  handleMenuToggle(toggledId) {
    this.setState(({ openMenus }) => ({
      openMenus: toggleNodeById(openMenus, toggledId)
    }));
  }

  handleChecked(toggledId) {
    this.setState(({ checkedMenus }) => ({
      checkedMenus: {
        ...checkedMenus,
        [toggledId]: checkedMenus[toggledId] ? unexpandedNode() : expandedNode()
      }
    }));
  }
}

function MenuItemContainer({
  openMenus,
  onMenuToggle,
  checkedMenus,
  onChecked,
  menuItems = []
}) {
  if (!menuItems.length) return null;

  const renderMenuItem = menuItem => (
    <li key={menuItem.id}>
      <MenuItem
        openMenus={openMenus}
        onMenuToggle={onMenuToggle}
        checkedMenus={checkedMenus}
        onChecked={onChecked}
        {...menuItem}
      />
    </li>
  );

  return <ul>{menuItems.map(renderMenuItem)}</ul>;
}

class MenuItem extends Component {
  constructor(props) {
    super(props);
    this.handleToggle = this.handleToggle.bind(this);
    this.handleChecked = this.handleChecked.bind(this);
  }

  render() {
    const {
      children,
      name,
      id,
      openMenus,
      onMenuToggle,
      checkedMenus,
      onChecked
    } = this.props;

    const isLastChild = !children;
    return (
      <Fragment>
        <Button onClick={isLastChild ? this.handleChecked : this.handleToggle}>
          {name}
        </Button>
        {isLastChild && (
          <Input
            addon
            type="checkbox"
            onChange={this.handleChecked}
            checked={!!checkedMenus[id]}
            value={id}
          />
        )}

        <Collapse isOpen={openMenus ? !!openMenus[id] : false}>
          <MenuItemContainer
            menuItems={children}
            // Pass down child menus' state
            openMenus={openMenus && openMenus[id]}
            onMenuToggle={onMenuToggle}
            checkedMenus={checkedMenus}
            onChecked={onChecked}
          />
        </Collapse>
      </Fragment>
    );
  }

  handleToggle() {
    this.props.onMenuToggle(this.props.id);
  }

  handleChecked() {
    this.props.onChecked(this.props.id);
  }
}

ReactDOM.render(
  <Menu initialOpenMenuId="3.1.1.1" initialCheckedMenuIds={["3.1.1.1.1"]} />,
  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>

下面的代码演练。

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, Input } = Reactstrap;

const replaceNode = (replacer, node, idPath, i) => {
  if (i <= idPath.length && !node) {
    // Not at target node yet, create nodes in between
    node = {};
  }
  if (i > idPath.length) {
    // Reached target node
    return replacer(node);
  }

  // Construct ID that matches this depth - depth meaning
  // the amount of dots in between the ID
  const id = idPath.slice(0, i).join(".");
  return {
    ...node,
    // Recurse
    [id]: replaceNode(replacer, node[id], idPath, i + 1)
  };
};

const replaceNodeById = (node, id, visitor) => {
  // Pass array of the id's parts instead of working on the string
  // directly - easy way to handle multi-number ID parts e.g. 3.1.15.32
  return replaceNode(visitor, node, id.split("."), 1);
};

const expandedNode = () => ({});
const unexpandedNode = () => undefined;

const toggleNodeById = (node, id) =>
  replaceNodeById(node, id, oldNode =>
    oldNode ? unexpandedNode() : expandedNode()
  );
const expandNodeById = (node, id) =>
  replaceNodeById(node, id, oldNode => expandedNode());

class Menu extends Component {
  constructor(props) {
    super(props);
    this.state = {
      menuItems: [],
      openMenus: {},
      checkedMenus: {}
    };
    this.handleMenuToggle = this.handleMenuToggle.bind(this);
    this.handleChecked = this.handleChecked.bind(this);
  }

  render() {
    const { menuItems, openMenus, checkedMenus } = this.state;

    return (
      <MenuItemContainer
        openMenus={openMenus}
        menuItems={menuItems}
        onMenuToggle={this.handleMenuToggle}
        checkedMenus={checkedMenus}
        onChecked={this.handleChecked}
      />
    );
  }

  componentDidMount() {
    const { initialOpenMenuId, initialCheckedMenuIds } = this.props;

    loadMenu().then(menuItems => {
      const initialMenuState = {};
      this.setState({
        menuItems,
        openMenus: expandNodeById(initialMenuState, initialOpenMenuId),
        checkedMenus: initialCheckedMenuIds.reduce(
          (acc, val) => ({ ...acc, [val]: true }),
          {}
        )
      });
    });
  }

  handleMenuToggle(toggledId) {
    this.setState(({ openMenus }) => ({
      openMenus: toggleNodeById(openMenus, toggledId)
    }));
  }

  handleChecked(toggledId) {
    this.setState(({ checkedMenus }) => ({
      checkedMenus: {
        ...checkedMenus,
        [toggledId]: checkedMenus[toggledId] ? unexpandedNode() : expandedNode()
      }
    }));
  }
}

function MenuItemContainer({
  openMenus,
  onMenuToggle,
  checkedMenus,
  onChecked,
  menuItems = []
}) {
  if (!menuItems.length) return null;

  const renderMenuItem = menuItem => (
    <li key={menuItem.id}>
      <MenuItem
        openMenus={openMenus}
        onMenuToggle={onMenuToggle}
        checkedMenus={checkedMenus}
        onChecked={onChecked}
        {...menuItem}
      />
    </li>
  );

  return <ul>{menuItems.map(renderMenuItem)}</ul>;
}

class MenuItem extends Component {
  constructor(props) {
    super(props);
    this.handleToggle = this.handleToggle.bind(this);
    this.handleChecked = this.handleChecked.bind(this);
  }

  render() {
    const {
      children,
      name,
      id,
      openMenus,
      onMenuToggle,
      checkedMenus,
      onChecked
    } = this.props;

    const isLastChild = !children;
    return (
      <Fragment>
        <Button onClick={isLastChild ? this.handleChecked : this.handleToggle}>
          {name}
        </Button>
        {isLastChild && (
          <Input
            addon
            type="checkbox"
            onChange={this.handleChecked}
            checked={!!checkedMenus[id]}
            value={id}
          />
        )}

        <Collapse isOpen={openMenus ? !!openMenus[id] : false}>
          <MenuItemContainer
            menuItems={children}
            // Pass down child menus' state
            openMenus={openMenus && openMenus[id]}
            onMenuToggle={onMenuToggle}
            checkedMenus={checkedMenus}
            onChecked={onChecked}
          />
        </Collapse>
      </Fragment>
    );
  }

  handleToggle() {
    this.props.onMenuToggle(this.props.id);
  }

  handleChecked() {
    this.props.onChecked(this.props.id);
  }
}

ReactDOM.render(
  <Menu initialOpenMenuId="3.1.1.1" initialCheckedMenuIds={["3.1.1.1.1"]} />,
  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>

在开始之前,我必须说我已经自由更改了一些代码以使用现代JavaScript功能,例如对象解构
阵列解构休息默认值

Before I start, I must say that I've taken the liberty to change some of the code to use modern JavaScript features like object destructuring, array destructuring, rest and default values.

因此。由于菜单项的ID是由点分隔的数字,因此在构造状态时可以利用它。状态本质上是一个树状结构,每个子菜单都是其父级的子级,叶子节点(最后一个菜单或最深菜单)的值为 {} (如果已扩展),或者 undefined (如果未扩展)。菜单的初始状态如下:

So. Since the IDs of the menu items are numbers delimited by a dot, we can take advantage of this when constructing the state. The state is essentially a tree-like structure, with each sub-menu being a child of its parent, and the leaf node ("last menu" or "deepest menu") having the value of either {} if it's expanded, or undefined if not. Here's how the initial state of the menu is constructed:

<Menu initialOpenMenuId="3.1.1.1" initialCheckedMenuIds={["3.1.1.1.1"]} />

// ...

loadMenu().then(menuItems => {
  const initialMenuState = {};
  this.setState({
    menuItems,
    openMenus: expandNodeById(initialMenuState, initialOpenMenuId),
    checkedMenus: initialCheckedMenuIds.reduce(
      (acc, val) => ({ ...acc, [val]: true }),
      {}
    )
  });
});

// ...

const expandedNode = () => ({});
const unexpandedNode = () => undefined;

const toggleNodeById = (node, id) =>
  replaceNodeById(node, id, oldNode =>
    oldNode ? unexpandedNode() : expandedNode()
  );
const expandNodeById = (node, id) =>
  replaceNodeById(node, id, oldNode => expandedNode());

const replaceNodeById = (node, id, visitor) => {
  // Pass array of the id's parts instead of working on the string
  // directly - easy way to handle multi-number ID parts e.g. 3.1.15.32
  return replaceNode(visitor, node, id.split("."), 1);
};

const replaceNode = (replacer, node, idPath, i) => {
  if (i <= idPath.length && !node) {
    // Not at target node yet, create nodes in between
    node = {};
  }
  if (i > idPath.length) {
    // Reached target node
    return replacer(node);
  }

  // Construct ID that matches this depth - depth meaning
  // the amount of dots in between the ID
  const id = idPath.slice(0, i).join(".");
  return {
    ...node,
    // Recurse
    [id]: replaceNode(replacer, node[id], idPath, i + 1)
  };
};

让我们一点一点地分开。

Let's take this apart bit by bit.

const expandedNode = () => ({});
const unexpandedNode = () => undefined;

这些只是我们定义的便捷函数,因此我们可以轻松更改用于表示扩展的值和未展开的节点。与仅在代码中使用文字 {} undefined 相比,这也使代码更具可读性。扩展和未扩展的值也可以分别为 true false ,重要的是扩展节点为真实,并且未展开的节点是虚假的。

These are just convenience functions that we define so we can easily change the value we use to represent an expanded and unexpanded node. It also makes the code a little bit more readable compared to just using literal {} or undefined in the code. The expanded and unexpanded values could just as well be true and false, what matters is that the expanded node is truthy and the unexpanded node is falsy. More about that later.

const toggleNodeById = (node, id) =>
  replaceNodeById(node, id, oldNode =>
    oldNode ? unexpandedNode() : expandedNode()
  );
const expandNodeById = (node, id) =>
  replaceNodeById(node, id, oldNode => expandedNode());

这些功能使我们可以在菜单状态下切换或扩展特定菜单。第一个参数是菜单状态本身,第二个参数是菜单的字符串ID(例如 3.1.1.1.1 ),第三个是执行此操作的函数更换。可以将其视为传递给 .map()的函数。替换功能与实际的递归树迭代是分开的,因此您以后可以轻松实现更多功能-例如,如果希望某些特定菜单不展开,则只需传入一个返回 unexpandedNode的函数即可。 ()

These functions let us toggle or expand a specific menu in the menu state. The first parameter is the menu state itself, the second is the string ID of a menu (e.g. "3.1.1.1.1"), and the third is the function that does the replacing. Think of this like the function you pass to .map(). The replacer functionality is separated from the actual recursive tree iteration so that you can easily implement more functionality later - for example, if you want some specific menu to be unexpanded, you can just pass in a function that returns unexpandedNode().

const replaceNodeById = (node, id, visitor) => {
  // Pass array of the id's parts instead of working on the string
  // directly - easy way to handle multi-number ID parts e.g. 3.1.15.32
  return replaceNode(visitor, node, id.split("."), 1);
};

前两个函数使用此功能提供更简洁的界面。此处的ID由点()分隔,这为我们提供了ID部分的数组。下一个函数直接在此数组上运行,而不是直接在ID字符串上运行,因为那样一来,我们就不必执行 .indexOf('。')恶作剧。

This function is used by the two previous ones to provide a cleaner interface. The ID is split here by the dots (.) which gives us an array of the ID parts. The next function operates on this array instead of the ID string directly, because that way we don't need to do .indexOf('.') shenanigans.

const replaceNode = (replacer, node, idPath, i) => {
  if (i <= idPath.length && !node) {
    // Not at target node yet, create nodes in between
    node = {};
  }
  if (i > idPath.length) {
    // Reached target node
    return replacer(node);
  }

  // Construct ID that matches this depth - depth meaning
  // the amount of dots in between the ID
  const id = idPath.slice(0, i).join(".");
  return {
    ...node,
    // Recurse
    [id]: replaceNode(replacer, node[id], idPath, i + 1)
  };
};

replaceNode 函数是物。它是一个递归函数,可从旧菜单树生成新树,并用提供的替换器函数替换旧目标节点。如果树缺少中间的部分,例如当树是 {} 但我们要替换节点 3.1.1.1 时,它会在两者之间创建父节点。如果您熟悉该命令,则有点像 mkdir -p

The replaceNode function is the meat of the matter. It is a recursive function that produces a new tree from the old menu tree, replacing the old target node with the provided replacer function. If the tree is missing parts from in between, e.g. when the tree is {} but we want to replace the node 3.1.1.1, it creates the parent nodes in between. Kind of like mkdir -p if you're familiar with the command.

这就是菜单状态。复选框状态( checkedMenus )基本上只是一个索引,键为ID,值为 true 。项目已检查。此状态不是递归的,因为不需要取消选中或递归检查它们。如果您决定要显示一个指示符,表明该菜​​单项下的某些内容已选中,那么一个简单的解决方案是将复选框状态更改为与菜单状态类似的递归状态。

So that's the menu state. The checkbox state (checkedMenus) is basically just an index, with the key being the ID and the value true if an item is checked. This state is not recursive, since they don't need to be unchecked or checked recursively. If you decide that you want to display an indicator that something under this menu item is checked, an easy solution would be to change the checkbox state to be recursive like the menu state.

< Menu> 组件将状态传递到 < MenuItemContainer> ,它呈现< MenuItem> s。

The <Menu> component passes down the states to <MenuItemContainer>, which renders the <MenuItem>s.

function MenuItemContainer({
  openMenus,
  onMenuToggle,
  checkedMenus,
  onChecked,
  menuItems = []
}) {
  if (!menuItems.length) return null;

  const renderMenuItem = menuItem => (
    <li key={menuItem.id}>
      <MenuItem
        openMenus={openMenus}
        onMenuToggle={onMenuToggle}
        checkedMenus={checkedMenus}
        onChecked={onChecked}
        {...menuItem}
      />
    </li>
  );

  return <ul>{menuItems.map(renderMenuItem)}</ul>;
}

< MenuItemContainer> 组件与原始组件没有太大区别。不过,< MenuItem> 组件的确有些不同。

The <MenuItemContainer> component is not very different from the original component. The <MenuItem> component does look a little bit different, though.

class MenuItem extends Component {
  constructor(props) {
    super(props);
    this.handleToggle = this.handleToggle.bind(this);
    this.handleChecked = this.handleChecked.bind(this);
  }

  render() {
    const {
      children,
      name,
      id,
      openMenus,
      onMenuToggle,
      checkedMenus,
      onChecked
    } = this.props;

    const isLastChild = !children;
    return (
      <Fragment>
        <Button onClick={isLastChild ? this.handleChecked : this.handleToggle}>
          {name}
        </Button>
        {isLastChild && (
          <Input
            addon
            type="checkbox"
            onChange={this.handleChecked}
            checked={!!checkedMenus[id]}
            value={id}
          />
        )}

        <Collapse isOpen={openMenus ? !!openMenus[id] : false}>
          <MenuItemContainer
            menuItems={children}
            // Pass down child menus' state
            openMenus={openMenus && openMenus[id]}
            onMenuToggle={onMenuToggle}
            checkedMenus={checkedMenus}
            onChecked={onChecked}
          />
        </Collapse>
      </Fragment>
    );
  }

  handleToggle() {
    this.props.onMenuToggle(this.props.id);
  }

  handleChecked() {
    this.props.onChecked(this.props.id);
  }
}

这里的关键部分是: openMenus = {openMenus& openMenus [id]} 。我们没有传递整个菜单状态,而只是传递了包含当前项目子项的状态树。这使组件可以非常容易地检查它是否应该打开或折叠-只需检查是否从对象中找到了自己的ID( openMenus?!! openMenus [id]:false )!

Here the crucial part is this: openMenus={openMenus && openMenus[id]}. Instead of passing down the entire menu state, we only pass down the state tree which contains the current item's children. This allows the component to very easily check if it should be open or collapsed - just check if its own ID is found from the object (openMenus ? !!openMenus[id] : false)!

我还更改了切换按钮,以切换复选框而不是菜单状态(如果它是菜单中最深的项)-如果这不是您所需要的

I also changed the toggle button to toggle the checkbox instead of the menu state if it's the deepest item in the menu - if this is not what you're looking for, it's pretty quick to change back.

我还利用 !! 在这里强制 {} undefined 从菜单状态更改为 true false 。这就是为什么我说只重要的是他们是真实的还是虚假的。 restraps 组件似乎想要显式 true false

I also make use of !! here to coerce {} and undefined from the menu state into true or false. This is why I said it only matters whether they're truthy or falsy. The reactstrap components seemed to want explicit true or false instead of truthy/falsy, so that's why it's there.

最后是:

ReactDOM.render(
  <Menu initialOpenMenuId="3.1.1.1" initialCheckedMenuIds={["3.1.1.1.1"]} />,
  document.getElementById("root")
);

在这里,我们将初始状态传递给< Menu> initialOpenMenuId 也可以是一个数组(或者 initialCheckedMenuIds 可以是单个字符串),但这符合问题的规范。

Here we pass the initial state to <Menu>. The initialOpenMenuId could also be an array (or initialCheckedMenuIds could be a single string), but this fit the question's spec.

The solution right now passes down lots of state all the way down, such as the onMenuToggle and onChecked callbacks, and the checkedMenus state which is not recursive. These could make use of React’s Context.

The solution right now passes down lots of state all the way down, such as the onMenuToggle and onChecked callbacks, and the checkedMenus state which is not recursive. These could make use of React's Context.

这篇关于默认情况下,根据ID打开可折叠菜单的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆