Let’s briefly list some of the nodes of interest in implementing tree components.

How does ~ call the component itself

Since the tree is a recursive data structure, recursive calls to the component itself are necessary. We simply give the component the name property, which can be used directly within the component. Note here that each call generates a separate scope.

<! -- html --> <template> <div> ... <my-tree></my-tree> </div> </template> <! -- js -->export default {
  name: 'myTree'. }Copy the code
How ~ props and event listeners are passed to child components (vue2.4.0+ is required for the following properties)

When we use a component, we may specify some properties to achieve differentiated customization of the component, which requires us to implement property inheritance from the parent component.

<my-tree v-bind="{...$props.$attrs}" v-on="$listeners"
  :data="item[props.children]"
  :child-node="true">
</my-tree>
Copy the code

$props represents a set of properties that the user specified when calling and that the component props has declared to receive. Properties that are not declared to receive are categorized under $attrs. However, we do not want to inherit data from the parent component, so we need to specify data to override the corresponding properties in the parent component. In addition, when we want to trigger a child component’s listening event, since the child component’s caller is its parent, we want to notify the caller of the outer tree component. $Listeners contain v-on event listeners in the parent scope (without the.native modifier), so that events are directly triggered by callers of the outer tree component, regardless of the component’s hierarchy. We can specify a child-node property in the child component and use props to receive it, which makes it very easy to tell if it is the top-level scope.

~ component slot

Allowing users the flexibility to customize content is essential. We cannot accommodate all usage scenarios, so slot usage is part of the component. When we customize a slot for a component, we need to transfer the data of the current component so that the user can display the content of the data. Slot: named slot and unnamed slot (default slot)

Subcomponent -- unnamed slot (the following are equivalent) <slot v-bind="{... item}">{{item.label}}</slot>
<slot v-bind:default="{... item}">{{item.label}}</slot> subcomponent -- named slot (specified name :emptyText) <slot V-bind :emptyText="keywords"> No data </slot>Copy the code

The parent component defines a custom slot

<my-tree> <! -- scope[slotName] is the value of v-bind --> <template v-slot="scope">
    <div>{{scope.default.xxx}}</div>
  </template>
  <template v-slot:emptyText="scope"> <span>{{scope. EmptyText}} </span> </template> </my-tree>Copy the code

Because a component calls itself recursively, slots need to be passed to child components from within. The following uses the default slot as an example:

<my-tree v-bind="{...$props.$attrs}" v-on="$listeners"
  :data="item[props.children]"
  :child-node="true">
  <template v-slot="scope">
    <slot v-bind:default="scope.default"></slot>
  </template>
</my-tree>
Copy the code
~ Component global variable

As mentioned earlier, each invocation of a component generates a separate scope, but in many cases we need a global variable to record the state of the component, such as the currently highlighted node, the keys of the expanded node, and so on. At this point, we need to modify variables to have global effects regardless of the scope of the component. It is easy to think of the Object. So we can have a global variable by defining an object in the top-level component and passing it down to all the child components.

<! -- subcomponent --> <my-tree v-bind="{...$props.$attrs}" v-on="$listeners"
  :data="item[props.children]"
  :child-node="true"
  :treeGlobal="treeGlobal">
  ...
</my-tree>
<!-- js -->
export default {
  name: 'myTree'.data() {
    let globalTemp = {
      currentKey: ' ', // Currently selected key selectedItems: [], // Selected node (optional) openedItems: [], // Expanded node... } // treeGlobal has no props to accept, in$sttrsif(this.$attrs.treeGlobal) {
      globalTemp = this.$attrs.treeGlobal;
    }
    return{ treeGlobal: globalTemp, treeData: [], ... }},... },Copy the code

This gives us a global variable, treeGlobal, and changes to the treeGlobal made by each child component take effect globally. Note that the easiest way to record whether a node is expanded or selected is to add the corresponding attribute to the node and then modify the state of the attribute according to the operation. However, this attribute is appended to its children after the treeData is declared, and vue does not listen for changes in its value, in other words, its changes do not refresh the view ($set does not work either). It is possible to force a view refresh with $forceUpdate(), but $forceUpdate() is not officially recommended, especially since each component that needs to change needs to trigger $forceUpdate() itself ($forceUpdate() only refreshes the contents of the current component). Above, we can declare an object in the global variable to record these states, and the monitoring can be returned to the Vue itself, we just need to pay attention to the data changes.

~ Part of the code display

If the code is too long, I don’t want to post it. I want to post some main parts to help understand it.

<! 标 准 标 准 标 准 标 准 标 准 标 准 标 准 标 准 标 准 标 准 标 准 标 准 标 准 标 准 标 准 标 准
<template>
  <div :class="['my-tree-box', childNode ? '' : className]" ref="treeOwnSelf">
    <div v-for="item in treeData" :key="item[nodeKey]">
      <div :class="['list-cell-box', 'list-cell-leaf', {'current-cell-style': funCurrentItem(item)}]"
            v-if="item.isLeaf"
            @click="nodeClick(item, 1)"
            :style="{paddingLeft: `${indent * item.treeLevel + 10}px`, ... cellHeightStyle}">
        <div class="list-label-box">
          <div :class="['list-label', {'text-ellipsis': textEllipsis}, {'list-label-checkbox': showCheckbox}]">
            <slot v-bind:default="simplifyItem(item)">{{item[props.label]}}</slot>
          </div>
          <span v-if="showCheckbox"
                :class="['select-checkbox', {'active': nodeSelect[item[nodeKey]]}]"
                @click.stop="checkboxClick(item)"></span>
        </div>
      </div>
      <template v-if=! "" item.isLeaf">
        <div :class="['list-cell-box', {'current-cell-style': funCurrentItem(item)}]"
              @click="nodeClick(item, 1)"
              :style="{paddingLeft: `${indent * item.treeLevel + 10}px`, ... cellHeightStyle}">
          <div class="list-label-box">
            <span :class="['arrow-box', item.isExpand ? 'arrow-bottom' : 'arrow-top']"
                  @click.stop="nodeClick(item, 0)"></span>
            <div :class="['list-label', {'text-ellipsis': textEllipsis}, {'list-label-checkbox': showCheckbox}]">
              <slot v-bind:default="simplifyItem(item)">{{item[props.label]}}</slot>
            </div>
            <span v-if="showCheckbox"
                  :class="['select-checkbox', {'active': nodeSelect[item[nodeKey]] === 2}, {'half': nodeSelect[item[nodeKey]] === 1}]"
                  @click.stop="checkboxClick(item)"></span>
          </div>
        </div>
        <div class="list-childs-box" v-if="isExpand(item)">
          <my-tree v-bind="{... $props, ... $attrs}" v-on="$listeners"
                    :data="item[props.children]"
                    :child-node="true"
                    :treeGlobal="treeGlobal">
            <template v-slot="scope">
              <slot v-bind:default="scope.default"></slot>
            </template>
          </my-tree>
        </div>
      </template>
    </div>
  </div>
</template>

<! -- props -->props: { data: { type: Array, default() {return []} }, props: { type: Object, default() { return { label: 'label', children: 'children'}}}, childNode: {type: Boolean, // Whether the inner loop calls the child if it does not repeat the formatting data default: False}, indent: {type: Number, // indent default: 20}, cellHeight: {type: Number, // single line height default: 34}, nodeKey: {type: String, // nodeKey default: ""}, currentNodeKey: {type: [String, Number], }, highlightCurrent: {type: String, // highlight default: 'leaf' // 'none': none highlight leaf: only leaf nodes (default) all: all nodes}, className: {type: String, // top-level style default: "}, showCheckbox: {type: Boolean, // display checkboxes default: false},},Copy the code

Attach an effect drawing