前言
今天来实现一个 React
的递归组件。具体的效果图如下:
假设后端返回的数据如下:
[{ id: 1, parent_id: 0, name: '广东省', children: [{ id: 2, parent_id: 1, name: '广州市', children: [{ id: 3, parent_id: 2, name: '番禺区' }, { id: 4, parent_id: 2, name: '天河区' }], }, { id: 7, parent_id: 1, name: '深圳市', children: [{ id: 8, parent_id: 7, name: '南山区' }, { id: 9, parent_id: 7, name: '宝安区' }, { id: 10, parent_id: 7, name: '龙岗区' }, { id: 11, parent_id: 7, name: '龙华区' }, ], }, ], }, { id: 56, parent_id: 0, name: '广西省', children: [{ id: 57, parent_id: 56, name: '南宁市' }, ], }, ]
分析
需求
- 每层元素都有一个
children
属性,选中这项后展示这一项所有的子菜单,首次展示或者切换时默认展示第一项子菜单 - 选中时高亮样式,默认为第一项高亮
实现思路
- 每一层的显示逻辑应该是一样的,都是渲染这一层的列表即可
- 对于子菜单的渲染逻辑也类似,如果当前项有
children
属性,递归调用即可 - 高亮样式的逻辑:
- 用一个路径数组记录好当前渲染的路径,如果某一项的
id
在数组里,则高亮 - 切换父项时,重置数组中子项的
id
,这里可以用一个深度变量dep
来控制
实现
渲染
- 使用假数据来模拟,格式如上述
App.js
中传入的dep
是0,后面每渲染一层依次加1即可- 每一层初始渲染时默认将第一项作为高亮节点填入路径数组中,路径数组具体如何操作在下面
//App.js import menuList from './mock/menu' //...... this.state = { activePath: [] } //...... componentDidMount() { let { id } = this.props.list[0] let { dep, activePath } = this.props if (!activePath[dep]) { this.props.changeActivePath(dep, id, this.props.list) } } render() { return ( <Menu list={menuList} dep={0} //以下两个prop来控制高亮逻辑 activePath={this.state.activePath} changeActivePath={this.changeActivePath.bind(this)} /> ) }
- 此处为渲染一层的逻辑,拿到这一层的数据后循环渲染一下即可
- 标红的样式控制用上面说的路径数组来控制
//Menu.js render() { let { list, dep } = this.props, renderList = list return ( <div> <ul className="list"> {renderList.map(item => { return <li id={item.id} onClick={this.clickItem.bind(this, item.id)} className={this.props.activePath.includes(item.id) ? 'active' : ''} key={item.id}>{item.name} </li> })} </ul> {this.renderMore(list, dep)} </div> ) }
下面是渲染子节点的逻辑:
- 先找出当前高亮的节点,要渲染它的子节点
- 递归调用我们的
Menu
组件即可,注意层级加1
renderMore(list, dep) { let moreList = [], id for (let i = 0; i < list.length; i++) { //找出当前高亮的节点 if (list[i].id == this.props.activePath[this.props.dep]) { moreList = list[i].children id = list[i].id break; } } if (Array.isArray(moreList) && moreList.length > 0) { return ( <Menu list={moreList} dep={dep + 1} activePath={this.props.activePath} changeActivePath={this.props.changeActivePath.bind(this)} /> ) } }
切换
- 切换的逻辑十分简单,将点击的id传入即可
- 下面具体来看路径数组的处理
clickItem(id) { this.props.changeActivePath(this.props.dep, id, this.props.list) }
处理高亮逻辑
- 如果是最后一层,则直接加入即可
- 如果不是,则将当前层点击的节点填入数组,重新构建下面的子节点
- 递归处理下子节点,默认是采用第一项作为高亮的节点
//App.js changeActivePath(dep, id, list) { let activePath = this.state.activePath if (!activePath[dep] || dep == activePath.length - 1) { //最后一个 添进去即可 activePath[dep] = id } else { //重新构建整个activePath数组 activePath[dep] = id let cur = [] for (let i = 0; i < list.length; i++) { let itemId = list[i].id if (itemId == id) { cur = list[i] break } } setPath(dep + 1, cur) } function setPath(dep, cur) { if (cur.children) { activePath[dep] = cur.children[0].id setPath(dep + 1, cur.children[0]) } } this.setState({ activePath }) }
完整代码
App.js
import React from 'react' import Menu from './components/Menu' import menuList from './mock/menu' class App extends React.Component { constructor(props) { super(props) this.state = { activePath: [] } } render() { return ( <Menu list={menuList} dep={0} activePath={this.state.activePath} changeActivePath={this.changeActivePath.bind(this)} /> ) } changeActivePath(dep, id, list) { let activePath = this.state.activePath if (!activePath[dep] || dep == activePath.length - 1) { //最后一个 添进去即可 activePath[dep] = id } else { //重新构建整个activePath数组 activePath[dep] = id let cur = [] for (let i = 0; i < list.length; i++) { let itemId = list[i].id if (itemId == id) { cur = list[i] break } } setPath(dep + 1, cur) } function setPath(dep, cur) { if (cur.children) { activePath[dep] = cur.children[0].id setPath(dep + 1, cur.children[0]) } } this.setState({ activePath }) } } export default App
Menu.js
import React, { Component } from 'react' import '../style/Menu.less' class Menu extends Component { constructor(props) { super(props) } componentDidMount() { let { id } = this.props.list[0] let { dep, activePath } = this.props if (!activePath[dep]) { this.props.changeActivePath(dep, id, this.props.list) } } renderMore(list, dep) { let moreList = [], id for (let i = 0; i < list.length; i++) { if (list[i].id == this.props.activePath[this.props.dep]) { moreList = list[i].children id = list[i].id break; } } if (Array.isArray(moreList) && moreList.length > 0) { return ( <Menu list={moreList} dep={dep + 1} activePath={this.props.activePath} changeActivePath={this.props.changeActivePath.bind(this)} /> ) } } clickItem(id) { this.props.changeActivePath(this.props.dep, id, this.props.list) } render() { let { list, dep } = this.props, renderList = list return ( <div> <ul className="list"> {renderList.map(item => { return <li id={item.id} onClick={this.clickItem.bind(this, item.id)} className={this.props.activePath.includes(item.id) ? 'active' : ''} key={item.id}>{item.name} </li> })} </ul> {this.renderMore(list, dep)} </div> ) } } export default Menu
Menu.less
ul, li { list-style: none; margin: 0; padding: 0; } .list { display: flex; li { margin: 10px; cursor: pointer; } } .active { color: red; }