Preface
The Tree component is very widely used in practical applications, such as the display of provinces, cities and counties. Generally, some data containing affiliation can be displayed using the Tree component. Let's learn more about what you need to understand to implement a Tree component through a practical demo. Principles and implementation details. The functions implemented in this article include the following three points.
- Implement a basic version of the Tree component that can display nested data
- Click on a label of the Tree component and its next level of data supports asynchronous loading
- The nodes of the Tree component support drag and drop
The final rendering of the Demo is as follows.
Basic version of Tree
It is very simple to implement a basic version of the Tree component, and the principle is to master the use of component nesting.
External call
First, set the template for externally calling the Tree component as follows. The Tree component only needs to pass in a data attribute to render the data data into the corresponding tree structure.
<template>
<Tree
:data="data"
/>
</template>
<script>
import Tree from "../../components/Tree/index.vue";
export default {
data() {
return {
data: [
{
label: "一级",
children: [
{
label: "二级1",
children: [
{
label: "三级1",
},
],
}
],
}
],
};
},
components: {
Tree,
}
}
</script>
Implementation of the Tree component
The Tree component contains two files, one is index.vue
and the other is Tree.vue
.
index.vue
index.vue
The content is as follows.It serves as a bridge between the Tree component and the outside world.Many extended functions can be processed in this middle layer, and Tree.vue
only the logic related to the data display is required.
<template>
<div class="tree">
<template v-for="(item, index) in data_source">
<Tree :item="item" :key="index" class="first-layer" />
</template>
</div>
</template>
<script>
import Tree from "./Tree";
import { deepClone } from "../util/tool.js"; //深度克隆函数
export default {
props: {
data: Object | Array,
},
data() {
return {
tree_data: deepClone(this.data),
};
},
computed: {
data_source() {
if (Array.isArray(this.tree_data)) {
return this.tree_data;
} else {
return [this.tree_data];
}
},
},
components: {
Tree,
}
}
</script>
The above code data
makes a deep clone assignment of the data passed in from the outside tree_data
. If there are some functions inside the component that change the data, then it can be operated directly tree_data
without affecting the data passed in from the outside world.
In order for the Tree component to support array rendering and object rendering, a new calculation property is data_source
added to convert all data sources into arrays, and then traverse the rendering Tree
components;
Tree.vue
Tree.vue
The file contains the specific code for rendering the tree structure data. Its template content is divided into two parts, one is to render the label
corresponding title content, and the other is to render the child.
Set a state in the component is_open
to control whether its next level is open or closed.
getClassName
The is_open
corresponding class name can be rendered to show whether the triangle icon is displayed downward or to the right.
In Tree.vue
setting a name attribute Tree
, then you can call yourself a nest in the template. By traversing item.children
, the data is copied to each level of Tree
components, to achieve the purpose of rendering the tree structure data.
<template>
<div class="tree">
<div
class="box"
@click="toggle()"
>
<div :class="['lt', getClassName]"></div>
<div class="label lt">{
{ item.label }}</div>
</div>
<div class="drop-list" v-show="is_open">
<template v-for="(child, index) in item.children">
<Tree :item="child" :key="index" />
</template>
</div>
</div>
</template>
<script>
export default {
name: "Tree",
props: {
item: Object
},
data() {
return {
is_open: false, //是否打开下一级
};
},
computed: {
getClassName(){
return this.is_open ? "down" : "right";
}
},
methods:{
toggle() {
this.is_open = !this.is_open;
},
}
};
</script>
The rendering results are as follows:
Asynchronous loading
The basic tree component above only supports basic data rendering. The external data needs to be prepared first, and then thrown to it to render the corresponding data.
Assuming that the data is kept on the server side, I hope label
it will request the server when it is clicked . During the waiting period, it will display loading...
the words displayed at the level where the Tree component is clicked , and wait until the data is completely returned before rendering the child. As shown in the following figure.
The Tree component itself cannot know where to request the data, so the logic for requesting the data belongs to custom content and needs to be written by the user. It is best to encapsulate this part of the logic into a function and pass it to the Tree component. When the Tree component label
is clicked , The Tree component detects that an asynchronous request is needed and will directly call the passed function. After the request is successful, it will add the data to its own tree_data
data source and let the page re-render. The steps are divided as follows.
- The externally defined data loading function is passed to the Tree component.
- When the Tree component is
label
clicked, the data loading function is triggered, and the status is updated toloading...
, waiting for the response result. - After the response data returns, update the entire
tree_data
trigger interface and re-render.
Externally defined data loading function
template
Two new attributes are added to the template lazy
and load
.
lazy
The specified data that is passed to the child component is rendered asynchronously, and load
the function corresponding to the attribute is the function loadNode
to obtain the data, which is passed to the Tree component for use.
<template>
<Tree
:data="data"
:load="loadNode"
:lazy="true"
/>
</template>
loadNode
The function we set when designing will return two parameter node
objects and a resolve
function. The node
object contains two properties layer
and children
.
layer
Clicking on label
the tab of the label is located at the level of several stages, and children
is one of the data shown in the following code for data
the first stage of the children
data, the user clicks on the first stage label
, the first stage children
data can pass node
objects Obtained.
resolve
The function execution will pass the final result to the Tree component. The following code can be described as when the user clicks on the first level label, data
the initial data defined in it is directly returned , and when the other level label is clicked, the asynchronous operation in the timer will be executed , resolve
Pass the packaged data to the Tree component for rendering.
外部调用文件
data(){
return {
label:"一级数据"
children:[{
label:"二级数据"
}]
}
},
methods: {
loadNode(node, resolve) {
const { layer, children } = node;
if (layer <= 1) {
resolve(children);
} else {
setTimeout(() => {
resolve([
{
title: `第${layer}层`,
},
]);
}, 1500);
}
},
}
Tree component handling loading function
In Tree.vue
the new document two properties loading
and loaded
is used to indicate the status of loading. When loading
is true, the template will render 加载中...
the words.
Once the received lazy
is true, the this.load
asynchronous data is obtained by executing the externally defined data loading function . this.load
Two parameters data
and resolve
functions are accepted .
Tree.vue
file
props: {
item: Object
},
data() {
return {
is_open: false, //是否打开下一级
loading: false, //是否加载中
loaded: false, //是否加载完毕
};
},
methods:{
toggle(){ //点击label时触发
if(this.lazy){ //异步请求数据
if (this.loading) {
//正在加载中
return false;
}
this.loading = true;
const resolve = (data) => {
this.is_open = !this.is_open;
this.loading = false;
this.loaded = true;
this.updateData({ data, layer: this.layer });
};
const data = { ...this.item, layer: this.layer.length };
this.load(data, resolve);//执行数据加载函数
}else{
...
}
}
}
const data = { ...this.item, layer: this.layer.length };
this.item
The data of the current level is stored. this.layer
The index array of the current level is stored, and its array length is the corresponding number of levels. The this.layer
detailed description is as follows.
Assume that the data source data is as follows. The user clicks on the 2-2级
label, and the this.layer
value is [0,1]
. Through this.layer
the index collection that can track the data at this level.
data = [{
label:"1级",
children:[{
label:"2-1级"
},{
label:"2-2级"
}}]
}]
this.load
The resolve
function will be passed in as a parameter, and the resolve
function will be executed once the asynchronous data is loaded . The loaded
status is updated to true
and loading
updated to false
. Then the this.updateData
function passed by the ancestor is executed , and the asynchronous return result is passed in data
. The this.updateData
execution will update the root level of the Tree component Data tree_data
, thereby re-rendering the component tree.
Update tree_data
updateData
The function gets the asynchronous response data data
and index array layer
passed by the child . These two parameters can be used to data
update the data source of the root node.
getTarget
The function of the function is to find the last level object corresponding to the array according to the index array. For example layer
, the value is [0,0]
, and result
the value is
[
{
label:"第一级",
children:[{
label:"第二级"
}}]
}
]
getTarget(layer,result)
The result of the execution will be returned label
to "第二级"
that object. Once the data of this object is manipulated, result
the data will change accordingly.
index.vue
file
methods:{
updateData(data) {
const { data: list, layer } = data;
let result = [...this.data_source];
const tmp = this.getTarget(layer, result);//根据索引数组和数据源找到那一级的数据
tmp.children = list;
this.tree_data = result;
}
}
getTarget
Find the data at that level through the function tmp
, children
update it to list
, and result
re-assign it to tree_data
. In this way, the asynchronously requested data is added to the data source.
Node drag
The drag and drop of nodes can be easily implemented with the drag and drop API in HTML5. When you add draggable="true"
it to the dom element , it means that the element is allowed to be dragged.
The HTML5 drag and drop API also includes several event listener functions, such as dragstart
, drop
, dragover
and so on.
dragstart
The event will be triggered when the mouse is held down on a dom element and it is about to be dragged. Its event objecte
supports callinge.dataTransfer.setData
functions to set parameter values. It is an event bound to the held dom element.dragover
It is a function that is triggered during the dragging process after the mouse presses down a certain dom element.drop
The event will be triggered when the mouse drags a dom element to another dom element and releases it. It is a listener event bound to another dom element, and its event objecte
cane.dataTransfer.getData
getdragstart
the parameter value set internally through a function .
All nodes of the Tree component are all bound dragstart
and drop
events, once a node 1 is moved to another node 2, dragstart
all the data information of node 1 can be captured through functions and e.dataTransfer.setData
stored.
Node 2 listens to the release of node 1 above it, and the drop
event will be triggered. Inside the drop
event, it can get the data information of the current node (that is, node 2), and it can also e.dataTransfer.getData
get the data information of node 1.
If the data information of node 1 and node 2 is obtained at the same time, it is equivalent to knowing clearly that tree_data
a data object needs to be moved under another data object on the root data source . In the end, the problem of moving the dom node is transformed into Operational tree_data
issues.
Bind drag event
First draggable="true"
, set attributes for each dom node on the template , so that all nodes support drag and drop. At the same time, bind three event functions dragstart
, drop
and dragover
.
Tree.vue
<template>
...
<div
class="box"
@click="toggle()"
@dragstart="startDrag"
@drop="dragEnd"
@dragover="dragOver"
draggable="true"
>
...
</template>
startDrag
Event storage array index this.layer
, due e.dataTransfer.setData
not support storing reference data type, so use JSON.stringify
converted it.
dragOver
It must be called in the event e.preventDefault()
, otherwise the dragEnd
function will not be triggered.
dragEnd
The function gets the data of the two nodes and starts to call the method dragData
update tree_data
of the ancestor.The ancestor method here dragData
is provide,inject
passed to the descendants through the mechanism, which can be seen in the entire code at the end.
methods: {
dragOver(e) {
e.preventDefault();
},
startDrag(e) {
e.dataTransfer.setData("data", JSON.stringify(this.layer));
},
dragEnd(e) {
e.preventDefault();
const old_layer = JSON.parse(e.dataTransfer.getData("data"));
this.dragData(old_layer, this.layer, this);
}
}
Update Tree_data
dragData
The execution process is to add the data object of the dragged node to the children
array of data objects of the new node .
By this.getTarget
finding the data objects of the two nodes, running new_obj.children.unshift(old_obj);
, the old data objects are added to the children
array of new objects . In addition, the old data objects in the original position must be deleted, otherwise there will be two copies of the old data objects.
If you want to delete the old data object in the original position, you must find its parent data object and its index value under the parent's children array. After finding it, you can use splice
the old data object in the original position to delete it. Finally. Assign the modified data to tree_data
.
index.vue
file
methods: {
dragData(old_layer, new_layer, elem) {
let result = [...this.data_source];
const old_obj = this.getTarget(old_layer, result);
const new_obj = this.getTarget(new_layer, result);
//找到被拖拽数据对象的父级数据对象
const old_obj_parent = this.getTarget(
old_layer.slice(0, old_layer.length - 1),
result
);
const index = old_layer[old_layer.length - 1]; //获取倒数第二个索引
if (!new_obj.children) {
new_obj.children = [];
}
if (Array.isArray(old_obj_parent)) {
old_obj_parent.splice(index, 1);
} else {
old_obj_parent.children.splice(index, 1); //删掉原来位置的被拖拽数据
}
new_obj.children.unshift(old_obj); //将被拖拽的数据加到目标处
this.tree_data = result;
}
...
}
Complete code
index.vue
<template>
<div class="tree">
<template v-for="(item, index) in data_source">
<Tree :item="item" :key="index" :layer="[index]" class="first-layer" />
</template>
</div>
</template>
<script>
import Tree from "./Tree";
export default {
props: {
data: Object | Array,
label: {
type: String,
default: "label",
},
children: {
type: String,
default: "children",
},
lazy: {
type: Boolean,
default: false,
},
load: {
type: Function,
default: () => {},
},
},
provide() {
return {
label: this.label,
children: this.children,
lazy: this.lazy,
load: this.load,
updateData: this.updateData,
dragData: this.dragData,
};
},
data() {
return {
tree_data: this.data,
};
},
computed: {
data_source() {
if (Array.isArray(this.tree_data)) {
return this.tree_data;
} else {
return [this.tree_data];
}
},
},
components: {
Tree,
},
methods: {
dragData(old_layer, new_layer, elem) {
//数据拖拽
const flag = old_layer.every((item, index) => {
return item === new_layer[index];
});
if (flag) {
//不能将元素拖拽给自己的子元素
return false;
}
let result = [...this.data_source];
const old_obj = this.getTarget(old_layer, result);
const new_obj = this.getTarget(new_layer, result);
const old_obj_parent = this.getTarget(
old_layer.slice(0, old_layer.length - 1),
result
);
const index = old_layer[old_layer.length - 1]; //获取倒数第二个索引
if (!new_obj[this.children]) {
new_obj[this.children] = [];
}
if (Array.isArray(old_obj_parent)) {
old_obj_parent.splice(index, 1);
} else {
old_obj_parent[this.children].splice(index, 1); //原来位置的被拖拽数据删掉x
}
new_obj[this.children].unshift(old_obj); //将被拖拽的数据加到目标处
this.tree_data = Array.isArray(this.tree_data) ? result : result[0];
this.$nextTick(() => {
!elem.is_open && elem.toggle(); //如果是关闭状态拖拽过去打开
});
},
getTarget(layer, result) {
if (layer.length == 0) {
return result;
}
let data_obj;
Array.from(Array(layer.length)).reduce((cur, prev, index) => {
if (!cur) return null;
if (index == 0) {
data_obj = cur[layer[index]];
} else {
data_obj = cur[this.children][layer[index]];
}
return data_obj;
}, result);
return data_obj;
},
updateData(data) {
const { data: list, layer } = data;
let result = [...this.data_source];
const tmp = this.getTarget(layer, result);
tmp[this.children] = list;
this.tree_data = Array.isArray(this.tree_data) ? result : result[0];
},
},
};
</script>
<style lang="scss" scoped>
.first-layer {
margin-bottom: 20px;
}
</style>
Tree.vue
<template>
<div class="tree">
<div
class="box"
@click="toggle()"
@dragstart="startDrag"
@drop="dragEnd"
@dragover="dragOver"
draggable="true"
>
<div :class="['lt', getClassName()]"></div>
<div class="label lt">{
{ item[label] }}</div>
<div class="lt load" v-if="loading_status">loading...</div>
</div>
<div class="drop-list" v-show="show_next">
<template v-for="(child, index) in item[children]">
<Tree :item="child" :key="index" :layer="[...layer, index]" />
</template>
</div>
</div>
</template>
<script>
export default {
name: "Tree",
props: {
item: Object,
layer: Array,
},
inject: ["label", "children", "lazy", "load", "updateData", "dragData"],
data() {
return {
is_open: false, //是否打开下一级
loading: false, //是否加载中
loaded: false, //是否加载完毕
};
},
computed: {
show_next() {
//是否显示下一级
if (
this.is_open === true &&
(this.loaded === true || this.lazy === false)
) {
return true;
} else {
return false;
}
},
loading_status() {
//控制loading...显示图标
if (!this.lazy) {
return false;
} else {
if (this.loading === true) {
return true;
} else {
return false;
}
}
},
},
methods: {
getClassName() {
if (this.item[this.children] && this.item[this.children].length > 0) {
return this.is_open ? "down" : "right";
} else {
return "gap";
}
},
dragOver(e) {
e.preventDefault();
},
startDrag(e) {
e.dataTransfer.setData("data", JSON.stringify(this.layer));
},
dragEnd(e) {
e.preventDefault();
const old_layer = JSON.parse(e.dataTransfer.getData("data"));
this.dragData(old_layer, this.layer, this);
},
toggle() {
if (this.lazy) {
if (this.loaded) {
//已经加载完毕
this.is_open = !this.is_open;
return false;
}
if (this.loading) {
//正在加载中
return false;
}
this.loading = true;
const resolve = (data) => {
this.is_open = !this.is_open;
this.loading = false;
this.loaded = true;
this.updateData({ data, layer: this.layer });
};
const data = { ...this.item, layer: this.layer.length };
this.load(data, resolve);
} else {
this.is_open = !this.is_open;
}
},
},
};
</script>
<style lang="scss" scoped>
.lt {
float: left;
}
.load {
font-size: 12px;
margin-left: 5px;
margin-top: 4px;
}
.gap {
margin-left: 10px;
width: 1px;
height: 1px;
}
.box::before {
width: 0;
height: 0;
content: "";
display: block;
clear: both;
cursor: pointer;
}
@mixin triangle() {
border-color: #57af1a #fff #fff #fff;
border-style: solid;
border-width: 4px 4px 0 4px;
height: 0;
width: 0;
}
.label {
font-size: 14px;
margin-left: 5px;
}
.down {
@include triangle();
margin-top: 8px;
}
.right {
@include triangle();
transform: rotate(-90deg);
margin-top: 8px;
}
.drop-list {
margin-left: 10px;
}
</style>
Externally call the Tree component (test file)
<template>
<Tree
:data="data"
label="title"
children="childrens"
:load="loadNode"
:lazy="true"
/>
</template>
<script>
import Tree from "../../components/Tree/index.vue";
export default {
data() {
return {
data: [
{
title: "一级",
childrens: [
{
title: "二级1",
childrens: [
{
title: "三级1",
},
],
},
{
title: "二级2",
childrens: [
{
title: "三级2",
},
],
},
],
},
{
title: "一级2",
childrens: [
{
title: "二级2",
},
],
},
],
};
},
components: {
Tree,
},
methods: {
loadNode(node, resolve) {
const { layer, childrens } = node;
if (childrens && childrens.length > 0) {
resolve(childrens);
} else {
setTimeout(() => {
resolve([
{
title: `第${layer}层`,
},
]);
}, 1500);
}
},
},
};
</script>
<style>
</style>