我正在制作一个嵌套菜单和子菜单,到目前为止一切都已经完成了..我现在需要根据给定的 id 使这个可折叠菜单默认打开..
您还可以查看下面完整的工作代码片段,
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 也只属于最后一级的 child id,因此肯定会有一个复选框,以便需要选中复选框,并且需要打开父级的菜单。
例如..,
由于 openMenuId 是
"3.1.1.1.1"
因此很明显菜单的最后一个子级three
这是 Three - one - one - one - one
需要检查为 openMenuId
和复选框值在这里匹配..然后需要将相应的菜单和子菜单扩展到最后一级。这仅适用于访问的页面上的默认行为,因此在该用户可以折叠回来并能够检查任何其他菜单中的任何其他复选框之后。需要在复选框中选中..
请帮助我通过比较作为 Prop 传递的 id 并检查相应的菜单来实现打开相应菜单的结果..
挣扎了很长时间,所以请帮助我.. 非常感谢..
最佳答案
多么好的问题啊!我真的很喜欢为这个问题想出一个解决方案。
由于您想为菜单状态和复选框状态提供初始状态,我认为在 <Menu>
上控制两者的状态级别(甚至更高!)是个好主意。这不仅使从父级定义初始状态变得容易,而且如果您将来需要更复杂的菜单或复选框行为,它还为您提供了更大的灵活性。
由于菜单的结构是递归的,我认为菜单状态的递归结构非常有效。在我进入代码之前,这里有一个简短的 GIF,我希望它有助于解释状态的样子:
演示
这是操场片段:
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 destructuring ,
array destructuring , rest和 default 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
在代码中。扩展和未扩展的值也可以是 true
和 false
,重要的是展开的节点是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 部分的数组。 next 函数直接操作这个数组而不是 ID 字符串,因为这样我们就不需要做 .indexOf('.')
恶作剧。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
如果一个项目被选中。此状态不是递归的,因为它们不需要取消选中或递归检查。如果您决定要显示一个指示器,表明此菜单项下的某些内容已被选中,一个简单的解决方案是将复选框状态更改为递归,就像菜单状态一样。渲染树
<Menu>
组件将状态传递给 <MenuItemContainer>
,呈现 <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>
不过,组件看起来确实有点不同。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
)!如果它是菜单中最深的项目,我还更改了切换按钮以切换复选框而不是菜单状态 - 如果这不是您要查找的内容,则可以很快改回来。
我也使用
!!
这里要胁{}
和 undefined
从菜单状态进入true
或 false
.这就是为什么我说只有它们是真还是假才重要。 reactstrap
组件似乎想要显式 true
或 false
而不是真/假,所以这就是它在那里的原因。最后:
ReactDOM.render(
<Menu initialOpenMenuId="3.1.1.1" initialCheckedMenuIds={["3.1.1.1.1"]} />,
document.getElementById("root")
);
这里我们将初始状态传递给
<Menu>
. initialOpenMenuId
也可以是数组(或 initialCheckedMenuIds
可以是单个字符串),但这符合问题的规范。改进空间
现在的解决方案一直向下传递许多状态,例如
onMenuToggle
和 onChecked
回调,以及 checkedMenus
不是递归的状态。这些可以使用 React 的 Context .
关于javascript - 默认根据id打开可折叠菜单,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60616336/