元素-洋葱树
以清晰的层次结构呈现信息,可以展开或折叠。
树木特征
参数 | 使用说明 | 类型 | 可选值 | 标准 |
---|---|---|---|---|
数据 | 显示数据 | 数组 | — | — |
节点键 | 每个树节点作为唯一标识符,整棵树必须是唯一的 | 弦 | — | — |
显示复选框 | 或者可以选择节点 | 布尔值 | — | 假 |
默认全部展开 | 是否默认展开所有节点 | 布尔值 | — | 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.vue 显示树
- tree-node.vue 显示子树
- tree-store.js 树状态管理器
- node.js定义了树节点的树形式和方法
tree.vue
tree.vue 是树组件的输入
- 接收props data传递的数据
- 根据root数据生成树节点
- 基于root的观树
获取props data传递的数据
props: {
// 树要展示的数据
data: {
type: Array,
},
// 树节点的key
nodeKey: String,
// 配置项
props: {
type: Object,
default: function() {
return {
// 指定子树为节点某个对象的值即"children"对应值表示子节点的数据
children: 'children',
// 指定节点标签为节点对象的某个属性的值
label: 'label',
}
}
},
}
根据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
树的树存储状态管理器,生成树节点集
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组件的递归调用
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。