[Element Plus 源码解析] Collapse 折叠面板

2,437 阅读2分钟

一、组件介绍

官网链接:Collapse 折叠面板 | Element (gitee.io)

collapse折叠面板组件可以折叠/展开内容区域,通常与collapse-item组件组合使用。

1.1 collapse 属性

  • v-model/model-value: string/array类型;指定当前激活的面板,当为手风琴模式时, 值的类型必须是string;
  • accordion: boolean类型;是否手风琴模式(一次只能展开一个面板),默认false;

1.2 collapse-item 属性

  • name: string/number类型,唯一标识符,如果不传入,会随机生成一个四位数字作为name;
  • title:string类型,面板标题;
  • disabled: boolean类型,是否禁用,默认false;

1.3 collapse-item 具名插槽

  • title: 可以使用具名插槽自定义标题

二、源码分析

2.1 collapse组件源码

<template>
  <div class="el-collapse" role="tablist" aria-multiselectable="true">
    <slot></slot>
  </div>
</template>

<script lang="ts">
// mitt是一个事件总线库,提供on/emit/off等方式进行事件监听/发射等功能
import mitt, { Emitter } from 'mitt'
setup(props, { emit }) {
    // modelValue可能是string也可能是array,通过[].concat将其装换成array类型
    const activeNames = ref([].concat(props.modelValue))
    // 生成一个事件总线
    const collapseMitt: Emitter = mitt()
    // 设置激活的面板,参数是面板的name
    const setActiveNames = _activeNames => {
      // 转换成数组
      activeNames.value = [].concat(_activeNames)
      // 手风琴模式下,只取第一个元素
      const value = props.accordion ? activeNames.value[0] : activeNames.value
      // 向上发射v-model事件
      emit(UPDATE_MODEL_EVENT, value)
      // 向上发射change事件
      emit(CHANGE_EVENT, value)
    }
    
    // item点击事件
    const handleItemClick = name => {
      if (props.accordion) {
        // 手风琴模式
        setActiveNames(
          (activeNames.value[0] || activeNames.value[0] === 0) &&
            activeNames.value[0] === name
            ? ''
            : name,
        )
      } else {
        const _activeNames = activeNames.value.slice(0)
        const index = _activeNames.indexOf(name)
        // 当前item已经展开,此次点击则是关闭操作
        if (index > -1) {
          _activeNames.splice(index, 1)
        } else {
        // 当前item未展开,此次点击是打开操作
          _activeNames.push(name)
        }
        // 调用setActiveNames
        setActiveNames(_activeNames)
      }
    }
    
    // 监听 modelValue 的值
    watch(
      () => props.modelValue,
      () => {
        activeNames.value = [].concat(props.modelValue)
      },
    )
    
    // mitt事件总线监听item-click,使用handleItemClick作为处理函数
    collapseMitt.on('item-click', handleItemClick)

    onUnmounted(() => {
      // 卸载时清除所有事件监听,防止内存泄漏
      collapseMitt.all.clear()
    })
    
    // 向子组件提供数据
    provide('collapse', {
      activeNames,
      collapseMitt,
    })

    return {
      activeNames,
      setActiveNames,
      handleItemClick,
    }
  },
})
</script>

总结:collapse组件代码比较简单,template部分提供默认插槽,script部分负责维护激活面板等数据,使用mitt事件总线与子组件通信,监听item-click事件。

2.2 collapse-item 组件

template

<template>
  <div
    class="el-collapse-item"
    :class="{'is-active': isActive, 'is-disabled': disabled }"
  >
    // 标题部分
    <div
      role="tab"
      :aria-expanded="isActive"
      :aria-controls="`el-collapse-content-${id}`"
      :aria-describedby="`el-collapse-content-${id}`"
    >
      <div
        :id="`el-collapse-head-${id}`"
        class="el-collapse-item__header"
        role="button"
        :tabindex="disabled ? -1 : 0"
        :class="{
          'focusing': focusing,
          'is-active': isActive
        }"
        @click="handleHeaderClick"
        @keyup.space.enter.stop="handleEnterClick"
        @focus="handleFocus"
        @blur="focusing = false"
      >
        // slot具名插槽
        <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是一个展示过渡效果的组件
    // 其内部使用vue官方的transition组件,并绑定enter/leave等钩子函数,实现动画效果
    <el-collapse-transition>
      // 内容部分
      <div
        v-show="isActive"
        :id="`el-collapse-content-${id}`"
        class="el-collapse-item__wrap"
        role="tabpanel"
        :aria-hidden="!isActive"
        :aria-labelledby="`el-collapse-head-${id}`"
      >
        <div class="el-collapse-item__content">
          <slot></slot>
        </div>
      </div>
    </el-collapse-transition>
  </div>
</template>

script部分

setup(props) {
    // inject注入父组件提供的数据
    const collapse = inject<CollapseProvider>('collapse')
    // 使用父组件提供的mitt事件总线
    const collapseMitt = collapse?.collapseMitt

    const contentWrapStyle = ref({
      height: 'auto',
      display: 'block',
    })
    const contentHeight = ref(0)
    const focusing = ref(false)
    const isClick = ref(false)
    // 生成随机Id
    const id = ref(generateId())
    
    // 动态计算,父组件的activeNames是否包含当前name
    const isActive = computed(() => {
      return collapse?.activeNames.value.indexOf(props.name) > -1
    })
    
    const handleFocus = () => {
      setTimeout(() => {
        if(!isClick.value) {
          focusing.value = true
        } else {
          isClick.value = false
        }
      }, 50)
    }
    
    // 点击标题事件
    const handleHeaderClick = () => {
      if(props.disabled) return
      // 通过mitt事件总线,发送item-click事件,参数是当前面板的name
      collapseMitt?.emit('item-click', props.name)
      focusing.value = false
      isClick.value = true
    }
    // enter按键事件,也是发送item-click事件
    const handleEnterClick = () => {
      collapseMitt?.emit('item-click', props.name)
    }

    return {
      isActive,
      contentWrapStyle,
      contentHeight,
      focusing,
      isClick,
      id,
      handleFocus,
      handleHeaderClick,
      handleEnterClick,
      collapse,
    }
  },

2.3 总结

  1. mitt是一个事件总线库,提供on/emit/off/clear等方法;
  2. 使用transition展示过渡效果,过渡效果可以通过js或css控制;