vue Recursive Component Simple Tree Control

Posted by dacio on Tue, 27 Aug 2019 10:23:04 +0200

1. Recursive Components - Preview of Simple Tree Controls and Problems

Problems encountered in writing tree components:

  • How can components be called recursively?
  • How do recursive component click events pass?

2. Basic Structure and Style of Tree Controls

<template>
  <ul class="vue-tree">
    <li class="tree-item">
      <div class="tree-content"><!--Node content-->
        <div class="expand-arrow"></div><!--Expand or contract node buttons-->
        <div class="tree-label">Primary school</div><!--Node text content-->
      </div>
      <ul class="sub-tree"><!--Subnode-->
        <li class="tree-item expand">
          <div class="tree-content">
            <div class="expand-arrow"></div>
            <div class="tree-label">Chinese</div>
          </div>
        </li>
        <li class="tree-item">
          <div class="tree-content">
            <div class="expand-arrow"></div>
            <div class="tree-label">Mathematics</div>
          </div>
        </li>
      </ul>
    </li>
  </ul>
</template>

<style lang="stylus">
.vue-tree{
  list-style: none;
  padding: 0;
  margin: 0;
  .tree-item{
    cursor: pointer;
    transition: background-color .2s;
    .tree-content{
      position: relative;
      padding-left: 28px;
      &:hover{
        background-color: #f0f7ff;
      }
    }
    .expand-arrow{
      position: absolute;
      top: 0;
      left: 0;
      width: 28px;
      height: 28px;
      cursor: pointer;
      &::after{
        position: absolute;
        top: 50%;
        left: 50%;
        display: block;
        content: ' ';
        border-width: 5px;
        border-style: solid;
        border-color: transparent;
        border-left-color: #ccc;
        margin: -5px 0 0 -2.5px;
        transition: all .2s;
      }
    }
    &.expand{
      &>.tree-content{
        background-color: #f0f7ff;
        &>.expand-arrow{
          &::after{
            transform: rotate(90deg);
            margin: -2.5px 0 0 -5px;
          }
        }
      }
    }
    .tree-label{
      height: 28px;
      line-height: 28px;
      font-size: 14px;
    }
    .sub-tree{
      display: none;
      list-style: none;
      padding: 0 0 0 28px;
      margin: 0;
    }
    &.expand>.sub-tree{
      display: block;
    }
    &.no-child{
      &>.tree-content{
        &>.expand-arrow{
          display: none;
        }
      }
    }
  }
}
</style>

3. Component catalogue and data structure

directory structure
vue-tree

  • VueTree.vue tree control parent component
  • TreeItem.vue Tree Control Recursive Component

Tree Control Data Structure

let treeData = [
  {
    text: "Class A", // Displayed text
    expand: false, // Whether to expand by default
    children: [ // Subnode
      {
        text: "Class A-1",
        expand: false,
      },
      {
        text: "Class A-2",
        expand: false,
        children: [
          {
            text: "Class A-2-1",
            expand: false,
          },
          {
            text: "Class A-2-2",
            expand: false,
          }
        ]
      }
    ]
  }
];

3.1, TreeItem.vue code

<template>
  <li class="tree-item" :class="{expand: isExpand, 'no-child': !treeItemData.children || treeItemData.children.length  === 0}">
    <div class="tree-content" @click="_clickEvent">
      <div class="expand-arrow" @click.stop="expandTree()"></div>
      <div class="tree-label">{{treeItemData.text}}</div>
    </div>
    <ul class="sub-tree" v-if="treeItemData.children && treeItemData.children.length > 0">
      <!--TreeItem Calls in components TreeItem assembly-->
      <TreeItem
        v-for="item in treeItemData.children"
        :tree-item-data="item"
        :key="uuid()"
        :tree-click-event="treeClickEvent"></TreeItem>
    </ul>
  </li>
</template>

<script>
  export default {
    name: "TreeItem",
    props: {
      treeItemData: {
        type: Object,
        default(){
          return {};
        }
      },
      // Node Click Event
      treeClickEvent: {
        type: Function,
        default() {
          return function () {};
        }
      }
    },
    data(){
      return {
        // Whether the node is expanded or not
        isExpand: this.treeItemData.expand || false
      }
    },
    methods: {
      // Expansion/contraction
      expandTree(flag){
        if(!this.treeItemData.children || this.treeItemData.children.length === 0){
          return;
        }
        if(typeof flag === 'undefined'){
          flag = !this.isExpand;
        }else{

          flag = !!flag;
        }
        this.isExpand = flag;
      },
      // Create a unique id
      uuid(){
        let str = Math.random().toString(32);
        str = str.substr(2);
        return str;
      },
      // Node Click Event
      _clickEvent(){
        // If an event function is passed, the event function is called and the current node data and components are passed.
        if(this.treeClickEvent && typeof this.treeClickEvent === 'function'){
          this.treeClickEvent(this.treeItemData, this);
        }
      }
    }
  }
</script>

3.1.1. How can a solution component be called recursively? problem

The call itself in the component template must clearly define the name attribute of the component, and the name attribute is the name attribute when the component is called recursively. If the name of the component in the TreeItem.vue component is'TreeItem', then the component name must be <TreeItem> when invoked in the template.
Of course, you can also register components globally, and you can see official vue documents specifically. Recursive components

3.1.2. How to transfer the click events of recursive components? problem

My solution here is to use props to pass in the event function, call the event function when clicking on the node, and pass in the corresponding data.
Previous attempts have been made to use the form of $emit and pass the data over. Because it is a recursive component, the data passed by the outermost layer will change. For example, the data passed by the third layer node will become the data of the first layer node at the end of execution.

4. VueTree.vue Component

<template>
  <ul class="vue-tree">
    <TreeItem
        v-for="(item, index) in treeData"
        :key="index"
        :treeItemData="item"
        :tree-click-event="treeClickEvent"></TreeItem>
  </ul>
</template>

<script>
  import TreeItem from "./TreeItem";
  export default {
    name: "VueTreeMenu",
    components: {
      TreeItem
    },
    props: {
     // Tree control data
      treeData: {
        type: Array,
        default(){
          return [];
        }
      },
      // Node Click Event
      treeClickEvent: {
        type: Function,
        default() {
          return function () {};
        }
      }
    }
  }
</script>

<style lang="stylus">
.vue-tree{
  list-style: none;
  padding: 0;
  margin: 0;
  .tree-item{
    cursor: pointer;
    transition: background-color .2s;
    .tree-content{
      position: relative;
      padding-left: 28px;
      &:hover{
        background-color: #f0f7ff;
      }
    }
    .expand-arrow{
      position: absolute;
      top: 0;
      left: 0;
      width: 28px;
      height: 28px;
      cursor: pointer;
      &::after{
        position: absolute;
        top: 50%;
        left: 50%;
        display: block;
        content: ' ';
        border-width: 5px;
        border-style: solid;
        border-color: transparent;
        border-left-color: #ccc;
        margin: -5px 0 0 -2.5px;
        transition: all .2s;
      }
    }
    &.expand{
      &>.tree-content{
        background-color: #f0f7ff;
        &>.expand-arrow{
          &::after{
            transform: rotate(90deg);
            margin: -2.5px 0 0 -5px;
          }
        }
      }
    }
    .tree-label{
      height: 28px;
      line-height: 28px;
      font-size: 14px;
    }
    .sub-tree{
      display: none;
      list-style: none;
      padding: 0 0 0 28px;
      margin: 0;
    }
    &.expand>.sub-tree{
      display: block;
    }
    &.no-child{
      &>.tree-content{
        /*padding-left: 0;*/
        &>.expand-arrow{
          display: none;
        }
      }
    }
  }
}
</style>

5. Using Tree Components

<template>
  <div class="app" id="app">
    <VueTree :tree-data="treeData2" :tree-click-event="treeClickEvent"></VueTree>
  </div>
</template>

<script>
import VueTree from "./components/vue-tree/VueTree";

export default {
  name: 'app',
  data(){
    return {
      treeData2: [
        {
          text: "Class A", // Displayed text
          expand: false, // Whether to expand by default
          children: [
            {
              text: "second level-1",
              expand: false,
            },
            {
              text: "second level-2",
              expand: false,
              children: [
                {
                  text: "Level three-1",
                  expand: false,
                },
                {
                  text: "Level three-2",
                  expand: false,
                  children: [
                    {
                      text: "Level Four-1",
                      expand: false,
                    }
                  ]
                }
              ]
            }
          ]
        },
        {
          text: "Class A-2",
          expand: false
        }
      ]
    }
  },
  methods: {
    treeClickEvent(item, treeItem){
      console.log(item);
    }
  },
  components: {
    VueTree
  }
}
</script>

Topics: Javascript Vue Attribute