Vue implements a Tree component

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.vueand the other is Tree.vue.

index.vue

index.vueThe 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.vueonly 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 datamakes 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_datawithout 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_sourceadded to convert all data sources into arrays, and then traverse the rendering Treecomponents;

Tree.vue

Tree.vueThe file contains the specific code for rendering the tree structure data. Its template content is divided into two parts, one is to render the labelcorresponding title content, and the other is to render the child.

Set a state in the component is_opento control whether its next level is open or closed.

getClassNameThe is_opencorresponding class name can be rendered to show whether the triangle icon is displayed downward or to the right.

In Tree.vuesetting 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 Treecomponents, 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 labelit 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 labelis 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_datadata 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 labelclicked, the data loading function is triggered, and the status is updated to loading..., waiting for the response result.
  • After the response data returns, update the entire tree_datatrigger interface and re-render.

Externally defined data loading function

templateTwo new attributes are added to the template lazyand load.

lazyThe specified data that is passed to the child component is rendered asynchronously, and loadthe function corresponding to the attribute is the function loadNodeto obtain the data, which is passed to the Tree component for use.

<template>
  <Tree
    :data="data"
    :load="loadNode"
    :lazy="true"
  />
</template>

loadNodeThe function we set when designing will return two parameter nodeobjects and a resolvefunction. The nodeobject contains two properties layerand children.

layerClicking on labelthe tab of the label is located at the level of several stages, and childrenis one of the data shown in the following code for datathe first stage of the childrendata, the user clicks on the first stage label, the first stage childrendata can pass nodeobjects Obtained.

resolveThe 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, datathe 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 , resolvePass 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.vuethe new document two properties loadingand loadedis used to indicate the status of loading. When loadingis true, the template will render 加载中...the words.

Once the received lazyis true, the this.loadasynchronous data is obtained by executing the externally defined data loading function . this.loadTwo parameters dataand resolvefunctions are accepted .

Tree.vuefile

 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.itemThe data of the current level is stored. this.layerThe index array of the current level is stored, and its array length is the corresponding number of levels. The this.layerdetailed description is as follows.

Assume that the data source data is as follows. The user clicks on the 2-2级label, and the this.layervalue is [0,1]. Through this.layerthe index collection that can track the data at this level.

  data = [{
    label:"1级",
    children:[{
       label:"2-1级"
    },{
       label:"2-2级"
    }}]
  }]

this.loadThe resolvefunction will be passed in as a parameter, and the resolvefunction will be executed once the asynchronous data is loaded . The loadedstatus is updated to trueand loadingupdated to false. Then the this.updateDatafunction passed by the ancestor is executed , and the asynchronous return result is passed in data. The this.updateDataexecution will update the root level of the Tree component Data tree_data, thereby re-rendering the component tree.

Update tree_data

updateDataThe function gets the asynchronous response data dataand index array layerpassed by the child . These two parameters can be used to dataupdate the data source of the root node.

getTargetThe 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 resultthe value is

[
  {
    label:"第一级",
    children:[{
      label:"第二级"
    }}]
  }
]

getTarget(layer,result)The result of the execution will be returned labelto "第二级"that object. Once the data of this object is manipulated, resultthe data will change accordingly.

index.vuefile

 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;
    }
 }

getTargetFind the data at that level through the function tmp, childrenupdate it to list, and resultre-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, dragoverand so on.

  • dragstartThe event will be triggered when the mouse is held down on a dom element and it is about to be dragged. Its event object esupports calling e.dataTransfer.setDatafunctions to set parameter values. It is an event bound to the held dom element.
  • dragoverIt is a function that is triggered during the dragging process after the mouse presses down a certain dom element.
  • dropThe 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 object ecan e.dataTransfer.getDataget dragstartthe parameter value set internally through a function .

All nodes of the Tree component are all bound dragstartand dropevents, once a node 1 is moved to another node 2, dragstartall the data information of node 1 can be captured through functions and e.dataTransfer.setDatastored.

Node 2 listens to the release of node 1 above it, and the dropevent will be triggered. Inside the dropevent, it can get the data information of the current node (that is, node 2), and it can also e.dataTransfer.getDataget 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_dataa 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_dataissues.

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, dropand dragover.

Tree.vue

<template>
...
<div
      class="box"
      @click="toggle()"
      @dragstart="startDrag"
      @drop="dragEnd"
      @dragover="dragOver"
      draggable="true"
>
...
</template>

startDragEvent storage array index this.layer, due e.dataTransfer.setDatanot support storing reference data type, so use JSON.stringifyconverted it.

dragOverIt must be called in the event e.preventDefault(), otherwise the dragEndfunction will not be triggered.

dragEndThe function gets the data of the two nodes and starts to call the method dragDataupdate tree_dataof the ancestor.The ancestor method here dragDatais provide,injectpassed 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

dragDataThe execution process is to add the data object of the dragged node to the childrenarray of data objects of the new node .

By this.getTargetfinding the data objects of the two nodes, running new_obj.children.unshift(old_obj);, the old data objects are added to the childrenarray 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 splicethe old data object in the original position to delete it. Finally. Assign the modified data to tree_data.

index.vuefile

 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>

Guess you like

Origin blog.csdn.net/brokenkay/article/details/111771038