From 08977634d45aab9aa89e4ac5d865c5a7002d5f7a Mon Sep 17 00:00:00 2001 From: ChuChencheng Date: Sun, 11 Aug 2024 02:25:01 +0800 Subject: [PATCH] feat: optimize loadChildren and update doc --- site/.vitepress/code/ReloadChildren.vue | 16 ++++- site/.vitepress/code/UpdateCustomField.vue | 66 ++++++++++++++++++++ site/api/vtree.md | 2 + site/en/api/vtree.md | 2 + site/en/examples/node-manipulation.md | 20 ++++++ site/examples/node-manipulation.md | 6 ++ src/store/tree-store.ts | 71 +++++++++++++++------- tests/unit/tree.spec.ts | 2 +- 8 files changed, 160 insertions(+), 25 deletions(-) create mode 100644 site/.vitepress/code/UpdateCustomField.vue diff --git a/site/.vitepress/code/ReloadChildren.vue b/site/.vitepress/code/ReloadChildren.vue index 47b2908..2a91d8d 100644 --- a/site/.vitepress/code/ReloadChildren.vue +++ b/site/.vitepress/code/ReloadChildren.vue @@ -1,8 +1,9 @@ @@ -47,6 +48,17 @@ const handleSetChildren = () => { const handleClearChildren = () => { tree.value.updateNode('node-1', { children: [] }) } +const handleUpdateChildren = () => { + tree.value.updateNode('node-1', { + children: children.map((child) => { + return { + ...child, + title: `${child.title} ${Date.now()}`, + checked: true, + } + }) + }) +} diff --git a/site/api/vtree.md b/site/api/vtree.md index 41d6d34..fe1aafa 100644 --- a/site/api/vtree.md +++ b/site/api/vtree.md @@ -97,6 +97,8 @@ | filter | 过滤节点 | `keyword: string`: 过滤关键词
`filterMethod: (keyword: string, node: TreeNode) => boolean`: 过滤方法,默认为 filterMethod Prop ,如果没有传 filterMethod Prop 则为搜索 title 字段的一个内置方法 | `void` | | showCheckedNodes | 展示已选节点 | `showUnloadCheckedNodes: boolean`: 是否显示未加载的选中节点,默认为 Prop 传入的值 | `void` | | loadRootNodes | 从远程加载根节点 | 无 | `Promise` | +| updateNode `4.1.0` | 更新单个节点 | `key: string \| number`: 节点 key
`newNode: object`: 新节点数据,某些字段将被忽略,例如以下划线 "_" 开头的字段,以及 key 字段和 `indeterminate`, `visible`, `isLeaf` 等 | `void` | +| updateNodes `4.1.0` | 更新多个节点 | `newNodes: object[]`: 新节点数据数组,与 `updateNode` 相同,特定的字段会被忽略,且没有 key 字段的元素将被忽略 | `void` | | scrollTo | 滚动到指定节点位置 | `key: string \| number`: 节点 key
`verticalPosition: 'top' \| 'center' \| 'bottom' \| number`: 滚动的垂直位置 | `void` | ## VTree Slots diff --git a/site/en/api/vtree.md b/site/en/api/vtree.md index ef725a0..0eb7e9b 100644 --- a/site/en/api/vtree.md +++ b/site/en/api/vtree.md @@ -97,6 +97,8 @@ Note: Since `2.0.8`, the node info returned in events contains the full node inf | filter | Filter nodes | `keyword: string`: filter keyword
`filterMethod: (keyword: string, node: TreeNode) => boolean`: filter method, default to filterMethod prop. if filterMethod prop is not present, it's an internal method that searches node title | `void` | | showCheckedNodes | Show checked nodes | `showUnloadCheckedNodes: boolean`: whether to show checked nodes that are not loaded, default to prop value | `void` | | loadRootNodes | Load root nodes from remote | None | `Promise` | +| updateNode `4.1.0` | Update single node | `key: string \| number`: node key
`newNode: object`: new node data, some fields will be ignored, like those start with underscore '_', the key field and `indeterminate`, `visible`, `isLeaf`, etc. | `void` | +| updateNodes `4.1.0` | Update multiple nodes | `newNodes: object[]`: new nodes array, some specific fields will be ignored like `updateNode`, and the elements without key field also will be ignored | `void` | | scrollTo | Scroll to specific node position | `key: string \| number`: node key
`verticalPosition: 'top' \| 'center' \| 'bottom' \| number`: vertical position of scrolling | `void` | ## VTree Slots diff --git a/site/en/examples/node-manipulation.md b/site/en/examples/node-manipulation.md index 5e4f92e..216d33c 100644 --- a/site/en/examples/node-manipulation.md +++ b/site/en/examples/node-manipulation.md @@ -22,3 +22,23 @@ Enable `draggable` and `droppable` - Invoke `remove` to remove a node + +## Update Node Title {#update-node-title} + +Invoke `updateNode` method to update some fields of tree node + +Invoke `updateNodes` to update multiple nodes + + + +## Update Custom Field {#update-custom-field} + +Invoke `updateNode` method to update custom fields in tree node + + + +## Reload Child Nodes {#reload-children} + +Invoke `updateNode` and pass a new `children` list to reload child nodes + + diff --git a/site/examples/node-manipulation.md b/site/examples/node-manipulation.md index a07ed86..98947b0 100644 --- a/site/examples/node-manipulation.md +++ b/site/examples/node-manipulation.md @@ -31,6 +31,12 @@ +## 更新自定义字段 {#update-custom-field} + +调用树组件的 `updateNode` 方法更新自定义字段 + + + ## 重新加载子节点 {#reload-children} 调用 `updateNode` 传入新的 `children` 列表可以重新加载子节点 diff --git a/src/store/tree-store.ts b/src/store/tree-store.ts index 631615e..15d1bd1 100644 --- a/src/store/tree-store.ts +++ b/src/store/tree-store.ts @@ -291,18 +291,15 @@ export default class TreeStore extends TreeEventTarget { } else { // 设置的节点不是当前已选中节点,要么当前没有选中节点,要么当前有选中节点 if (value) { - if (this.currentSelectedKey === null) { - // 当前没有选中节点 - node.selected = value - this.currentSelectedKey = node[this.options.keyField] - } else { + if (this.currentSelectedKey !== null) { // 取消当前已选中,设置新的选中节点 if (this.mapData[this.currentSelectedKey]) { this.mapData[this.currentSelectedKey].selected = false } - node.selected = value - this.currentSelectedKey = node[this.options.keyField] } + node.selected = value + this.currentSelectedKey = node[this.options.keyField] + this.unloadSelectedKey = null } } @@ -327,9 +324,7 @@ export default class TreeStore extends TreeEventTarget { triggerDataChange: boolean = true ): void { if (value) { - if (this.currentSelectedKey) { - this.setSelected(this.currentSelectedKey, false, false, false) - } + this.currentSelectedKey = null this.unloadSelectedKey = key } else { if (this.unloadSelectedKey === key) { @@ -492,8 +487,13 @@ export default class TreeStore extends TreeEventTarget { } } + private isChildrenChanged(node: TreeNode, newNode: ITreeNodeOptions): boolean { + return ('children' in newNode) && (!!node.children.length || !!newNode.children?.length) + } + updateNode(key: TreeNodeKeyType, newNode: ITreeNodeOptions, triggerEvent = true, triggerDataChange = true) { - if (!this.mapData[key]) return + const node = this.mapData[key] + if (!node) return const newNodeCopy: ITreeNodeOptions = {} const notAllowedFields = [ @@ -512,14 +512,15 @@ export default class TreeStore extends TreeEventTarget { const previousCheckedKeys = this.getCheckedKeys() const previousSelectedKey = this.getSelectedKey() + let triggerSetDataFlag = this.isChildrenChanged(node, newNodeCopy) - if ('children' in newNodeCopy) { + if (('children' in newNodeCopy) && (!!node.children.length || !!newNodeCopy.children?.length)) { // remove all children this.removeChildren(key, false, false) // add new children if (Array.isArray(newNodeCopy.children)) { - this.loadChildren(this.mapData[key], newNodeCopy.children, this.mapData[key].expand) + this.loadChildren(node, newNodeCopy.children, node.expand) } delete newNodeCopy.children @@ -537,7 +538,7 @@ export default class TreeStore extends TreeEventTarget { delete newNodeCopy.expand } Object.keys(newNodeCopy).forEach((field) => { - this.mapData[key][field] = newNodeCopy[field] + node[field] = newNodeCopy[field] }) const currentCheckedKeys = this.getCheckedKeys() @@ -554,6 +555,9 @@ export default class TreeStore extends TreeEventTarget { } if (triggerDataChange) { + if (triggerSetDataFlag) { + this.emit('set-data') + } this.emit('visible-data-change') } } @@ -564,9 +568,15 @@ export default class TreeStore extends TreeEventTarget { const previousCheckedKeys = this.getCheckedKeys() const previousSelectedKey = this.getSelectedKey() + let triggerSetDataFlag = false - validNodes.forEach((node) => { - this.updateNode(node[this.options.keyField], node, false, false) + validNodes.forEach((newNode) => { + const key = newNode[this.options.keyField] + const node = this.mapData[key] + if (node) { + triggerSetDataFlag = triggerSetDataFlag || this.isChildrenChanged(node, newNode) + this.updateNode(key, newNode, false, false) + } }) const currentCheckedKeys = this.getCheckedKeys() @@ -580,6 +590,10 @@ export default class TreeStore extends TreeEventTarget { this.triggerSelectedChange(true, false) } + if (triggerSetDataFlag) { + this.emit('set-data') + } + this.emit('visible-data-change') } @@ -894,6 +908,7 @@ export default class TreeStore extends TreeEventTarget { if (!node || !node.children.length) return null const firstChild = node.children[0] + let movingNode = firstChild // 从 flatData 中移除 const index = this.findIndex(node) @@ -905,6 +920,11 @@ export default class TreeStore extends TreeEventTarget { // 从 mapData 中移除 delete this.mapData[this.flatData[i][this.options.keyField]] deleteCount++ + + // 如果是 Selected 的节点,则记录 + if (this.flatData[i].selected) { + movingNode = this.flatData[i] + } } else break } this.flatData.splice(index + 1, deleteCount) @@ -915,7 +935,7 @@ export default class TreeStore extends TreeEventTarget { node.indeterminate = false // 更新被移除处父节点状态 - this.updateMovingNodeStatus(firstChild, triggerEvent, triggerDataChange) + this.updateMovingNodeStatus(movingNode, triggerEvent, triggerDataChange) if (triggerDataChange) { this.emit('visible-data-change') @@ -935,7 +955,7 @@ export default class TreeStore extends TreeEventTarget { const currentCheckedKeys = this.getCheckedKeys() const flattenChildren = this.flattenData( node.children, - this.getSelectedKey === null + this.getSelectedKey() === null ) this.insertIntoFlatData(parentIndex + 1, flattenChildren) // 如果有未加载的选中节点,判断其是否已加载 @@ -943,6 +963,8 @@ export default class TreeStore extends TreeEventTarget { if (this.unloadSelectedKey !== null) { this.setUnloadSelectedKey(this.unloadSelectedKey) } + + this.checkNodeUpward(node, true) } private getInsertedNode( @@ -1168,8 +1190,6 @@ export default class TreeStore extends TreeEventTarget { if (node.checked && this.options.cascade) { // 向下勾选,包括自身 this.checkNodeDownward(node, true) - // 向上勾选父节点直到根节点 - this.checkNodeUpward(node) } if (node.selected && overrideSelected) { @@ -1191,6 +1211,12 @@ export default class TreeStore extends TreeEventTarget { this.flattenData(node.children, overrideSelected, result) } } + + if (this.options.cascade && !!length) { + // 向上勾选父节点直到根节点 + this.checkNodeUpward(nodes[0]) + } + return result } @@ -1230,9 +1256,10 @@ export default class TreeStore extends TreeEventTarget { /** * 向上勾选/取消勾选父节点,不包括自身 * @param node 需要勾选的节点 + * @param fromCurrentNode 是否从当前节点开始处理 */ - private checkNodeUpward(node: TreeNode) { - let parent = node._parent + private checkNodeUpward(node: TreeNode, fromCurrentNode = false) { + let parent = fromCurrentNode ? node : node._parent while (parent) { this.checkParentNode(parent) parent = parent._parent diff --git a/tests/unit/tree.spec.ts b/tests/unit/tree.spec.ts index 7050458..f342c34 100644 --- a/tests/unit/tree.spec.ts +++ b/tests/unit/tree.spec.ts @@ -168,7 +168,7 @@ describe('树展示测试', () => { ).toBe(true) expect( treeNodes[1].find('.vtree-tree-node__checkbox_indeterminate').exists() - ).toBe(true) + ).toBe(false) expect( treeNodes[2].find('.vtree-tree-node__title_selected').exists() ).toBe(true)