先简单列一下在实现树组件的过程中一些值得关注的节点.
~ 如何调用组件自身
由于树是一个递归的数据结构, 必然需要对组件自身的递归调用.
我们只需给组件指定 name 属性, 即可以在组件内部直接使用. 此处需要注意的是每次调用都会生成一个独立的作用域.
- <!-- html -->
- <template>
- <div>
- ...
- <my-tree></my-tree>
- </div>
- </template>
- <!-- js -->
- export default {
- name: 'myTree',
- ...
- }
~ props 及事件监听如何传递给子组件(以下属性需 vue2.4.0+)
我们在使用组件的时候可能会指定一些属性以实现对组件的差异化定制, 这就需要我们自己去实现对父组件的属性继承.
- <my-tree v-bind="{...$props, ...$attrs}" v-on="$listeners"
- :data="item[props.children]"
- :child-node="true">
- </my-tree>
$props 表示用户调用时指定并且在组件 props 中有声明去接收的属性集合, 没有声明接收的属性则会归类到 $attrs 中. 但是此时我们肯定不希望继承父组件的数据, 所以我们需要自己指定 data 去覆盖掉父组件中对应的属性.
此外, 当我们想触发子组件的监听事件时, 由于子组件的调用者是其父组件, 然而我们想要通知的是外层树组件的调用者. 此时我们就可以借用 $listeners,$listeners 包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器, 这样不管子组件所处的层级, 事件都会直接触发给外层树组件的调用者.
我们可以子组件中指定一个 child-node 属性, 通过 props 去接收它, 这样就可以非常方便的区分是否是最顶层的作用域.
~ 组件 slot
允许用户灵活自定义内容是必不可少的一环, 我们不可能兼顾所有的使用的场景, 所以 slot 的使用也是组件的一部分.
1,slot 数据的传递
当我们自定义组件 slot 时, 我们需要将当前组件的数据传出去以便用户展示数据的内容.
slot 分具名 slot 和不具名 slot(即默认的 slot)
子组件 -- 不具名 slot (以下两种写法是等价的)
- <slot v-bind="{...item}">
- {{item.label}}
- </slot>
- <slot v-bind:default="{...item}">
- {{item.label}}
- </slot>
子组件 -- 具名 slot (指定了名字: emptyText)
<slot v-bind:emptyText="keywords">暂无数据</slot>
父组件自定义 slot
- <my-tree>
- <!-- scope[slotName]的值就是 v-bind 传递过来的数据 -->
- <template v-slot="scope">
- <div>{{scope.default.xxx}}</div>
- </template>
- <template v-slot:emptyText="scope">
- <span>{{scope.emptyText}}下无搜索结果</span>
- </template>
- </my-tree>
2,slot 递归
由于组件递归调用自身, 所以组件内部需要将 slot 逐级传递给子组件.
以下以默认 slot 为例:
- <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>
~ 组件全局变量
前面提到组件的每次调用都会生成一个独立的作用域, 但是很多情况下我们需要一个全局的变量去记录组件的状态, 如: 当前高亮选中的节点, 已展开节点的 keys 等.
此时我们需求是不论在哪个组件作用域内, 修改变量可以达到影响全局的效果, 很容易就能想到它 --Object 对象. 所以我们可以通过在顶层组件内定义一个对象, 逐级传给所有的子组件, 这样我们就能拥有一个全局的变量了.
- <!-- 子组件 -->
- <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: '', // 当前选中的 key
- selectedItems: [], // 已选择的节点 (可选状态下)
- openedItems: [], // 已展开的节点
- ...
- }
- // treeGlobal 没有 props 接收, 在 $sttrs 中
- if(this.$attrs.treeGlobal) {
- globalTemp = this.$attrs.treeGlobal;
- }
- return {
- treeGlobal: globalTemp,
- treeData: [],
- ...
- }
- },
- ...
- },
这样我们就拥有了一个全局变量 treeGlobal, 每个子组件对 treeGlobal 的修改都会在全局范围内生效.
需要注意的是, 记录节点是否展开或者选中, 最容易想到的方法就是给节点添加对应的属性, 然后根据操作修改该属性的状态. 然而这个属性是在申明了 treeData 后再在其子对象上追加上去的, vue 并不是监听其值得变化, 话句话说, 它的变化并不会刷新视图 (用 $set 也没用的). 当然可以通过 $forceUpdate() 去强制刷新视图, 但官方并不推荐使用 $forceUpdate(), 更何况每个需要变化的组件自身都需要触发一下 $forceUpdate()($forceUpdate()只会刷新当前组件的内容). 以上, 我们可以在全局变量中申明一个对象去记录这些状态, 监听的事就可以还给 vue 自身, 我们只需关注数据的变化就好了.
~ 部分代码展示
代码太长就不贴了, 贴一点主要的部分, 辅助理解.
- <!--
- 自定义 props 自定义缩进 自定义行高 内容 slot 默认展开(key) 事件响应 选中样式(默认选中及样式可配置) 宽度问题 checkbox
- -->
- <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, // 是否内部循环调用的子节点 若是则不重复调用格式化数据
- default: false
- },
- indent: {
- type: Number, // 缩进
- default: 20
- },
- cellHeight: {
- type: Number, // 单行高度
- default: 34
- },
- nodeKey: {
- type: String, // 必须 设置 nodeKey
- default: ''
- },
- currentNodeKey: {
- type: [String, Number], // 当前选中的节点
- default: ''
- },
- highlightCurrent: {
- type: String, // 高亮展示
- default: 'leaf' // 'none': 都不高亮 leaf: 仅叶子节点(默认) all: 所有节点
- },
- className: {
- type: String, // 最顶层样式
- default: ''
- },
- showCheckbox: {
- type: Boolean, // 显示复选框
- default: false
- },
- },
附一张效果图
以上, tks~
来源: http://www.jianshu.com/p/f372828aa727