对于数据量较大的节点树,用elementUI的Tree自带的懒加载模式可以较为方便的进行展示。但同时使用懒加载与选中功能时,选中节点的回填就会比较麻烦。这里举一个完整的实例,同时记录一下回填需要注意的地方。
选中与保存
举1个可以保存选中节点的懒加载树例子:
效果:(注意看一下节点1-2)
具体代码:
HTML:
该例中用数组value保存已选节点、expandedNodes保存已展开节点(用于实现尽量保存下层节点、以及后期添加回填功能)。 levelProps是自行根据源数据结构定义的配置选项,本例中为
{label: ‘dicName’,
isLeaf: ‘isLeaf’,}。
该例中以id为节点的key,保存在value中的是对象数组,只是展示时为方便只展示节点名称。
<div>已选节点:{{value.map(item=> {return item.dicName})}}</div>
<div>已展开过(拿到过其下级)的节点:{{expandedNodes.map(item=> {return item.dicName})}}</div>
<el-tree
ref="dicTree"
:props="levelProps"
highlight-current
node-key="id"
:load="getNodes"
accordion
show-checkbox
lazy
@node-expand="keepExpandedNode"
@check="handleCheckChange">
</el-tree>
//获取树节点
getNodes(node, resolve) {
//这里应是一个异步从后端拿取数据的方法,这不是重点
if (0 === node.level) {
setTimeout(() => {
let myRoot=[
{id: 1, dicName: "A ", isLeaf: false},
{id: 2, dicName: "B ", isLeaf: false},
{id: 3, dicName: "C ", isLeaf: false}
];
resolve(myRoot);
}, 1000);
} else {
setTimeout(() => {
let levelData=[
{id: node.level*10+1, dicName: `${node.level}-1`, isLeaf: false, parentId: node.data.id},
{id: node.level*10+2, dicName: `${node.level}-2`, isLeaf: false, parentId: node.data.id},
{id: node.level*10+3, dicName: `${node.level}-3`, isLeaf: false, parentId: node.data.id}
];
resolve(levelData);
this.$nextTick(() => {
//已选节点变成展开节点时、选值自动替换为下层节点
if (node.checked) {
let list = this.$refs.dicTree.getCheckedNodes();
list = list.filter(mItem => {
return this.expandedNodes.every(item => {
return item.id !== mItem.id
})
});
this.value = list;
}
});
}, 500);
}
},
//保存展开过的节点
keepExpandedNode(nodeData, node, tree) {
let newFlag = this.expandedNodes.every(item => {
return item.id !== nodeData.id
});
if (newFlag && !nodeData.isLeaf) {
this.expandedNodes.push(this.cloneObj(nodeData))
}
}
//点击多选框->整理已选选项并保存
handleCheckChange(node, tree) {
let list = this.cloneObj(tree.checkedNodes); //tree.checkedNodes-未展开过的节点的子节点无法获取到
list = list.filter(mItem => {
return this.expandedNodes.every(item => {
return item.id !== mItem.id
})
});
this.value = list;
},
这样一来,选中的节点对象就即时地保存到了value里。也可以处理后只保留id,因为回填时只需要用来做节点key的那个字段就够了。
回填
如果我们把value里的值保存下来,希望在下次重新渲染树时回填,事情就变得复杂起来了。
element-tree自带属性*“default-checked-keys”——“默认勾选的节点的 key 的数组”*,非懒加载的普通树可以直接把保存的key放到这里。
但是对于懒加载树,父节点并不知道你是否选过它的子节点,而对于未加载完数据的树枝的回填会出现很多bug。这里有几个本人踩坑重写多次后总结的要点:
<template>
<div>
<div>已选节点:{{
value.map(item => {
return item.dicName
})
}}
</div>
<div>已展开过(拿到过其下级)的节点:{{
expandedNodes.map(item => {
return item.dicName
})
}}
</div>
<el-tree
ref="dicTree"
:props="levelProps"
highlight-current
node-key="id"
:load="getNodes"
accordion
show-checkbox
@check="handleCheckChange"
@node-expand="keepExpandedNode"
:default-checked-keys="defaultCheckedNodes"
:default-expanded-keys="defaultExpandedNodes"
lazy>
</el-tree>
</div>
</template>
<script>
export default {
name: "index.vue",
data() {
return {
levelProps: {
label: 'dicName',
isLeaf: 'isLeaf'
},
defaultCheckedNodes:[],
defaultExpandedNodes:[],
expandedNodes: [{id: 1, dicName: 'A', isLeaf: false},
{id: 12, dicName: 1 - 2, isLeaf: false, parentId: 1}], // 保存已展开节点
value: [{id: 11, dicName: '1-1', isLeaf: false, parentId: 1},
{id: 21, dicName: '2-1', isLeaf: false, parentId: 12},
{id: 22, dicName: 2 - 2, isLeaf: false, parentId: 12},
{id: 23, dicName: 2 - 3, isLeaf: false, parentId: 12}]
};
},
methods: {
//深度复制对象
cloneObj(obj) {
let newObj = {};
if (typeof obj === "object") {
if (obj instanceof Array) {
newObj = [];
}
for (var key in obj) {
let val = obj[key];
newObj[key] =
typeof val === "object" ? this.cloneObj(val) : val;
}
return newObj;
} else {
return obj;
}
},
//带查重的深复制(仅适用于单层数组)
cloneWithCheck(Arr) {
let newArr = [];
if (Arr.length > 0) {
newArr.push(this.cloneObj(Arr[0]));
for (let i = 1; i < Arr.length; i++) {
let newFlag = newArr.every(item => {
return item.id !== Arr[i].id
});
if (newFlag) {
newArr.push(this.cloneObj(Arr[i]));
}
}
}
return newArr;
},
//获取树节点
getNodes(node, resolve) {
if (0 === node.level) {
///如果需要多次回填,该初始化必需
this.meaningExpandedNum = 0;
this.defaultCheckedNodes = [];
///回填展开节点(这里我略去了从后端拿数据回填到this.expandedNodes里的代码。实际上这里如果能直接拿数据放到defaultExpandedNodes里也行)
let expandedNodes = this.cloneWithCheck(this.expandedNodes);
this.defaultExpandedNodes = expandedNodes.map(item => {
return item.id
});
setTimeout(() => {
let myRoot = [
{id: 1, dicName: "A ", isLeaf: false},
{id: 2, dicName: "B ", isLeaf: false},
{id: 3, dicName: "C ", isLeaf: false}
];
resolve(myRoot);
///注意回填要在树渲染后才生效
this.$nextTick(() => {
///没展开过节点,则直接在根节点层级回填
///(这里我略去了在页面开始渲染前就从后端拿数据回填到this.value里的代码。实际上这里如果能直接拿数据放到defaultCheckedNodes里也行)
if (0 === this.defaultExpandedNodes.length) {
this.defaultCheckedNodes = this.value.map(item => {
return item.id
});
}
});
}, 500);
} else {
setTimeout(() => {
let levelData = [
{id: node.level * 10 + 1, dicName: `${node.level}-1`, isLeaf: false, parentId: node.data.id},
{id: node.level * 10 + 2, dicName: `${node.level}-2`, isLeaf: false, parentId: node.data.id},
{id: node.level * 10 + 3, dicName: `${node.level}-3`, isLeaf: false, parentId: node.data.id}
];
resolve(levelData);
this.$nextTick(() => {
///已选节点变成展开节点时、选值自动替换为下层节点;这是为了下次回填做的准备,回填过程中用不到
if (node.checked) {
let list = this.$refs.dicTree.getCheckedNodes();
list = list.filter(mItem => {
return this.expandedNodes.every(item => {
return item.id !== mItem.id
})
});
this.value = list;
}
///回填时保证在全部渲染后再回填(选中节点的回填时机是核心难点)
this.meaningExpandedNum++;
if (this.meaningExpandedNum === this.defaultExpandedNodes.length) {
///(这里我略去了在页面开始渲染前就从后端拿数据回填到this.value里的代码。实际上这里如果能直接拿数据放到defaultCheckedNodes里也行)
this.defaultCheckedNodes = this.value.map(item => {
return item.id
});
}
});
}, 500);
}
},
//保存展开过的节点
keepExpandedNode(nodeData, node, tree) {
let newFlag = this.expandedNodes.every(item => {
return item.id !== nodeData.id
});
if (newFlag && !nodeData.isLeaf) {
this.expandedNodes.push(this.cloneObj(nodeData))
}
},
//点击多选框->整理已选选项并保存
handleCheckChange(node, tree) {
let list = this.cloneObj(tree.checkedNodes); //tree.checkedNodes-未展开过的节点的子节点无法获取到
list = list.filter(mItem => {
return this.expandedNodes.every(item => {
return item.id !== mItem.id
})
});
this.value = list;
},
},
created() {
}
}
</script>
<style scoped>
</style>