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

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

问题描述

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

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

const loadMenu = () =>Promise.resolve([{id:"1",name:"One",children:[{id:"1.1",name:"One - one",children:[{id:"1.1.1",name:"一-一-一"},{id:"1.1.2",name:"一-一-二"},{id:"1.1.3",name:"一-一-三"}]}]},{id:"2",name:"Two",children:[{id:"2.1",name:"Two - one"}]},{id:"3",name:"Three",children:[{id:"3.1",name:"三一",children:[{id:"3.1.1",name:"三一一",children:[{id:"3.1.1.1",name:"三一一一一",children:[{id:"3.1.1.1.1",name:"三一一一一一一"}]}]}]}]},{id:"4",name:"Four"},{id:"5",name:"Five",children:[{id:"5.1",name:"Five - one"},{id:"5.2",name:"五-二"},{id:"5.3",name:"五-三"},{id:"5.4",name:"五-四"}]},{id:"6",name:"6"}]);const openMenuId = "3.1.1.1";const {组件,片段} = 反应;const {按钮,折叠,输入} = Reactstrap;类菜单扩展组件{构造函数(道具){超级(道具);this.state = {menuItems: []};}使成为() {return <MenuItemContainer menuItems={this.state.menuItems}/>;}componentDidMount() {loadMenu().then(menuItems => this.setState({menuItems}));}}功能菜单项容器(道具){if (!props.menuItems.length) 返回空值;const renderMenuItem = menuItem =><li key={menuItem.id}><MenuItem {...menuItem}/></li>;return 
    {props.menuItems.map(renderMenuItem)}
;}MenuItemContainer.defaultProps = {menuItems: []};类 MenuItem 扩展组件 {构造函数(道具){超级(道具);this.state = {isOpen: false};this.toggle = this.toggle.bind(this);}使成为() {让 isLastChild = this.props.children ?假:真;返回 (<片段><Button onClick={this.toggle}>{this.props.name}</Button><片段>{是最后一个孩子?<Input type="checkbox" value={this.props.id}/>:''}</片段><折叠 isOpen={this.state.isOpen}><MenuItemContainer menuItems={this.props.children}/></收起></片段>);}切换(){this.setState(({isOpen}) => ({isOpen: !isOpen}));}}ReactDOM.render(
, 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和checkbox的值在这里匹配..然后需要将各自的菜单和子菜单扩展到最后一层.

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

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

挣扎了很长时间,所以请帮助我.. 提前致谢..

解决方案

多好的问题!我真的很喜欢为这个问题想出一个解决方案.

因为你想给菜单状态和复选框状态一个初始状态,我认为在 <Menu> 级别(甚至更高!)控制两者的状态是一个好主意.这不仅使从父级定义初始状态变得容易,而且如果您将来需要更复杂的菜单或复选框行为,它还为您提供了更大的灵活性.

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

演示

这是操场片段:

const loadMenu = () =>Promise.resolve([{编号:1",name: "一个",孩子们: [{编号:1.1",name: "一-一",孩子们: [{ id: "1.1.1", name: "一一一" },{ id: "1.1.2", name: "一-一-二" },{ id: "1.1.3", name: "一-一-三" }]}]},{ id: "2", name: "Two", children: [{ id: "2.1", name: "Two - one" }] },{编号:3",name: "三",孩子们: [{编号:3.1",name: "三一",孩子们: [{编号:3.1.1",name: "三一一",孩子们: [{编号:3.1.1.1",名称:三一一一",孩子们: [{ id: "3.1.1.1.1", name: "三一一一一一" }]}]}]}]},{ id: "4", name: "4" },{编号:5",name: "五",孩子们: [{ id: "5.1", name: "五-一" },{ id: "5.2", name: "五-二" },{ id: "5.3", name: "五-三" },{ id: "5.4", name: "五 - 四" }]},{ id: "6", name: "6" }]);const { 组件,片段} = 反应;const { 按钮,折叠,输入 } = Reactstrap;const replaceNode = (replacer, node, idPath, i) =>{if (i <= idPath.length && !node) {//还没有到达目标节点,在中间创建节点节点 = {};}如果(我> idPath.length){//到达目标节点返回替换器(节点);}//构造与此深度匹配的ID - 深度含义//ID之间的点数const id = idPath.slice(0, i).join(".");返回 {...节点,//递归[id]:replaceNode(replacer, node[id], idPath, i + 1)};};const replaceNodeById = (node, id,visitor) =>{//传递 id 部分的数组而不是处理字符串//直接 - 处理多编号 ID 部分的简单方法,例如3.1.15.32return replaceNode(visitor, node, id.split("."), 1);};const expandNode = () =>({});const unexpandedNode = () =>不明确的;const toggleNodeById = (node, id) =>replaceNodeById(node, id, oldNode =>旧节点?unexpandedNode() : expandNode());const expandNodeById = (node, id) =>replaceNodeById(node, id, oldNode => expandNode());类菜单扩展组件{构造函数(道具){超级(道具);this.state = {菜单项:[],打开菜单:{},选中的菜单:{}};this.handleMenuToggle = this.handleMenuToggle.bind(this);this.handleChecked = this.handleChecked.bind(this);}使成为() {const { menuItems, openMenus, checkedMenus } = this.state;返回 (

<div style={{ padding: "10px", marginLeft: "auto" }}><p>菜单状态</p><pre>{JSON.stringify(openMenus, null, 2)}</pre>

<div style={{ padding: "10px", width: "177px" }}><p>复选框状态</p><pre>{JSON.stringify(checkedMenus, null, 2)}</pre>

);}componentDidMount() {const { initialOpenMenuId, initialCheckedMenuIds } = this.props;loadMenu().then(menuItems => {const initialMenuState = {};this.setState({菜单项,openMenus: expandNodeById(initialMenuState, initialOpenMenuId),已检查菜单:initialCheckedMenuIds.reduce((acc, val) =>({ ...acc, [val]: true }),{})});});}handleMenuToggle(toggledId){this.setState(({ openMenus }) => ({openMenus: toggleNodeById(openMenus, toggledId)}));}处理检查(toggledId){this.setState(({checkedMenus}) =>({检查菜单:{...检查菜单,[toggledId]:checkedMenus[toggledId] ?unexpandedNode() : expandNode()}}));}}功能菜单项容器({打开菜单,onMenuToggle,检查菜单,已检查,菜单项 = []}) {if (!menuItems.length) 返回空值;const renderMenuItem = menuItem =>(<li key={menuItem.id}><菜单项openMenus={openMenus}onMenuToggle={onMenuToggle}选中的菜单={选中的菜单}onChecked={onChecked}{...菜单项}/>);return

    {menuItems.map(renderMenuItem)}
;}类 MenuItem 扩展组件 {构造函数(道具){超级(道具);this.handleToggle = this.handleToggle.bind(this);this.handleChecked = this.handleChecked.bind(this);}使成为() {常量{孩子们,姓名,ID,打开菜单,onMenuToggle,检查菜单,已检查} = this.props;const isLastChild = !children;返回 (<片段><按钮 onClick={isLastChild ?this.handleChecked : this.handleToggle}>{姓名}</按钮>{isLastChild &&(<输入添加在类型=复选框"onChange={this.handleChecked}选中={!!checkedMenus[id]}值={id}/>)}<收起 isOpen={openMenus ?!!openMenus[id] : false}><菜单项容器菜单项={子项}//传递子菜单的状态openMenus={openMenus &&openMenus[id]}onMenuToggle={onMenuToggle}选中的菜单={选中的菜单}onChecked={onChecked}/></收起></片段>);}手柄切换(){this.props.onMenuToggle(this.props.id);}处理检查(){this.props.onChecked(this.props.id);}}ReactDOM.render(<Menu initialOpenMenuId="3.1.1.1" initialCheckedMenuIds={["3.1.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([{编号:1",name: "一个",孩子们: [{编号:1.1",name: "一-一",孩子们: [{ id: "1.1.1", name: "一一一" },{ id: "1.1.2", name: "一-一-二" },{ id: "1.1.3", name: "一-一-三" }]}]},{ id: "2", name: "Two", children: [{ id: "2.1", name: "Two - one" }] },{编号:3",name: "三",孩子们: [{编号:3.1",name: "三一",孩子们: [{编号:3.1.1",name: "三一一",孩子们: [{编号:3.1.1.1",名称:三一一一",孩子们: [{ id: "3.1.1.1.1", name: "三一一一一一" }]}]}]}]},{ id: "4", name: "4" },{编号:5",name: "五",孩子们: [{ id: "5.1", name: "五-一" },{ id: "5.2", name: "五-二" },{ id: "5.3", name: "五-三" },{ id: "5.4", name: "五 - 四" }]},{ id: "6", name: "6" }]);const { 组件,片段} = 反应;const { 按钮,折叠,输入 } = Reactstrap;const replaceNode = (replacer, node, idPath, i) =>{if (i <= idPath.length && !node) {//还没有到达目标节点,在中间创建节点节点 = {};}如果(我> idPath.length){//到达目标节点返回替换器(节点);}//构造与此深度匹配的ID - 深度含义//ID之间的点数const id = idPath.slice(0, i).join(".");返回 {...节点,//递归[id]:replaceNode(replacer, node[id], idPath, i + 1)};};const replaceNodeById = (node, id,visitor) =>{//传递 id 部分的数组而不是处理字符串//直接 - 处理多编号 ID 部分的简单方法,例如3.1.15.32return replaceNode(visitor, node, id.split("."), 1);};const expandNode = () =>({});const unexpandedNode = () =>不明确的;const toggleNodeById = (node, id) =>replaceNodeById(node, id, oldNode =>旧节点?unexpandedNode() : expandNode());const expandNodeById = (node, id) =>replaceNodeById(node, id, oldNode => expandNode());类菜单扩展组件{构造函数(道具){超级(道具);this.state = {菜单项:[],打开菜单:{},选中的菜单:{}};this.handleMenuToggle = this.handleMenuToggle.bind(this);this.handleChecked = this.handleChecked.bind(this);}使成为() {const { menuItems, openMenus, checkedMenus } = this.state;返回 (<菜单项容器openMenus={openMenus}菜单项={菜单项}onMenuToggle={this.handleMenuToggle}选中的菜单={选中的菜单}onChecked={this.handleChecked}/>);}componentDidMount() {const { initialOpenMenuId, initialCheckedMenuIds } = this.props;loadMenu().then(menuItems => {const initialMenuState = {};this.setState({菜单项,openMenus: expandNodeById(initialMenuState, initialOpenMenuId),已检查菜单:initialCheckedMenuIds.reduce((acc, val) =>({ ...acc, [val]: true }),{})});});}handleMenuToggle(toggledId){this.setState(({ openMenus }) => ({openMenus: toggleNodeById(openMenus, toggledId)}));}处理检查(toggledId){this.setState(({checkedMenus}) =>({检查菜单:{...检查菜单,[toggledId]:checkedMenus[toggledId] ?unexpandedNode() : expandNode()}}));}}功能菜单项容器({打开菜单,onMenuToggle,检查菜单,已检查,菜单项 = []}) {if (!menuItems.length) 返回空值;const renderMenuItem = menuItem =>(<li key={menuItem.id}><菜单项openMenus={openMenus}onMenuToggle={onMenuToggle}选中的菜单={选中的菜单}onChecked={onChecked}{...菜单项}/>);return 
    {menuItems.map(renderMenuItem)}
;}类 MenuItem 扩展组件 {构造函数(道具){超级(道具);this.handleToggle = this.handleToggle.bind(this);this.handleChecked = this.handleChecked.bind(this);}使成为() {常量{孩子们,姓名,ID,打开菜单,onMenuToggle,检查菜单,已检查} = this.props;const isLastChild = !children;返回 (<片段><按钮 onClick={isLastChild ?this.handleChecked : this.handleToggle}>{姓名}</按钮>{isLastChild &&(<输入添加在类型=复选框"onChange={this.handleChecked}选中={!!checkedMenus[id]}值={id}/>)}<收起 isOpen={openMenus ?!!openMenus[id] : false}><菜单项容器菜单项={子项}//传递子菜单的状态openMenus={openMenus &&openMenus[id]}onMenuToggle={onMenuToggle}选中的菜单={选中的菜单}onChecked={onChecked}/></收起></片段>);}手柄切换(){this.props.onMenuToggle(this.props.id);}处理检查(){this.props.onChecked(this.props.id);}}ReactDOM.render(<Menu initialOpenMenuId="3.1.1.1" initialCheckedMenuIds={["3.1.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 功能,例如 对象解构,数组解构rest默认值.

创建状态

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

 {const initialMenuState = {};this.setState({菜单项,openMenus: expandNodeById(initialMenuState, initialOpenMenuId),已检查菜单:initialCheckedMenuIds.reduce((acc, val) =>({ ...acc, [val]: true }),{})});});//...const expandNode = () =>({});const unexpandedNode = () =>不明确的;const toggleNodeById = (node, id) =>replaceNodeById(node, id, oldNode =>旧节点?unexpandedNode() : expandNode());const expandNodeById = (node, id) =>replaceNodeById(node, id, oldNode => expandNode());const replaceNodeById = (node, id,visitor) =>{//传递 id 部分的数组而不是处理字符串//直接 - 处理多编号 ID 部分的简单方法,例如3.1.15.32return replaceNode(visitor, node, id.split("."), 1);};const replaceNode = (replacer, node, idPath, i) =>{if (i <= idPath.length && !node) {//还没有到达目标节点,在中间创建节点节点 = {};}如果(我> idPath.length){//到达目标节点返回替换器(节点);}//构造与此深度匹配的ID - 深度含义//ID之间的点数const id = idPath.slice(0, i).join(".");返回 {...节点,//递归[id]:replaceNode(replacer, node[id], idPath, i + 1)};};

让我们一点一点地把它拆开.

const expandNode = () =>({});const unexpandedNode = () =>不明确的;

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

const toggleNodeById = (node, id) =>replaceNodeById(node, id, oldNode =>旧节点?unexpandedNode() : expandNode());const expandNodeById = (node, id) =>replaceNodeById(node, id, oldNode => expandNode());

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

const replaceNodeById = (node, id,visitor) =>{//传递 id 部分的数组而不是处理字符串//直接 - 处理多编号 ID 部分的简单方法,例如3.1.15.32return replaceNode(visitor, node, id.split("."), 1);};

前两个函数使用这个函数来提供一个更简洁的界面.ID 在这里由点 (.) 分割,这为我们提供了 ID 部分的数组.next 函数直接操作这个数组而不是 ID 字符串,因为这样我们就不需要做 .indexOf('.') 恶作剧.

const replaceNode = (replacer, node, idPath, i) =>{if (i <= idPath.length && !node) {//还没有到达目标节点,在中间创建节点节点 = {};}如果(我> idPath.length){//到达目标节点返回替换器(节点);}//构造与此深度匹配的ID - 深度含义//ID之间的点数const id = idPath.slice(0, i).join(".");返回 {...节点,//递归[id]:replaceNode(replacer, node[id], idPath, i + 1)};};

replaceNode 函数是问题的关键.它是一个递归函数,它从旧菜单树中生成一棵新树,用提供的替换器函数替换旧的目标节点.如果树缺少中间的部分,例如当树是 {} 但我们想要替换节点 3.1.1.1 时,它会在它们之间创建父节点.如果您熟悉该命令,则类似于 mkdir -p.

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

渲染树

组件将状态传递给 ,后者呈现 .

function MenuItemContainer({打开菜单,onMenuToggle,检查菜单,已检查,菜单项 = []}) {if (!menuItems.length) 返回空值;const renderMenuItem = menuItem =>(<li key={menuItem.id}><菜单项openMenus={openMenus}onMenuToggle={onMenuToggle}选中的菜单={选中的菜单}onChecked={onChecked}{...菜单项}/>);return 
    {menuItems.map(renderMenuItem)}
;}

组件与原始组件没有太大区别.不过, 组件看起来确实有些不同.

class MenuItem 扩展组件 {构造函数(道具){超级(道具);this.handleToggle = this.handleToggle.bind(this);this.handleChecked = this.handleChecked.bind(this);}使成为() {常量{孩子们,姓名,ID,打开菜单,onMenuToggle,检查菜单,已检查} = this.props;const isLastChild = !children;返回 (<片段><按钮 onClick={isLastChild ?this.handleChecked : this.handleToggle}>{姓名}</按钮>{isLastChild &&(<输入添加在类型=复选框"onChange={this.handleChecked}选中={!!checkedMenus[id]}值={id}/>)}<折叠 isOpen={openMenus ?!!openMenus[id] : false}><菜单项容器菜单项={子项}//传递子菜单的状态openMenus={openMenus &&openMenus[id]}onMenuToggle={onMenuToggle}选中的菜单={选中的菜单}onChecked={onChecked}/></收起></片段>);}手柄切换(){this.props.onMenuToggle(this.props.id);}处理检查(){this.props.onChecked(this.props.id);}}

这里的关键部分是:openMenus={openMenus &&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.1"]}/>,document.getElementById("root"));

这里我们将初始状态传递给

.initialOpenMenuId 也可以是一个数组(或者 initialCheckedMenuIds 可以是单个字符串),但这符合问题的规范.

改进空间

现在的解决方案一直向下传递许多状态,例如 onMenuToggleonChecked 回调,以及 checkedMenus 状态这不是递归的.这些可以利用 React 的 Context.

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>

Requirement:

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)..

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.

Eg..,

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.

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..

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.

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.

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:

Demo

Here's the playground snippet:

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>

Answer

Code walkthrough 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 { 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>

Walkthrough

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.

Creating the state

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;

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

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

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

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.

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.

Rendering the tree

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

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

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.

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.

And finally:

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

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.

Room for improvement

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天全站免登陆