>> 博客原文連接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
鬥智鬥勇的一次(苦笑臉)...