最近在使用自己封装的 Tree 的时候, 发现只有点击复选框的时候, 全选
复选框可以联动, 通过 setCheckedNodes
、 setCheckedKeys
和 default-checked-keys
设置目前勾选的节点时, 无法联动, 需要再优化
如下图所示:
通过 node
设置后, 全选并没有显示为半选状态
方法一(推荐, 以后只维护这个)
源码在这里
需要用到 check-change
这个 Event
节点选中状态发生变化时的回调
共三个参数, 依次为: 传递给 data 属性的数组中该节点所对应的对象、节点本身是否被选中、节点的子树中是否有被选中的节点
需要在这个方法里判断全选的状态, 但发现这个逻辑有点麻烦: 首先你要对传入的三个参数都需要判断, 还需要和 allNodes
做判断.
对, 其实不需要判断传入的三个参数, 直接拿到 allNodes
, 根据所有根节点的选中状态来处理全选的状态
但这个 Event
只要有节点变化就会触发, 像上图例子会调用好几次, 而且点击复选框的时候也会调用. 咱们在点击复选框的时候已经处理过逻辑了, 如果要使用 check-change
, 那么 check
这个事件就不能要了
1 2 3 4 5 6 7 8 9 10
| <el-tree :ref="ref" v-bind="$attrs" :node-key="nodeKey" :show-checkbox="showCheckbox" v-on="$listeners" @check-change="handleCheckChange" > <slot slot-scope="{ node, data }" v-bind="{ node, data }"> {{ node.label }} </slot> </el-tree>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| data() { return { ref: 'elTree', isIndeterminate: false, isCheckAll: false } }, watch: { defaultCheckedKeys: { handler: 'handleCheckChange', immediate: true } }, methods: { handleCheckChange() { if (!this.showCheckAll || !this.showCheckbox) { return } this.debounce( this.$nextTick(() => { this.handleCheckAllStatus() }), 100 ) }, debounce(func, wait) { var timeout
return function() { var context = this var args = arguments
clearTimeout(timeout) timeout = setTimeout(function() { func.apply(context, args) }, wait) } }, handleCheckAllStatus() { const elTreeStore = this.$refs[this.ref].store const allNodes = elTreeStore ._getAllNodes() .filter(({ level, visible }) => level === 1 && visible) this.checkAll = allNodes.every(({ checked }) => checked) this.isIndeterminate = allNodes.some(({ indeterminate }) => indeterminate) || (allNodes.some(({ checked }) => checked) && !this.checkAll) } }
|
方法二
源码在这里
处理一下传入 el-tree
的 data
数据, 加上一个 全选
的根节点
1 2 3 4 5 6 7 8 9 10
| <el-tree :ref="ref" v-bind="$attrs" :data="treeData" :node-key="nodeKey" :show-checkbox="showCheckbox" v-on="$listeners" > <slot slot-scope="{ node, data }" v-bind="{ node, data }"> {{ node.label }} </slot> </el-tree>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| props: { data: { type: Array, default() { return [] } } }, data() { return { treeData: [], ref: 'elTree' } }, mounted() { this.treeData = this.showCheckAll && this.showCheckbox ? [ { [this.$refs[this.ref].props.label]: '全选', [this.$refs[this.ref].nodeKey || 'id']: 'rootId', [this.$refs[this.ref].props.children]: this.data } ] : this.data }
|
好了, 组件封装完了. 全选的状态由 el-tree
内部处理, 咱们什么也不用管, 完成!
才怪嘞!!! 这样虽然简单, 但毕竟修改数据了, 一旦涉及到修改数据, 就会有很多麻烦需要处理.
比如: getChecked
这些获取选中节点的方法有可能会返回咱们添加的全选的数据, 而且全选的 check
和 check-change
也会触发(不过这个影响不算太大, 可以根据实际需求灵活应变)
那就只能修改 getChecked
方法, 先拿到当前选中的数据, 过滤掉 全选
的数据, 不能使用 el-tree
的默认方法了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| data() { return { treeData: [], ref: 'elTree', checkAllId: '__rootId__' } }, watch: { data: { handler: 'handleData', immediate: true } } computed: { isCheckAll() { return this.showCheckAll && this.showCheckbox } }, methods: { handleData() { if (this.isCheckAll && this.data.length) { this.treeData = [ { [this.$refs[this.ref].props.label]: '全选', [this.nodeKey]: this.checkAllId, [this.$refs[this.ref].props.children]: this.data } ] } else { this.treeData = this.data } }, getCheckedNodes(leafOnly, includeHalfChecked) { if (this.isCheckAll) { return this.$refs[this.ref] .getCheckedNodes(leafOnly, includeHalfChecked) .filter(node => node[this.nodeKey] !== this.checkAllId) } return this.$refs[this.ref].getCheckedNodes(leafOnly, includeHalfChecked) }, getHalfCheckedNodes() { if (this.isCheckAll) { return this.$refs[this.ref] .getHalfCheckedNodes() .filter(node => node[this.nodeKey] !== this.checkAllId) } return this.$refs[this.ref].getHalfCheckedNodes() }, getCheckedKeys(leafOnly) { if (this.isCheckAll) { return this.$refs[this.ref].getCheckedKeys(leafOnly).filter(key => key !== this.checkAllId) } return this.$refs[this.ref].getCheckedKeys(leafOnly) }, getHalfCheckedKeys() { if (this.isCheckAll) { return this.$refs[this.ref].getHalfCheckedKeys().filter(key => key !== this.checkAllId) } return this.$refs[this.ref].getHalfCheckedKeys() } }, mounted() { for (let key in this.$refs[this.ref]) { if (!(key in this) && typeof this.$refs[this.ref][key] === 'function') { this[key] = this.$refs[this.ref][key].bind(this.$refs[this.ref]) } } }
|
文本溢出显示省略号
刚好最近封装了一个文本溢出显示省略号的组件, 可以拿来一用
1 2 3
| <slot slot-scope="{ node, data }" v-bind="{ node, data }"> <text-ellipsis :content="node.label"></text-ellipsis> </slot>
|
单选时增加 disabled
el-tree
只对选择框处理了 disabled
, 单选时却没有, 所以我们需要增加一下
disabled
有两种方式:
- 通过
props
的 disabled
字段和在 data
中 disabled
对应字段来禁用
- 通过
props
的 disabled
方法来禁用
我们保持这种方式不变, 通过源码可知 disabled
保存在 node
里, 所以我们只需要在这里取即可
我们只需要做两件事:
- 设置
disabled
样式
- 当点击
disabled
节点后, 通过 setCurrentKey
来重置当前选中节点
1 2 3 4 5 6 7 8 9 10
| <el-tree v-on="{ ...$listeners, 'current-change': handleCurrentChange, 'node-click': handleNodeClick }" > <slot slot-scope="{ node, data }" v-bind="{ node, data }"> <text-ellipsis :class="{ 'custom-disabled': node.disabled }" :content="node.label" ></text-ellipsis> </slot> </el-tree>
|
这里使用 v-on="{ ...$listeners, 'current-change': handleCurrentChange, 'node-click': handleNodeClick }"
是防止 emit
两遍 current-change
和 node-click
方法
1 2 3 4
| .custom-disabled { color: #94969a; cursor: not-allowed; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| handleCurrentChange(data, node) { const { key, disabled } = node if (disabled) { this.$refs[this.ref].setCurrentKey(this.currentKey) return } this.currentKey = key this.$emit('current-change', data, node) }, handleNodeClick(data, node, self) { const { disabled } = node if (disabled) return this.$emit('node-click', data, node, self) }
|
el-tree
节点还有右键事件和拖拽事件, 目前项目中没有用到, 就不处理 disabled
了, 如果有用到的话就需要处理一下
过滤时加载对应子节点
el-tree
不会返回过滤节点的子节点, 具体看这里: el-tree 节点过滤加载对应子节点 | Henry
所以我们需要自定义一下过滤方法
1 2 3 4
| <el-tree :filter-node-method="filterNodeMethod" > </el-tree>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| props: { filterNodeMethod: { type: Function, default(value, data, node) { if (!value) return true let parentNode = node.parent, labels = [node.label], level = 1 while (level < node.level) { labels = [...labels, parentNode.label] parentNode = parentNode.parent level++ } return labels.some(label => label.indexOf(value) !== -1) } } }, methods: { filter(value) { this.$refs[this.ref].filter(value) } }
|
单选只能选择叶子节点
最近在工作中经常遇到只能单选叶子节点的功能, 所以我们就需要封装一下.
以前在 SelectTree
中做过这个功能, 但其实应该是 Tree
的功能, 所以就搬到这里啦
基于 ElementUI 封装的 SelectTree | Henry
由以前经验可知需要分两种情况:
- 使用
el-tree
自带的 node.isLeaf
判断是否是叶子节点
- 传入
isLeafMethod
函数来自定义叶子节点
而且处理逻辑与 disabled
类似, 就提取一个函数处理即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| handleCurrentChange(data, node) { const { key, disabled } = node if (this.handleDisabled(disabled, data, node)) { return } this.currentKey = key this.$emit('current-change', data, node) }, handleNodeClick(data, node, self) { const { disabled } = node if (this.handleDisabled(disabled, data, node)) { return } this.$emit('node-click', data, node, self) }, handleDisabled(disabled, data, node) { if ( disabled || (this.isLeafMethod ? !this.isLeafMethod(data, node) : this.currentIsLeaf && !node.isLeaf) ) { this.$refs[this.ref].setCurrentKey(this.currentKey) return true } }
|
问题
方法一 data 为空数组, 全选仍存在(已解决)
由于全选标签只判断了 showCheckAll && showCheckbox
, 所以没数据的时候也是会显示的, 所以还需要判断是否有数据: v-if="showCheckAll && showCheckbox && data.length"
方法一 data 改变后, 显示有问题(已解决)
目前 data
改变后, 没有重新处理全选的状态, 导致全选选中状态有问题.
所以需要 watch data
; 而且发现 store._getAllNodes()
会保留历史数据: 比如第一次 data
就一条数据, _getAllNodes
获取的数据为一条, data
改变为两条数据后, _getAllNodes
获取的数据为三条
通过查看源码发现 nodesMap
保存所有数据, 只要注册节点并且该节点不在 nodesMap
中, 就添加, _getAllNodes
获取的就是这里的所有数据
所以 data
改变后, 需要重新加载 el-tree
:
1 2 3 4 5 6
| watch: { data() { this.key = Math.random() this.handleCheckChange() } }
|
这样修改后, 全选状态确实可以正常显示了, 但绑定的 el-tree
方法又出现问题了: 绑定方法只在 mounted
中绑定了一次, 通过修改 key
重新加载 el-tree
后, 绑定的方法仍然是重新加载前的, 而且重新绑定也不行; 有一些方法需要用到 store
, data
改变后, store
没有更新, 导致 getCheckedNodes
等方法仍获取的是第一次的值, 所以需要重新加载一下 tree
组件
所以不能在封装组件中通过修改 key
来重新加载组件, 会导致绑定的方法有问题
目前暂时在父组件中使用 v-if
控制, 以后看看有没有好方法, 这样上面的代码也不用加了
对比 data
改变前后通过 _getAllNodes
获取的数据发现, 改变前选中的数据, 改变后 checked
仍为 true
, 但通过 getCheckedKeys
获取的数据只有改变后选中的数据, 是不是可以查看源码看看这部分是如何实现的?
node_modules/element-ui/packages/tree/src/model/tree-store.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| getCheckedNodes(leafOnly = false, includeHalfChecked = false) { const checkedNodes = []; const traverse = function(node) { const childNodes = node.root ? node.root.childNodes : node.childNodes;
childNodes.forEach((child) => { if ((child.checked || (includeHalfChecked && child.indeterminate)) && (!leafOnly || (leafOnly && child.isLeaf))) { checkedNodes.push(child.data); }
traverse(child); }); };
traverse(this);
return checkedNodes; }
getCheckedKeys(leafOnly = false) { return this.getCheckedNodes(leafOnly).map((data) => (data || {})[this.key]); }
|
原来人家并没有用 _getAllNodes
获取全部数据, 而是通过 root
拿到所有根节点数据, 再依次遍历子节点来拿到所有选中的数据
那咱们也用 root
不就行了吗?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| watch: { data: { handler: 'handleData', immediate: true, deep: true } }, methods: { handleData() { this.$nextTick(() => { this.allNodes = this.getAllNodes(this.$refs[this.ref].root[childNodes]) this.allNodes.length && (this.maxLevel = Math.max.apply( null, this.allNodes.map(({ level }) => level) )) this.$emit('max-level', this.maxLevel) this.handleCheckChange() return Promise.resolve() }) }, getAllNodes() { let allNodes = [] const traverse = function(node) { const childNodes = node.root ? node.root.childNodes : node.childNodes childNodes.forEach(child => { allNodes.push(child) traverse(child) }) } traverse(this.$refs[this.ref]) return allNodes } }
|
这次把 allNodes
缓存起来了, 避免每次都要获取一下, 由于是引用地址, 所以状态变化后会跟着改变的; 并且还 emit
出去了 max-level
, 省的还要在通过 getTreeMaxLevel
获取
defaultExpandAll defaultExpandedKeys 无效(已解决)
由于目前在初始化的时候就执行了 expandToLevel
方法, 导致只展开到 level
级
而如果用户没有传入 level
, 只传入 defaultExpandAll
或 defaultExpandedKeys
, 默认 level
为 1
, 那只能展开到一级
所以需要添加 isFirst
判断: 默认为 true
, 调用 expandToLevel
时判断是否是第一次调用, 第一次调用的时候判断 defaultExpandAll
或 defaultExpandedKeys
是否有值, 有值的话就不执行后面的代码, 并且把 isFirst
置为 false
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| props: { defaultExpandAll: { type: Boolean, default: false }, defaultExpandedKeys: { type: Array, default() { return [] } } }, data() { return { isFirst: true } }, methods: { expandToLevel(level) { if (this.isFirst && (this.defaultExpandAll || this.defaultExpandedKeys.length)) { this.isFirst = false return } this.isFirst = false } }
|