>> 博客原文连接css
Antd是基于Ant Design设计体系的React UI组件库,主要用于研发企业级中后台产品,在前端不少项目中都有使用。除了提供一些比较基础的例如Button
、Form
、Input
、Modal
、List
...组件,还有Tree
、Upload
、Table
这几个功能集成度比较高的复杂组件,其中Tree
组件的应用场景挺多的,在一些涉及显示树形结构数据的功能中能够体现:目录结构展现、族谱关系图...,总之在须要呈现多个父子层级之间结构关系的场景中就可能用到这种Tree组件,Antd虽然官方提供了Tree组件可是它的功能比较有限,定位是主要负责对数据的展现工做,树数据的增删查改这些功能基本没有支持,可是Antd Tree的属性支持比较完善,咱们能够基于Antd树来实现支持编辑功能的EditableTree
组件。前端
源码:nojsja/EditableTreenode
已经发布为npm组件,能够直接安装:react
$: npm install editable-tree-antd # or $: yarn add editable-tree-antd
基于React / Antd / Mobxgit
Antd Tree文档github
--- index.js -- 入口文件,数据初始化、组件生命周期控制、递归调用TreeNode
进行数据渲染
--- Tree.js -- Tree类用于抽象化树形数据的增删查改操做,至关于Model
层
--- lang.js -- 多语言文件
--- TreeNode.jsx -- 单层树节点组件,用于隔离每层节点状态显示和操做
------- TreeNodeDisplay.jsx -- 非编辑状态下树数据的展现
------- TreeNodeNormalEditing.jsx -- 普通节点处于编辑状态下时
------- TreeNodeYamlEditing.jsx -- yaml节点处于编辑状态下时
------- TreeNodeActions.jsx -- 该层级树节点的全部功能按钮组
--- styles / editable-tree.css -- 树样式
--- styles / icon-font / * -- 图标依赖的iconfont文件npm
Tree
数据格式:[ { title: 'parent 1', key: '0-0', children: [ { title: 'parent 1-0', key: '0-0-0', disabled: true, children: [ { title: 'leaf', key: '0-0-0-0', disableCheckbox: true, }, { title: 'leaf', key: '0-0-0-1', } ] }, { title: 'parent 1-1', key: '0-0-1', children: [{ title: <span style={{ color: '#1890ff' }}>sss</span>, key: '0-0-1-0' }] } ] } ]
title
(文字label)、key
(节点惟一标识)、children
(子结点列表)属性外,还有其它不少自定义参数好比配置节点是否选中等等,这里就不对其它功能配置项作细研究了,感兴趣能够查看官方文档。title
值其实不仅是一个字符串,还能够是一个ReactNode,也就是说Antd官方为咱们提供了一个树改造的后门,咱们能够用本身的渲染逻辑来替换官方的title
渲染逻辑,因此关键点就是分离这个title
渲染为一个独立的React组件,在这个组件里咱们独立管理每一层级的树节点数据展现,同时又向这个组件暴露操做整个树形数据的方法。另外一方面Tree型数据通常都须要使用递归逻辑来进行节点渲染和数据增删查改,这里TreeNode.js
就是递归渲染的Component对象,而增删查改逻辑咱们把它分离到Tree.js
Model里面进行管理,这样子思路就比较清晰了。
入口文件,用于:数据初始化、组件生命周期控制、递归调用
TreeNode
进行数据渲染、加载lang文件等等
componentDidMount
中咱们初始化一个Tree Model,并设置初始化state数据。componentWillReceiveProps
中咱们更新这个Model和state以控制界面状态更新,注意使用的Js数据深比较函数deepComparison
用来避免没必要要的数据渲染,数据深比较时要使用与树显示相关的节点属性裸数据
(见方法getNudeTreeData
),好比nodeName
,nodeValue
等属性,其它的无关属性好比id
和depth
须要忽略。formatNodeData
主要功能是将咱们传入的自定义树数据递归 “翻译” 成Antd Tree渲染须要的原生树数据。[ { nodeName: '出版者', id: '出版者', // unique id, required nameEditable: true, // is level editable (name), default true valueEditable: true, // is level editable (value), default true nodeDeletable: false, // is level deletable, default true nodeValue: [ { nodeName: '出版者描述', isInEdit: true, // is level in edit status id: '出版者描述', nodeValue: [ { nodeName: '出版者名称', id: '出版者名称', nodeValue: '出版者A', }, { nodeName: '出版者地', id: '出版者地', valueEditable: false, nodeValue: '出版地B1', }, ], } ], }, ... ];
... class EditableTree extends Component { state = { treeData: [], // Antd Tree 须要的结构化数据 expandedKeys: [], // 将树的节点展开/折叠状态归入控制 maxLevel: 50, ;// 默认最大树深度 enableYaml: false, lang: 'zh_CN' }; dataOrigin = [] treeModel = null key=getRandomString() /* 组件挂载后初始化树数据,生成treeModel,更新state */ componentDidMount() { const { data, maxLevel = 50, enableYaml, lang="zh_CN" } = this.props; if (data) { this.dataOrigin = data; TreeClass.defaultTreeValueWrapper(this.dataOrigin); // 树节点添加默认值 TreeClass.levelDepthWrapper(this.dataOrigin); // 添加层级深度属性 const formattedData = this.formatTreeData(this.dataOrigin); // 生成格式化后的Antd Tree数据 this.updateTreeModel({ data: this.dataOrigin, key: this.key }); // 更新model const keys = TreeClass.getTreeKeys(this.dataOrigin); // 获取各个层级的key,默认展开全部层级 this.setState({ treeData: formattedData, expandedKeys: keys, enableYaml: !!enableYaml, maxLevel, lang, }); } } /* 组件props数据更新后更新treeModel和state */ componentWillReceiveProps(nextProps) { const { data, maxLevel = 50, enableYaml, lang="zh_CN" } = nextProps; this.setState({ enableYaml: !!enableYaml, lang, maxLevel }); // 深比较函数避免没必要要的树更新 if ( !deepComparison( TreeClass.getNudeTreeData(deepClone(this.dataOrigin)), TreeClass.getNudeTreeData(deepClone(data)) ) ) { this.dataOrigin = data; TreeClass.defaultTreeValueWrapper(this.dataOrigin); TreeClass.levelDepthWrapper(this.dataOrigin); const formattedData = this.formatTreeData(this.dataOrigin); this.updateTreeModel({ data: this.dataOrigin, key: this.key }); const keys = TreeClass.getTreeKeys(this.dataOrigin); this.onDataChange(this.dataOrigin); // 触发onChange回调钩子 this.setState({ treeData: formattedData, expandedKeys: keys }); } } /* 修改节点 */ modifyNode = (key, treeNode) => { const modifiedData = this.treeModel.modifyNode(key, treeNode); // 更新model this.setState({ treeData: this.formatTreeData(modifiedData), // 更新state,触发数据回调钩子 }, () => this.onDataChange(this.dataOrigin)); } /** * 如下省略的方法具备跟modifyNode类似的逻辑 * 调用treeModel修改数据而后更新state **/ /* 进入编辑模式 */ getInToEditable = (key, treeNode) => { ... } /* 添加一个兄弟节点 */ addSisterNode = (key) => { ... } /* 添加一个子结点 */ addSubNode = (key) => { ... } /* 移除一个节点 */ removeNode = (key) => { ... } /* 递归生成树节点数据 */ formatNodeData = (treeData) => { let tree = {}; const key = `${this.key}_${treeData.id}`; if (treeData.toString() === '[object Object]' && tree !== null) { tree.key = key; treeData.key = key; tree.title = /* 关键点 */ (<TreeNode maxLevel={this.maxLevel} focusKey={this.state.focusKey} treeData={treeData} enableYaml={this.state.enableYaml} modifyNode={this.modifyNode} addSisterNode={this.addSisterNode} addExpandedKey={this.addExpandedKey} getInToEditable={this.getInToEditable} addSubNode={this.addSubNode} addNodeFragment={this.addNodeFragment} removeNode={this.removeNode} lang={lang(this.state.lang)} />); if (treeData.nodeValue instanceof Array) tree.children = treeData.nodeValue.map(d => this.formatNodeData(d)); } else { tree = ''; } return tree; } /* 生成树数据 */ formatTreeData = (treeData) => { let tree = []; if (treeData instanceof Array) tree = treeData.map(treeNode => this.formatNodeData(treeNode)); return tree; } /* 更新 tree model */ updateTreeModel = (props) => { if (this.treeModel) { this.treeModel.update(props); } else { const _lang = lang(this.state.lang); this.treeModel = new TreeClass( props.data, props.key, { maxLevel: this.state.maxLevel, overLevelTips: _lang.template_tree_max_level_tips, completeEditingNodeTips: _lang.pleaseCompleteTheNodeBeingEdited, addSameLevelTips: _lang.extendedMetadata_same_level_name_cannot_be_added, } ); } } /* 树数据更新钩子,提供给上一层级调用 */ onDataChange = (modifiedData) => { const { onDataChange = () => {} } = this.props; onDataChange(modifiedData); } ... render() { const { treeData } = this.state; return ( <div className="editable-tree-wrapper"> { (treeData && treeData.length) ? <Tree showLine onExpand={this.onExpand} expandedKeys={this.state.expandedKeys} // defaultExpandedKeys={this.state.expandedKeys} defaultExpandAll treeData={treeData} /> : null } </div> ); } } EditableTree.propTypes = { data: PropTypes.array.isRequired, // tree data, required onDataChange: PropTypes.func, // data change callback, default none maxLevel: PropTypes.number, // tree max level, default 50 lang: PropTypes.string, // lang - zh_CN/en_US, default zh_CN enableYaml: PropTypes.bool // enable it if you want to parse yaml string when adding a new node, default false };
Tree类用于抽象化树形数据的增删查改操做,至关于
Model
层
逻辑不算复杂,不少都是递归树数据修改节点,具体代码不予赘述:json
export default class Tree { constructor(data, treeKey, { maxLevel, overLevelTips = '已经限制模板树的最大深度为:', addSameLevelTips = '同层级已经有同名节点被添加!', completeEditingNodeTips = '请完善当前正在编辑的节点数据!', }) { this.treeData = data; this.treeKey = treeKey; this.maxLevel = maxLevel; this.overLevelTips = overLevelTips; this.completeEditingNodeTips = completeEditingNodeTips; this.addSameLevelTips = addSameLevelTips; } ... /* 为输入数据覆盖默认值 */ static defaultTreeValueWrapper() { ... } /* 查询是否有节点正在编辑 */ static findInEdit(items) { ... } /* 进入编辑模式 */ getInToEditable(key, { nodeName, nodeValue, id, isInEdit } = {}) { ... } /* 修改一个节点数据 */ modifyNode(key, { nodeName = '', nodeValue = '', nameEditable = true, valueEditable = true, nodeDeletable = true, isInEdit = false, } = {}) { ... } /* 添加一个目标节点的兄弟结点 */ addSisterNode(key, { nodeName = '', nameEditable = true, valueEditable = true, nodeDeletable = true, isInEdit = true, nodeValue = '', } = {}) { ... } /* 添加一个目标节点的子结点 */ addSubNode(key, { nodeName = '', nameEditable = true, valueEditable = true, nodeDeletable = true, isInEdit = true, nodeValue = '', } = {}) { ... } /* 移除节点 */ removeNode(key) { ... } /* 获取树数据 */ getTreeData() { return deepClone(this.treeData); } /* 更新树数据 */ update({ data, key }) { this.treeData = data; this.treeKey = key; } }
表示单个树节点的React组件,如下均为其子组件,用于展现各个状态下的树层级
每一个层级节点均可以添加子节点、添加同级节点、编辑节点名、编辑节点值、删除当前节点(一并删除子节点),nameEditable
属性控制节点名是否可编辑,valueEditable
树形控制节点值是否可编辑,nodeDeletable
属性控制节点是否能够删除,默认值都是为true
。 数组
isInEdit
属性代表当前节点是否处于编辑状态,处于编辑状态时显示输入框,不然显示文字,当点击文字时当前节点变成编辑状态。antd
简单的页面展现组件,具体实现见 源码:TreeNode.jsx
expandedKeys
属性能够显式指定整颗树中须要展开的节点,expandedKeys
即须要展开节点的key值数组,为了将每一个树节点折叠状态变成受控状态,咱们将expandedKeys
存在state或mobx store中,并在树节点折叠状态改变后更新这个值。... render() { const { treeData } = this.state; return ( <div className="editable-tree-wrapper"> { (treeData && treeData.length) ? <Tree showLine onExpand={this.onExpand} expandedKeys={this.state.expandedKeys} treeData={treeData} /> : null } </div> ); }
TreeNode.jsx
组件中有一个比较严重的问题,如上文提到的EditableTree
的某一层级处于编辑状态时,该层级中的文字展现组件<span>
会变成输入组件<input>
,我发如今编辑模式下Antd的Row/Col
格子布局正常工做,在非编辑模式下因为节点内容从块元素input
变成了内联元素span
,格子布局塌陷了,这种状况下即便声明了Col占用的格子数量,内容依旧使用最小宽度展现,即文字占用的宽度。Row/Col
格子布局自身的问题,没有深究,这边只是将<span>
元素换成了<div>
元素,而且在样式中声明div
占用的最小宽度min-width
,同时设置max-width
和overflow
避免文字元素超出边界。其实Tree组件已经不止写过一次了,以前基于Semantic UI
写过一次,不过由于Semantic UI
没有Tree的基础实现,因此基本上是彻底本身重写的,基本思路其实跟这篇文章写的大体相同,也是递归更新渲染节点,将各个节点的折叠状态放入state进行受控管理,不过此次实现的EditableTree
最主要一点是分离了treeModel
的数据管理逻辑,让界面操做层TreeNode.jsx
、数据管理层Tree.js
和控制层index.jsx
彻底分离开来,结构明了,后期即便想扩展功能也何尝不可。又是跟Antd
斗智斗勇的一次(苦笑脸)...