>> 博客原文链接

I 前言


Antd是基于Ant Design设计体系的React UI组件库,次要用于研发企业级中后盾产品,在前端很多我的项目中都有应用。除了提供一些比拟根底的例如ButtonFormInputModalList...组件,还有TreeUploadTable这几个性能集成度比拟高的简单组件,其中Tree组件的利用场景挺多的,在一些波及显示树形构造数据的性能中能够体现:目录构造展现、族谱关系图...,总之在须要出现多个父子层级之间构造关系的场景中就可能用到这种Tree组件,Antd尽管官网提供了Tree组件然而它的性能比拟无限,定位是次要负责对数据的展现工作,树数据的增删查改这些性能根本没有反对,然而Antd Tree的属性反对比较完善,咱们能够基于Antd树来实现反对编辑性能的EditableTree组件。

源码:nojsja/EditableTree

曾经公布为npm组件,能够间接装置:

$: npm install editable-tree-antd# or$: yarn add editable-tree-antd

预览

II 功能分析


  1. 非叶子节点的节点名不为空,节点值为空或数组
  2. 叶子节点的节点名可为空,节点值不可为空
  3. 点击树节点进入节点编辑状态,提交后实现节点数据更新
  4. 非叶子节点每一层级都反对兄弟节点增加、子节点增加、以后节点删除以及节点名、节点值编辑
  5. 叶子节点只反对以后节点删除和以后节点的节点名、节点值编辑
  6. 树的每一层级的节点名和节点值是否能够编辑、节点是否能够删除均能够通过传入的节点数据属性管制,默认状况下所有节点可编辑、可删除
  7. 树的层级深度反对属性配置,子节点深度不能超过树的最大深度值,默认为50层子级
  8. 新增反对:将一段yaml字符串解析为多个树节点

III 实现解析


基于React / Antd / Mobx

Antd Tree文档

文件构造

--- 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文件

实现原理

  • 先来看下Antd原生须要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.jsModel外面进行治理,这样子思路就比拟清晰了。

关键点阐明:index.js

入口文件,用于:数据初始化、组件生命周期管制、递归调用TreeNode进行数据渲染、加载lang文件等等
  • 在生命周期componentDidMount中咱们初始化一个Tree Model,并设置初始化state数据。
  • componentWillReceiveProps中咱们更新这个Model和state以管制界面状态更新,留神应用的Js数据深比拟函数deepComparison用来防止不必要的数据渲染,数据深比拟时要应用与树显示相干的节点属性裸数据(见办法getNudeTreeData),比方nodeNamenodeValue等属性,其它的无关属性比方iddepth须要疏忽。
  • 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.js

Tree类用于抽象化树形数据的增删查改操作,相当于Model

逻辑不算简单,很多都是递归树数据批改节点,具体代码不予赘述:

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

关键点阐明:TreeNode.jsx

示意单个树节点的React组件,以下均为其子组件,用于展现各个状态下的树层级
  • TreeNodeDisplay.jsx -- 非编辑状态下树数据的展现
  • TreeNodeNormalEditing.jsx -- 一般节点处于编辑状态下时
  • TreeNodeYamlEditing.jsx -- yaml节点处于编辑状态下时
  • TreeNodeActions.jsx -- 该层级树节点的所有性能按钮组

每个层级节点都能够增加子节点、增加同级节点、编辑节点名、编辑节点值、删除以后节点(一并删除子节点),nameEditable属性管制节点名是否可编辑,valueEditable树形管制节点值是否可编辑,nodeDeletable属性管制节点是否能够删除,默认值都是为true

isInEdit属性表明以后节点是否处于编辑状态,处于编辑状态时显示输入框,否则显示文字,当点击文字时以后节点变成编辑状态。

简略的页面展现组件,具体实现见 源码:TreeNode.jsx

IV 遇到的问题&解决办法


树数据更新渲染导致的节点折叠状态重置

  • 设想咱们关上了树的两头某个层级进行节点名编辑,编辑实现后点击提交,树从新渲染刷新,而后之前编辑的节点又从新折叠起来了,咱们须要从新关上那个层级看是否编辑胜利,这种应用体验无疑是苦楚的。
  • 造成树节点折叠状态重置的起因就是树的从新渲染,且这个折叠状态的控制数据并没有裸露到每个TreeNode上,所以在咱们本人实现的TreeNode中无奈独立管制树节点的折叠/开展。
  • 查看官网文档,传入树的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>    );  }

Antd格子布局塌陷

  • TreeNode.jsx组件中有一个比较严重的问题,如上文提到的EditableTree的某一层级处于编辑状态时,该层级中的文字展现组件<span>会变成输出组件<input>,我发现在编辑模式下Antd的Row/Col格子布局失常工作,在非编辑模式下因为节点内容从块元素input变成了内联元素span,格子布局塌陷了,这种状况下即便申明了Col占用的格子数量,内容仍旧应用最小宽度展现,即文字占用的宽度。
  • 揣测起因是Antd的Row/Col格子布局本身的问题,没有深究,这边只是将<span>元素换成了<div>元素,并且在款式中申明div占用的最小宽度min-width,同时设置max-widthoverflow防止文字元素超出边界。

V 结语


其实Tree组件曾经不止写过一次了,之前基于Semantic UI写过一次,不过因为Semantic UI没有Tree的根底实现,所以基本上是齐全本人重写的,基本思路其实跟这篇文章写的大致相同,也是递归更新渲染节点,将各个节点的折叠状态放入state进行受控治理,不过这次实现的EditableTree最次要一点是拆散了treeModel的数据管理逻辑,让界面操作层TreeNode.jsx、数据管理层Tree.js和管制层index.jsx齐全拆散开来,构造明了,前期即便想扩大性能也未尝不可。又是跟Antd斗智斗勇的一次(苦笑脸)...