设计树形组件
组件功能分析
树形组件的主要特点是可以无限层级、这种需求在日常工作和生活中其实很常见,比如后台管理系统的菜单管理、文件夹管理、生物分类、思维导图等等。
树形组件的节点可以无限展开,父节点可以展开和收起节点,并且每一个节点有一个复选框,可以切换当前节点和所有子节点的选择状态。同一级所有节点选中的时候,父节点也能自动选中
<el-tree :data="data"
show-checkbox
v-model:expanded="expandedList"
v-model:checked="checkedList"
:defaultNodeKey="defaultNodeKey"
>
</el-tree>
<script>
export default {
data() {
return {
expandedList: [4, 5],
checkedList: [5],
data: [
{
id: 1,
label: '一级 1',
children: [
{
id: 4,
label: '二级 1-1',
children: [
{
id: 9,
label: '三级 1-1-1'
},
{
id: 10,
label: '三级 1-1-2'
}
]
}
]
},
{
id: 2,
label: '一级 2',
children: [
{
id: 5,
label: '二级 2-1'
},
{
id: 6,
label: '二级 2-2'
}
]
}
],
defaultNodeKey: {
childNodes: 'children',
label: 'label'
}
}
}
}
</script>组件实现
这里父节点和子节点的样式操作完全一致,并且可以无限嵌套,这种需求需要组件递归来实现,也就是组件内部渲染自己渲染自己
首先进入到 Element3 的 tree 文件夹内部,然后找到 tree.vue 文件。tree.vue 是组件的入口容器,用于接收和处理数据,并将数据传递给 TreeNode.vue;TreeNode.vue 负责渲染树形组件的选择框、标题和递归渲染子元素
提供了 el-tree 的容器并导入了 el-tree-node 进行渲染。tree.vue 通过 provide 向所有子元素提供 tree 的数据,通过 useExpand 判断树形结构的展开状态,并且用到了 watchEffect 去向组件外部通知 update:expanded 事件
<template>
<div class="el-tree">
<el-tree-node v-for="child in tree.root.childNodes" :node="child" :key="child.id"></el-tree-node>
</div>
</template>
<script>
import ElTreeNode from './TreeNode.vue'
const instance = getCurrentInstance()
const tree = new Tree(props.data, props.defaultNodeKey, {
asyncLoadFn: props.asyncLoadFn,
isAsync: props.async
})
const state = reactive({
tree
})
provide('elTree', instance)
useTab()
useExpand(props, state)
function useExpand(props, state) {
const instance = getCurrentInstance()
const { emit } = instance
if (props.defaultExpandAll) {
state.tree.expandAll()
}
watchEffect(() => {
emit('update:expanded', state.tree.expanded)
})
watchEffect(() => {
state.tree.setExpandedByIdList(props.expanded, true)
})
onMounted(() => {
state.tree.root.expand(true)
})
}
</script>进入到 Tree.Node.vue 文件中,tree-node 组件是树组件的核心,一个 TreeNode 组件包含四个部分:展开按钮、文本的多选框、每个节点的标题和递归的 children 子节点。
看 TreeNode.vue 的模板基本结构,把下面的 div 标签分成四个部分:el-tree-node__content 负责每个树节点的渲染,第一个 span 就是渲染展开符;el-checkbox 组件负责显示复选框,并且绑定了 node.isChecked 属性;el-node__contentn 负责渲染树节点的标题;el-tree__children 负责递归渲染 el-tree-node 节点,组件内部渲染自己,这就是组件递归的写法
<div
v-show="node.isVisable"
class="el-tree-node"
:class="{
'is-expanded': node.isExpanded,
'is-current': elTree.proxy.dragState.current === node,
'is-checked': node.isChecked,
}"
role="TreeNode"
ref="TreeNode"
:id="'TreeNode' + node.id"
@click.stop="onClickNode"
>
<div class="el-tree-node__content">
<span
:class="[
{ expanded: node.isExpanded, 'is-leaf': node.isLeaf },
'el-tree-node__expand-icon',
elTree.props.iconClass
]"
@click.stop="node.isLeaf ||(elTree.props.accordion ? node.collapse() : node.expand())
">
</span>
<el-checkbox
v-if="elTree.props.showCheckbox"
:modelValue="node.isChecked"
@update:modelValue="onChangeCheckbox"
@click="elTree.emit('check', node, node.isChecked, $event)"
>
</el-checkbox>
<el-node-content class="el-tree-node__label" :node="node"></el-node-content>
</div>
<div
class="el-tree-node__children"
v-show="node.isExpanded"
v-if="!elTree.props.renderAfterExpand || node.isRendered"
role="group"
:aria-expanded="node.isExpanded"
>
<el-tree-node
v-for="child in node.childNodes"
:key="child.id"
:node="child"
>
</el-tree-node>
</div>
</div>然后看下 tree-node 中需要处理的数据有哪些。先通过 inject 注入 tree 组件最完成的配置。然后在点击节点的时候,通过判断 elTree 的全局配置,去决定点击之后的切换功能,并且在展开和 checkbox 切换的同时,通过 emit 对父组件触发事件
const elTree = inject('elTree')
const onClickNode = (e) => {
!elTree.props.expandOnClickNode ||props.node.isLeaf ||
(elTree.props.accordion ? props.node.collapse() : props.node.expand())
!elTree.props.checkOnClickNode ||props.node.setChecked(undefined, elTree.props.checkStrictly)
elTree.emit('node-click', props.node, e)
elTree.emit('current-change', props.node, e)
props.node.isExpanded? elTree.emit('node-expand', props.node, e): elTree.emit('node-collapse', props.node, e)
}
const onChangeCheckbox = (e) => {
props.node.setChecked(undefined, elTree.props.checkStrictly)
elTree.emit('check-change', props.node, e)
}到这里,树结构的渲染其实就结束了。
但是有些场景需要对树节点的渲染内容进行自定。比如后面这段代码,在节点的右侧加上 append 和 delete 操作按钮,这种需求在菜单树的管理中很常见
这个时候节点需要支持内容的自定义,然后注册了 el-node-content 组件。这个组件使用起来非常简单,由于还需要支持节点的自定义渲染,所以要把这部分抽离成组件。当 slots.default 为函数的时候,返回函数的执行内容;或者传递的 renderContent 是函数的话,也要返回函数执行的结果。
import { TreeNode } from './entity/TreeNode'
import { inject, h } from 'vue'
render(ctx) {
const elTree = inject('elTree')
if (typeof elTree.slots.default === 'function') {
return elTree.slots.default({ node: ctx.node, data: ctx.node.data.raw })
} else if (typeof elTree.props.renderContent === 'function') {
return elTree.props.renderContent({
node: ctx.node,
data: ctx.node.data.raw
})
}
return h('span', ctx.node.label)
}结合代码例子做理解,下面的代码中用了 render-content 的方式返回树形结构的渲染结果,render-content 传递的函数内部会根据 node 和 data 数据,返回对应的标题,并且新增了两个 el-button 组件
<div class="custom-tree-container">
<div class="block">
<p>使用 render-content</p>
<el-tree
:data="data1"
show-checkbox
default-expand-all
:expand-on-click-node="false"
:render-content="renderContent"
>
</el-tree>
</div>
</div>
<script>
function renderContent({ node, data }) {
return (
<span class="custom-tree-node">
<span>{data.label}</span>
<span>
<el-button
size="mini"
type="text"
onClick={() => this.append(node, data)}
>
Append
</el-button>
<el-button
size="mini"
type="text"
onClick={() => this.remove(node, data)}
>
Delete
</el-button>
</span>
</span>
)
}
</script>上面的代码会渲染出下面的示意图的效果

最后还可以对树实现更多操作方式的支持。比如可以支持树形结构的拖拽修改、可以把任何任意节点拖拽到其他树形内部、修改整个树形结构的内容。想要实现这些功能,就需要监听节点的 drag-over、drag-leave 等拖拽事件,在 drop 事件执行的时候,把拖拽的节点数据,复制给拖拽的节点中完成修改即可