Element-ui Tree

Element-ui Tree

用清晰的层级结构展示信息,可展开或折叠。

Tree Attributes

参数 说明 类型 可选值 默认值
data 展示的数据 Array
node-key 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的 String
show-checkbox 节点是否可被选择 boolean false
default-expand-all 是否默认展开所有节点 boolean fasle

基础的树形结构展示

<template>
    <el-tree 
        :data="data" 
        :props="defaultProps" >
    </el-tree>
</template>
<script>
  export default {
    data() {
      return {
        data: [{
          label: '一级 1',
          children: [{
            label: '二级 1-1',
            children: [{
              label: '三级 1-1-1'
            }]
          }]
        }, {
          label: '一级 2',
        }],
        defaultProps: {
          children: 'children',
          label: 'label'
        }
      };
    },
  };
</script>
 

Tree 结构

实现思路

  • tree.vue 渲染树
  • tree-node.vue 渲染子树
  • tree-store.js 树的状态管理器
  • node.js 定义树节点的树形和方法

tree.vue

tree.vue 为树组件的入口

  • 接收 props 传递的数据 data
  • 根据 data 生成树节点 root
  • 根据 root 渲染树
接收 props 传递的数据 data
props: {
    // 树要展示的数据
    data: {
        type: Array,
    },
    // 树节点的key
    nodeKey: String,
    // 配置项
    props: {
        type: Object,
        default: function() {
            return {
                // 指定子树为节点某个对象的值即"children"对应值表示子节点的数据
                children: 'children',
                // 指定节点标签为节点对象的某个属性的值
                label: 'label',
            }
        }
    },
}
 
根据 data 生成树节点 root
created() {
    // 给子树判断父组件是否为树
    this.isTree = true;

    // 创建树的store
    this.store = new TreeStore({
        key: this.nodeKey,
        data: this.data,
        props: this.props,
    });

    // 从树根开始
    this.root = this.store.root;
},
 

根据 root 渲染树

<template>
    <div
        class="y-tree">
        <!-- 子树如何渲染?循环生成?多层嵌套? -->
        <y-tree-node
            v-for="(child) in root.childNodes"
            :node="child"
            :show-checkbox="showCheckbox"
            :key="getNodeKey(child)"
        >
        </y-tree-node>
    </div>
</template>
 

tree-node.vue

tree-node.vue 渲染子树

如何渲染子树的子树…循环嵌套渲染子树?我原来的想法是循环嵌套就是顺下去,往下走;实际上,循环嵌套式一个环,往下走完还要往回走

  • 渲染该节点展示的内容
  • 渲染该节点的子树
渲染子树
<template>
<div
class="y-tree-node"
:aria-expanded="expanded"
@click.stop="handleClick"
>
<!-- 1.渲染树节点
节点内容的展示,el-tree节点主要分四部分(展开图标展示,多选框,加载中图标,节点内容展示)
动态计算偏移量:(node.level - 1) * treeC.indent + 'px', 形成阶梯式子树效果
-->
<div
class="y-tree-node__content"
:style="{'padding-left': (node.level - 1) * treeC.indent + 'px'}"
>
<!-- 多选框 
@click.native.stop 阻止事件冒泡
事件修饰符-官方文档:https://cn.vuejs.org/v2/guide/events.html
-->
<y-checkbox
v-if="showCheckbox"
v-model="node.checked"
@click.native.stop
>
</y-checkbox>
<!-- 内容 -->
<node-content :node="node"></node-content>
</div>
<!-- 2.渲染该节点的子树
子树如何渲染?
组件YCollapseTransition是一个函数式组件
官方文档:https://cn.vuejs.org/v2/guide/render-function.html
这里为什么要用函数式组件(无状态、无实例)来包装树节点组件从而实现子树的渲染?为什么不直接使用? -- 子树的展开和收缩效果
-->
<y-collapse-transition>
<!-- v-if="node.expanded && node.childNodes.length"
v-if: 动态的控制DOM元素的添加和删除
v-show: 同css的display来控制元素的显示和隐藏
-->
<div
v-if="childNodeRendered"
v-show="expanded"
class="y-tree-node__children"
:aria-expanded="expanded"
>
<y-tree-node
v-for="(child) in node.childNodes"
:key="getNodeKey(child)"
:node="child"
:show-checkbox="showCheckbox"
>
</y-tree-node>
</div>
</y-collapse-transition>
</div>
</template>

tree-store.js

tree-store 树的状态管理器,生成树节点集

import Node from './node';
export default class TreeStore {
constructor(options) {
// 赋值初始化:options是对象,遍历使用for...in...
for(let option in options) {
if(options.hasOwnProperty(option)) {
this[option] = options[option];
}
}
/**
* 实例化根节点Node
* 根节点实例化
* 由根节点开始生成树
* 根节点->根节点的childNodes->...
*/
this.root = new Node({
data: this.data,
store: this,
});
}
} 

node.js

node.js 树节点的属性和方法, 每个节点都有的,保证节点的独立性

import objectAssign from '../../../../src/utils/merge';
import {
markNodeData,
} from './utils';
/**
* getPropertyFromData(this, 'children')
* node.store为tree-store中的this
* node.store.children: 函数 | 字符串 | undefined
* 从node.data中获取prop对应的值
* 
* store.props 是树的配置项
* 
* @param {*} node 
* @param {*} prop 
*/
const getPropertyFromData = function(node, prop) {
const props = node.store.props;
const data = node.data || {};
const config = props && props[prop];
// console.log('888', props, config, data[config]);
if(typeof config === 'function') {
return config(data, node);
} else if (typeof config === 'string') {
return data[config];
} else if (typeof config === 'undefined') {
const dataProp = data[prop];
// console.log('children', dataProp)
return dataProp === undefined ? '' : dataProp;
}
}
// 树节点的id
let nodeIdSeed = 0;
export default class Node {
constructor(options) {
this.id = nodeIdSeed++;
// 节点data
this.data = null;
// 是否选中,默认false:取消选中(true:选中)
this.checked = false;
// 半选中,默认false
this.indeterminate = false;
// 父亲节点
this.parent = null;
// 是否展开
this.expanded = false;
// 是否是当前节点
this.isCurrent = false;
// 赋初值
for(let option in options) {
if(options.hasOwnProperty(option)) {
this[option] = options[option];
}
}
// internal
// 该节点的层级,默认为0
this.level = 0;
// 该节点的子节点
this.childNodes = [];
// 计算层级 根节点层级为0
if(this.parent) {
this.level = this.parent.level + 1;
}
const store = this.store;
if(!store) {
throw new Error('[Node]store is required!');
}
// 构建子树
this.setData(this.data);
// 设置节点的展开属性
// console.log('store', this.store.defaultExpandAll);
if(store.defaultExpandAll) {
this.expanded = true;
}
// 节点注册,为什么会在tree-store中呢?为什么要注册?
// store.registerNode(this);
// console.log('Node', this, options);
}
/**
* 通过 node.label 调用(即执行get方法)
*/
get label() {
return getPropertyFromData(this, 'label');
}
/**
* A instanceof B:A是否是B的实例
* 设置该节点的data和childNodes
* 根节点下的data是一个数组,其子节点便是根据此生成的
* @param {*} data 
*/
setData(data) {
// console.log('setData', Array.isArray(data), data instanceof Array);
// 如果data不是数组即非根节点,则需要给节点标记id
if(!Array.isArray(data)) {
markNodeData(this, data);
}
this.data = data;
this.childNodes = [];
let children;
// 如果该节点的层级为0,且该data为数组类型
// 根节点下的data是一个数组,其子节点便是根据此生成的
if(this.level === 0 && this.data instanceof Array) {
children = this.data;
} else {
// 非根节点,看其children字段是否还存在
children = getPropertyFromData(this, 'children') || [];
}
// 子节点的生成
for(let i = 0, j = children.length; i < j; i++) {
this.insertChild({data: children[i]});
}
// console.log('ndoe', this);
}
/**
* 插入子节点childNodes
* @param {*} child 
* @param {*} index 
*/
insertChild(child, index) {
// console.log('insertChild', child, child instanceof Node);
// 如果child不是Node的实例对象
if(!(child instanceof Node)) {
// 将后面的对象值添加到child
objectAssign(child, {
parent: this,
store: this.store,
});
// 创建child节点
child = new Node(child);
// console.log('chi', child);
}
child.level = this.level + 1;
// console.log('ch', child, index);
/**
* typeof index !== 'undefined'
* index !== undefined
* 
* 将child插入到childNodes
*/
if(typeof index === 'undefined' || index < 0) {
// console.log(typeof index !== 'undefined')
this.childNodes.push(child);
} else {
// console.log(index, index === undefined)
this.childNodes.splice(index, 0, child)
}
}
/**
* 子树收缩
* 设置展开属性
* node.expanded = false
*/
collapse() {
this.expanded = false;
// console.log('collapse', this, this.expanded);
}
/**
* 展开子树
* 设置节点的展开属性
* node.expanded = true
* 
* 注意:树上的每个节点都具有展开和伸缩子树的方法,而不是将这两个方法共享
* 保证了树节点的独立性质
*/
expand() {
// console.log('展开子树', this);
this.expanded = true;
}
}

总结

实现思路

  • tree.vue 渲染树
  • tree-node.vue 渲染子树
  • tree-store.js 树的状态管理器
  • node.js 定义树节点的属性和方法

学到知识

  • Vue 函数式组件
  • Vue 组件的递归调用

原创文章,作者:我心飞翔,如若转载,请注明出处:https://www.pipipi.net/14752.html

发表评论

登录后才能评论