ShiningDan的博客

一天一个 Element 组件 - Collapse

本文是 Element 的组件源码学习系列。

项目源码:ElemeFE/element | GitHub,Tag:v2.13.0

Collapse 组件使用文档:Tag 标签

.vue 文件:/packages/collapse

.scss 文件:/packages/theme-chalk/collapse.scss

.d.ts 文件:/types/collapse.d.ts

折叠面板,是前端入门的同学,经常手写的入门 Demo,我们现在也来学习一下,Element 是怎么做折叠面板这个组件的实现的。

Props

一样的,咱们先来了解一下 el-collapse 组件的入参,看看用户是如何使用这个组件的:

1
2
3
4
5
6
7
8
9
props: {
accordion: Boolean,
value: {
type: [Array, String, Number],
default() {
return [];
}
}
},

accordion 属性,表示的是,当前折叠面板,是否使用手风琴模式。

value 属性,当前激活的面板(如果是手风琴模式,绑定值类型需要为 string,否则为array)。表示的是用户页面上,被激活的面板是哪一个。手风琴模式,只有一个唯一的被激活面板,所以绑定值类型就是一个 string 了。

但是,我们并不是直接给 value 属性赋值,而是使用的 v-model 来利用 value 属性。组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件

我们来看一下具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
// collapse.vue
data() {
return {
activeNames: [].concat(this.value)
};
},

watch: {
value(value) {
this.activeNames = [].concat(value);
}
},

首先,当我们给 value 属性赋值,或者给 v-model 赋值的时候,都会改变 activeNames 的值。这里比较巧妙的是使用 Array.prototype.concat 方法,因为 value 的类型可能是一个数组,也可能是一个元素,所以直接用 concat 方法统一处理成一个元素。

首先,用户在点击折叠面板的时候,点击的是里层 el-collapse-item 元素,所以我们来看一下,用户在点击打开或关闭一个面板,会有什么相关的逻辑:

1
2
3
4
5
6
7
// collpase-item.vue
handleHeaderClick() {
if (this.disabled) return;
this.dispatch('ElCollapse', 'item-click', this);
this.focusing = false;
this.isClick = true;
},

当这个组件没有设置不可点击的时候,首先会通过 this.dispatch 方法,给父 ElCollapse 组件发出一个 item-click 事件。然后设置两个属性的值。

等等,this.dispatch 是个啥?好像不是原生的 dispatchEvent 属性啊,但是看起来差不多。也没有见到 Vue 提供 dispatch 方法呀。最后,我们在 src/mixins/emitter.js 中找到了该方法的实现,原来是是注册到全局的一个混入方法。我们来看一下具体的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/mixins/emitter.js
dispatch(componentName, eventName, params) {
var parent = this.$parent || this.$root;
var name = parent.$options.componentName;

while (parent && (!name || name !== componentName)) {
parent = parent.$parent;

if (parent) {
name = parent.$options.componentName;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},

其本质方法,就是通过 this.$parent,层层向上,找对应的组件,然后调用该组件的 $emit 方法,产生一个事件。

在咱们这里,是找到祖先的 ElCollapse 组件,因为 Element 的推荐使用方案里面,el-collapse-item 组件是要被包裹在 el-collpase 组件内部的。然后产生一个 item-click 事件。我们来看看 item-click 事件做了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// collpase.vue
methos: {
setActiveNames(activeNames) {
activeNames = [].concat(activeNames);
let value = this.accordion ? activeNames[0] : activeNames;
this.activeNames = activeNames;
this.$emit('input', value);
this.$emit('change', value);
},
handleItemClick(item) {
if (this.accordion) {
this.setActiveNames(
(this.activeNames[0] || this.activeNames[0] === 0) &&
this.activeNames[0] === item.name
? '' : item.name
);
} else {
let activeNames = this.activeNames.slice(0);
let index = activeNames.indexOf(item.name);

if (index > -1) {
activeNames.splice(index, 1);
} else {
activeNames.push(item.name);
}
this.setActiveNames(activeNames);
}
}
}

created() {
this.$on('item-click', this.handleItemClick);
}

el-collpasecreated 方法中,设置了组件需要监听 item-click 事件,并且调用 handleItemClick 方法。

handleItemClick 方法,主要是处理 el-collpase-item 事件的入参。大致的逻辑是,将 this.activeNames 设置为正确的值。什么是正确的值呢?

  1. 对于手风琴模式:如果当前 item 已经是 this.activeNames[0](说明已经打开了),则设置为 ''(关上),否则将 this.activeNames[0] 设置为被点击 item(打开)。
  2. 对于非手风琴模式:如果当前 item 已经在 this.activeNames 数组中存在了(说明已经打开了),则从数组中过滤(关上);否则添加到数组中(打开)

最后再触发 input 事件 和 change 事件。input 事件是 v-model 要求触发的事件;change 事件是 el-collapse 对外提供的监听事件。

所以,我们对于 el-collpase 组件负责的事情,有一个总结:

  1. 维护一个折叠面板的总体面板开关状态
  2. 监听每一个面板的点击事件,计算出最新的总体面板开关状态
  3. 提供对外的 change 事件

而每个面板 el-collpase-item,是否显示,在面板组件中自己实现。

el-collpase-item 控制显示效果

下面,我们来看看,el-collpase-item 如何控制自己是否要显示的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// el-collpase-item
props: {
name: {
type: [String, Number],
default() {
return this._uid;
}
},
},
inject: ['collapse'],
computed: {
isActive() {
return this.collapse.activeNames.indexOf(this.name) > -1;
}
},

每一个 el-collpase-item,都会注入父 el-collpase 组件,然后监听 this.collapse.activeNames 数组,来判断自己是否要显示。所有的 el-collpase-item 组件,都需要提供一个唯一的 name 属性,来表明自己是谁,这样才能计算出哪个面板需要展开。如果没有提供 name 属性,Element 会使用 this._uid 属性来表明每一个组件。this._uid 是 Vue 的一个内置属性,每个 Vue 实例都会有一个递增的id。

现在,el-collpase-item 已经有了 isActive 属性来表示该组件是否应该展示了,下面来看看展示、消失的效果是如何实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// el-collpase-item 的 <template>
<div class="el-collapse-item"
:class="{'is-active': isActive, 'is-disabled': disabled }">
<div
>
<div
@click="handleHeaderClick"
@keyup.space.enter.stop="handleEnterClick"
:class="{
'focusing': focusing,
'is-active': isActive
}"
@focus="handleFocus"
@blur="focusing = false"
>
<slot name="title">{{title}}</slot>
<i
class="el-collapse-item__arrow el-icon-arrow-right"
:class="{'is-active': isActive}">
</i>
</div>
</div>
<el-collapse-transition>
<div
class="el-collapse-item__wrap"
v-show="isActive"
:id="`el-collapse-content-${id}`"
>
<div class="el-collapse-item__content">
<slot></slot>
</div>
</div>
</el-collapse-transition>
</div>

整个 el-collpase-item 主要分两部分,第一部分是 Header,第二部分是会被折叠的主要内容。

Header 部分我就不仔细介绍了,主要是使用 flex 布局,然后最右边的箭头初始化的时候是一个向右的箭头,如果有 is-active 类,箭头就会通过 CSS 旋转能力转为向下 transform: rotate(90deg);

Content 部分,要展示的内容,通过一个 <slot> 被包裹在一个 el-collapse-transition 中。这个 el-collapse-transition 是什么呢?我们可以在 src/transition/collapse-transition.js 中找到。具体的实现多,但主要是实现各个动画接口的效果。所有的动画接口,是复用的 Vue transition 组件的接口,参考 过渡 & 动画 | JavaScript 钩子,也不一一细讲了,