【问题标题】:Open the collapsible menu by default based on the id默认根据id打开折叠菜单
【发布时间】:2020-06-22 06:35:21
【问题描述】:

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

你也可以看看下面完整的工作代码sn-p,

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 数组变量下面查看这个变量)..

即使有多个子菜单,此 id 也只会属于最后一级子 id,因此肯定会有一个复选框,因此需要选中复选框,并且需要打开父级菜单。

例如..,

由于 openMenuId 是 "3.1.1.1.1",因此很明显,菜单 three 的最后一个子级别 Three - one - one - one - one 需要检查为 openMenuId 和复选框值在这里匹配.. 然后相应菜单和子菜单需要展开到最后一级。

这仅适用于访问页面上的默认行为,因此在该用户可以折叠回来并能够检查任何其他菜单中的任何其他复选框。但是在访问该页面时,我将有一个需要打开的特定 id默认,也需要在checkbox里打勾。。

请帮助我通过比较作为道具传递的id并检查相应菜单来实现打开相应菜单的结果..

苦苦挣扎了很久,所以请帮帮我..提前非常感谢..

【问题讨论】:

    标签: javascript html reactjs accordion reactstrap


    【解决方案1】:

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

    由于您想为菜单状态和复选框状态提供初始状态,我认为在&lt;Menu&gt; 级别(甚至更高!)上控制两者的状态是个好主意。这不仅使从父级定义初始状态变得容易,而且如果您将来需要一些更复杂的菜单或复选框行为,它还可以为您提供更大的灵活性。

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

    演示

    这里是游乐场 sn-p:

    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 功能,例如 object destructuringarray destructuringrestdefault values

    创建状态

    所以。由于菜单项的 ID 是由点分隔的数字,因此我们可以在构造状态时利用这一点。状态本质上是一个树状结构,每个子菜单都是其父级的子菜单,叶节点(“最后一个菜单”或“最深的菜单”)如果展开,则具有 {} 的值,或者undefined 如果没有。以下是菜单初始状态的构造方式:

    <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)
      };
    };
    

    让我们一点一点地分解。

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

    这些只是我们定义的便利函数,因此我们可以轻松更改用于表示展开和未展开节点的值。与仅在代码中使用文字 {}undefined 相比,它还使代码更具可读性。展开和未展开的值也可以是truefalse,重要的是展开的节点是truthy,而未展开的节点是假的。稍后再详细介绍。

    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() 的函数。

    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('.') 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,如果你熟悉这个命令的话。

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

    渲染树

    &lt;Menu&gt; 组件将状态向下传递给&lt;MenuItemContainer&gt;,它会呈现&lt;MenuItem&gt;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>;
    }
    

    &lt;MenuItemContainer&gt; 组件与原始组件没有太大区别。不过,&lt;MenuItem&gt; 组件看起来确实有点不同。

    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 &amp;&amp; openMenus[id]}。我们只传递包含当前项的子项的状态树,而不是传递整个菜单状态。这允许组件非常容易地检查它是否应该打开或折叠 - 只需检查是否从对象中找到了它自己的 ID (openMenus ? !!openMenus[id] : false)!

    如果它是菜单中最深的项目,我还更改了切换按钮以切换复选框而不是菜单状态 - 如果这不是您要查找的内容,则可以很快改回来。

    我还在这里使用!!{}undefined 从菜单状态强制转换为truefalse。这就是为什么我说只有它们是真的还是假的才重要。 reactstrap 组件似乎需要明确的 truefalse 而不是真/假,这就是它存在的原因。

    最后:

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

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

    改进空间

    目前的解决方案一直向下传递大量状态,例如onMenuToggleonChecked 回调,以及非递归的checkedMenus 状态。这些可以利用 React 的 Context

    【讨论】:

    • 感谢您花费大量时间并提供了非常详细的解决方案..
    【解决方案2】:

    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.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;
        let {isOpen} = this.state;
        if(openMenuId.startsWith(this.props.id)){isOpen = true;}
        return (
          <Fragment>
            <Button onClick={this.toggle}>{this.props.name}</Button>
            <Fragment>
              {isLastChild ? <Input type="checkbox" checked={openMenuId === this.props.id} value={this.props.id} /> : ''}
            </Fragment>
            <Collapse isOpen={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>

    【讨论】:

    • 代码转储不是有用的答案。说出你做了什么,以及如何工作。
    • @TJCrowder,我也无法关闭这个打开的菜单项。而且就像你说的那样,这个答案没有太多信息。如前所述,你能帮我实现请问结果??
    • 这个解决方案并没有完全解决我的问题,因为它打开了菜单三中的最后一级,但我无法让它再次折叠回来,也无法让任何其他复选框来检查它。 . 但是感谢您的帮助,但不幸的是,它并没有像预期的那样完全成熟..
    【解决方案3】:

    假设您只需要在开始时打开某个菜单,您可以将 MenuItem 组件设置为期望布尔属性 defaultOpen 并使用它来设置初始 isOpen

    那么我们需要做的就是在加载时在menuItems 中设置这个属性。

    import React from 'react'
    import { Button, Collapse, Input } from 'reactstrap';
    import 'bootstrap/dist/css/bootstrap.min.css';
    
    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;
    
    function setDefaultOpen(menuItems, openMenuId) {
      if(!menuItems) return
      const openMenuItem = menuItems.find(item => openMenuId.startsWith(item.id))
      if(!openMenuItem) return
      openMenuItem.defaultOpen = true
      setDefaultOpen(openMenuItem.children, openMenuId)
    }
    
    export default class Menu extends Component {
      constructor(props) {
        super(props);
        this.state = {menuItems: []};
      }
    
      render() {
        return <MenuItemContainer menuItems={this.state.menuItems} />;
      }
    
      componentDidMount() {
        loadMenu().then(menuItems => {
          setDefaultOpen(menuItems, openMenuId)
          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: props.defaultOpen};
        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}));
      }
    }
    

    如果您需要在初始渲染后打开菜单项,则需要将MenuItem 设为受控组件。

    即将isOpen 状态拉到父级并将其作为道具传递给MenuItem 组件以及回调函数,它将在单击时调用,并将其id 作为参数传递。 parent 中的回调函数将在其状态中切换具有给定 id 的项目的 isOpen 属性。

    【讨论】:

      【解决方案4】:

      只需添加一个类.actve 或您想要的任何其他类并根据您的要求对其进行样式设置,然后添加script,如果您使用的是普通js,则添加document.querySelector("youElementClassOrId").classList.toggle("idOrClassYouWantToToggle")。 我希望这会奏效

      【讨论】:

      • 不,这不是我需要的东西..此外,切换和工作正常..我需要与值进行比较,需要选中复选框并从选中的菜单打开到父级别的菜单..
      猜你喜欢
      • 1970-01-01
      • 2014-12-19
      • 1970-01-01
      • 1970-01-01
      • 2014-10-05
      • 2017-05-24
      • 2021-04-10
      • 2015-04-18
      • 1970-01-01
      相关资源
      最近更新 更多