6

一天一个 Element 组件 - Collapse

 2 years ago
source link: https://shiningdan.github.io/2020/02/16/%E4%B8%80%E5%A4%A9%E4%B8%80%E4%B8%AA-Element-%E7%BB%84%E4%BB%B6-Collapse/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文是 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 组件的入参,看看用户是如何使用这个组件的:

props: {
accordion: Boolean,
value: {
type: [Array, String, Number],
default() {
return [];
}
}
},

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

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

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

我们来看一下具体实现:

// 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 元素,所以我们来看一下,用户在点击打开或关闭一个面板,会有什么相关的逻辑:

// 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 中找到了该方法的实现,原来是是注册到全局的一个混入方法。我们来看一下具体的实现:

// 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 事件做了什么:

// 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 如何控制自己是否要显示的:

// 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 属性来表示该组件是否应该展示了,下面来看看展示、消失的效果是如何实现的:

// 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 钩子,也不一一细讲了,


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK