本篇文章主要讲述element-ui中的tree组件的树节点选择功能实现,没看过前两篇内容请自行查看。
Element源码之tree组件分析和
Element源码之tree组件分析- 懒加载
思路:树节点选择肯定需要checkbox组件去选择节点,然后在勾选的时候需要考虑当勾选的是父节点前面的checkbox,父节点下面的子节点全都要设置为选中状态。同理可得,取消勾选父节点前面的勾选框,父节点下面的子节点全都要取消勾选; 当勾选子节点的时候,需要考虑的是当同一层级的子节点都勾选上了,其父节点也需要设置为选中状态。同理,当同一层级的子节点从有变成无,也需要取消其父节点的选中。
1. 显示checkbox组件
先在tree.vue中定义showCheckbox、checkStrictly、checkOnClickNode和checkDescendants属性,showCheckbox表示节点是否可选,checkStrictly表示在显示节点可选的情况下,父子节点不相互关联(通俗来讲就是父节点选中的情况下,子节点不会选中;同一层级子节点都选中的情况下父节点不选中),checkOnClickNode就是是否在点击节点的时候选中节点,checkDescendants表示在选中父节点的时候是否选择后代节点。
这里只展示代码新增部分
<el-tree-node
v-for="child in root.childNodes"
:node="child"
:props="props"
:render-after-expand="renderAfterExpand"
:key="getNodeKey(child)"
:show-checkbox="showCheckbox" // 新增
:render-content="renderContent"
@node-expand="handleNodeExpand">
</el-tree-node>
// 属性
props: {
...
checkStrictly: Boolean,
checkOnClickNode: Boolean,
// 是否在点击节点的时候选中节点,默认值为false,即只有在点击复选框时才会选中节点。
checkDescendants: {
type: Boolean,
default: false,
},
showCheckbox: {
type: Boolean,
default: false,
},
}
// 数据监听
watch: {
...
checkStrictly(newVal) {
this.store.checkStrictly = newVal;
},
},
// created生命周期
created() {
this.isTree = true;
this.store = new TreeStore({
...,
checkStrictly: this.checkStrictly,
checkDescendants: this.checkDescendants,
});
},
在tree组件中定义了相关属性就进行可选节点的渲染。我们在tree组件中把showCheckbox属性传递给tree-node组件,那么可选节点的渲染自然也是在tree-node组件进行渲染。
<el-checkbox
v-if="showCheckbox"
v-model="node.checked"
:indeterminate="node.indeterminate"
:disabled="!!node.disabled"
@click.native.stop
@change="handleCheckChange"
>
</el-checkbox>
<el-tree-node
:render-content="renderContent"
v-for="child in node.childNodes"
:render-after-expand="renderAfterExpand"
:show-checkbox="showCheckbox" // 新增
:key="getNodeKey(child)"
:node="child"
@node-expand="handleChildNodeExpand"
>
</el-tree-node>
props: {
showCheckbox: {
type: Boolean,
default: false,
},
}
值得注意的是,这里也需要定义showCheckbox属性,因为这里需要递归渲染tree-node组件,所以需要定义showCheckbox属性并传入tree-node组件中。
2. 节点选择监听
下面就描述一下勾选节点之后的操作,勾选节点之后肯定是要选中当前节点,这是毋庸置疑的。然后根据checkStrictly和checkDescendants属性来判断是否需要进行父子节点的关联。
话不多说,我们来看下checkbox的change事件回调handleCheckChange。
handleCheckChange(value, ev) {
this.node.setChecked(ev.target.checked, !this.tree.checkStrictly);
this.$nextTick(() => {
const { store } = this.tree;
this.tree.$emit('check', this.node.data, {
checkedNodes: store.getCheckedNodes(),
checkedKeys: store.getCheckedKeys(),
halfCheckedNodes: store.getHalfCheckedNodes(),
halfCheckedKeys: store.getHalfCheckedKeys(),
});
});
},
这段代码首先调用了node的setChecked方法进行节点选中处理。然后在tree实例上发送check事件。
我们重点看下setChecked方法是怎么处理节点选中的。
setChecked(value, deep, recursion, passValue) {
this.indeterminate = value === 'half';
this.checked = value === true;
if (this.store.checkStrictly) return;
if (!(this.shouldLoadData() && !this.store.checkDescendants)) {
let { all, allWithoutDisable } = getChildState(this.childNodes);
if (!this.isLeaf && (!all && allWithoutDisable)) {
this.checked = false;
value = false;
}
const handleDescendants = () => {
if (deep) {
const childNodes = this.childNodes;
for (let i = 0, j = childNodes.length; i < j; i++) {
const child = childNodes[i];
passValue = passValue || value !== false;
const isCheck = child.disabled ? child.checked : passValue;
child.setChecked(isCheck, deep, true, passValue);
}
const { half, all } = getChildState(childNodes);
if (!all) {
this.checked = all;
this.indeterminate = half;
}
}
};
if (this.shouldLoadData()) {
// Only work on lazy load data;
this.loadData(() => {
handleDescendants();
reInitChecked(this);
}, {
checked: value !== false
});
return;
} else {
handleDescendants();
}
}
const parent = this.parent;
if (!parent || parent.level === 0) return;
if (!recursion) {
reInitChecked(parent);
}
}
代码还是比较长的,我们逐行进行分析。
首先就是函数传参, value:是否选中,deep: 是否深层次进行选中(也就是对后代节点的选中),recursion: 是否进行递归,passValue:父节点是否选中的值。
indeterminate表示节点是否部分选中,只有当它的值为half才是部分选中。
checked属性只有当前值为true才设置为true, 这里是为了防止传进来的值是undefined、null等不可预料的值,如果直接赋值的话就会产生预料不到的结果。
如果checkStrictly值为true的话,则直接返回。这也从侧面说明下面的代码是处理父子节点选择的联动。
如果是除了懒加载并且不选择后代节点这种情况,就进行父子节点联动处理。
接下来这行有个getChildState方法,我们先贴代码看看。
export const getChildState = node => {
let all = true;
let none = true;
let allWithoutDisable = true;
for (let i = 0, j = node.length; i < j; i++) {
const n = node[i];
if (n.checked !== true || n.indeterminate) {
all = false;
if (!n.disabled) {
allWithoutDisable = false;
}
}
if (n.checked !== false || n.indeterminate) {
none = false;
}
}
return { all, none, allWithoutDisable, half: !all && !none };
};
getChildState顾名思义就是获取子节点状态,通过遍历子节点数组来判断当前节点状态。all就是是否全选,none就是是否全部未选,allWithoutDisable表示是否全部禁用,但是这个变量名还是具有迷惑性的。
接下来我们回到setChecked方法,获取完状态之后,如果当前节点不是叶子节点并且部分节点已勾选并且全部节点禁用的话,就设置当前节点为为未选中状态。
接下来定义了一个处理叶子节点的选中状态。如果是深层次选中(deep为true)的话,就遍历其叶子节点设置选中状态,然后再递归遍历深一层的孙节点,直到叶子结点。注意:如果当前节点的子节点部分选中,则当前节点设置为非选中状态,并设置为部分节点选中。
接下来就是懒加载和非懒加载调用handleDescendants方法了。
我们注意到懒加载获取数据后除了处理叶子节点选中状态,还调用了另外一个方法reInitChecked。
话不多说,直接贴代码。
const reInitChecked = function(node) {
if (node.childNodes.length === 0 || node.loading) return;
const {all, none, half} = getChildState(node.childNodes);
if (all) {
node.checked = true;
node.indeterminate = false;
} else if (half) {
node.checked = false;
node.indeterminate = true;
} else if (none) {
node.checked = false;
node.indeterminate = false;
}
const parent = node.parent;
if (!parent || parent.level === 0) return;
if (!node.store.checkStrictly) {
reInitChecked(parent);
}
};
这个方法就是根据当前节点的子节点选中状态来设置当前节点的选中状态。只有当子节点全部选中,当前节点才设置选中状态,当部分节点选中时,indeterminate才设置为true。
如果已经到了根节点则停止递归设置节点选中状态,如果checkStrictly设置为false, 也就是父子节点不关联,则不进行父节点的递归设置选中状态。
最后一步就是获取父节点,如果是非递归的话,就设置当前节点的父节点状态。这句代码的意思是,等递归到叶子节点,此时recursion的值是undefined,这个时候就开始由下往上逐级递归设置父节点选中状态。
其他细节我就不赘述了。