Element Plus Collapse 折叠面板完全指南:从基础用法到源码级实现原理
【免费下载链接】element-plus🎉 A Vue.js 3 UI Library made by Element team项目地址: https://gitcode.com/GitHub_Trending/el/element-plus
导读
Collapse(折叠面板)是 Element Plus 中最常用的内容收纳组件之一,用于将大量内容按区块折叠展示,帮助页面保持简洁并提升信息检索效率。本文以 Collapse 官方文档 为主体,结合 Element Plus 仓库中packages/components/collapse的源码与测试,系统讲解 Collapse 的多面板展开、手风琴模式、自定义标题与图标、展开图标位置、防折叠拦截等全部能力,并深入剖析其底层状态管理与过渡动画的实现机制。读完本文,你将能够熟练使用 Collapse 及其全部 API,并理解其在 Vue 3 组合式 API 下的实现原理。
一、Collapse 是什么
Collapse 用于"收纳内容"(官方文档原话:Use Collapse to store contents)。它由两部分组成:
- ElCollapse:容器组件,负责维护当前展开面板的状态列表;
- ElCollapseItem:单个面板,包含标题区(header)与内容区(content),点击标题即可展开/收起。
在组件树中,二者通过依赖注入(provide/inject)通信,这与 Element Plus 众多父子组件的协作模式一致。源码结构见 collapse 目录,核心文件如下:
| 文件 | 职责 |
|---|---|
| collapse.vue | 容器组件,暴露activeNames与setActiveNames |
| collapse-item.vue | 单个面板组件,含标题、图标、内容与过渡动画 |
| collapse.ts | Collapse 的 props 与 emits 类型定义 |
| collapse-item.ts | CollapseItem 的 props 类型定义 |
| use-collapse.ts | 容器状态管理组合函数 |
| use-collapse-item.ts | 面板交互逻辑组合函数 |
| constants.ts | 父子通信的 InjectionKey 定义 |
| collapse.test.tsx | 组件行为测试用例 |
二、基础用法:多面板展开
默认情况下,Collapse 允许多个面板同时展开。通过v-model绑定当前展开面板的name数组,并通过change事件监听变化:
<template> <div class="demo-collapse"> <el-collapse v-model="activeNames" @change="handleChange"> <el-collapse-item title="Consistency" name="1"> <div>Consistent with real life: in line with the process and logic of real life...</div> </el-collapse-item> <el-collapse-item title="Feedback" name="2"> <div>Operation feedback: enable the users to clearly perceive their operations...</div> </el-collapse-item> </el-collapse> </div> </template> <script lang="ts" setup> import { ref } from 'vue' import type { CollapseModelValue } from 'element-plus' const activeNames = ref(['1']) const handleChange = (val: CollapseModelValue) => { console.log(val) } </script>完整示例见 basic.vue。
要点说明:
- v-model 绑定值类型:非手风琴模式下是
array(CollapseModelValue),对应源码中CollapseModelValue = Arrayable<CollapseActiveName>,其中CollapseActiveName = string | number,见 collapse.ts; - 默认值:
modelValue默认为空数组[](源码中default: () => mutable([])); change事件:在激活面板发生变化时触发,回调参数为当前激活的name数组或字符串。
从源码看,容器内部将modelValue归一化为数组并存入activeNames这个ref(use-collapse.ts),同时通过watch深度监听外部modelValue的变化并同步内部状态(同文件 L84-L88)。也就是说,父组件修改绑定值时,面板状态会即时响应。
多面板展开的切换逻辑
在非手风琴模式下,点击某个面板的标题时,handleChange会先复制当前activeNames,若目标name已在其中则移除(收起),否则追加(展开):
// packages/components/collapse/src/use-collapse.ts const _activeNames = [...activeNames.value] const index = _activeNames.indexOf(name) if (index > -1) { _activeNames.splice(index, 1) } else { _activeNames.push(name) } setActiveNames(_activeNames)随后setActiveNames会同时触发update:modelValue与change两个事件(use-collapse.ts),保证双向绑定与外部监听都能拿到最新状态。
三、手风琴模式(Accordion)
开启accordion属性后,同一时间只允许展开一个面板;点击已展开的面板则会将其收起:
<template> <div class="demo-collapse"> <el-collapse v-model="activeName" accordion> <el-collapse-item title="Consistency" name="1"> <div>...</div> </el-collapse-item> <el-collapse-item title="Feedback" name="2"> <div>...</div> </el-collapse-item> </el-collapse> </div> </template> <script lang="ts" setup> import { ref } from 'vue' const activeName = ref('1') </script>完整示例见 accordion.vue。
手风琴模式有两个关键差异,均可在源码中得到印证:
- v-model 绑定值类型变为
string(或number),而不是数组; - 切换逻辑为互斥替换:点击某个面板时,若它已是当前激活面板则设为空字符串(收起),否则直接替换为点击的面板:
// packages/components/collapse/src/use-collapse.ts if (props.accordion) { setActiveNames([activeNames.value[0] === name ? '' : name]) }setActiveNames在向外 emit 时,会把手风琴模式下的数组压缩成单个值:
const value = props.accordion ? activeNames.value[0] : activeNames.value emit(UPDATE_MODEL_EVENT, value) emit(CHANGE_EVENT, value)测试用例印证
仓库的测试用例 collapse.test.tsx 分别验证了多面板模式与手风琴模式的行为:
create用例:初始激活['1'],点击第 3 个面板后1与3同时激活,再点击第 1 个面板则1收起——验证多面板同时展开;accordion用例:初始激活['1'],点击第 3 个面板后1自动收起、3激活——验证互斥展开。
四、自定义标题(Custom title)
除了title属性,你还可以通过具名插槽#title自定义标题内容,例如加入图标、徽标或根据展开状态改变样式。
重要版本说明:自版本^(2.9.10)起,title插槽会提供一个isActive属性,用于指示当前面板是否处于激活状态,这为"展开时高亮标题"等交互提供了便捷的响应式数据。
<template> <div class="demo-collapse"> <el-collapse accordion> <el-collapse-item name="1"> <template #title="{ isActive }"> <div :class="['title-wrapper', { 'is-active': isActive }]"> Consistency <el-icon class="header-icon"> <info-filled /> </el-icon> </div> </template> <div>...</div> </el-collapse-item> <el-collapse-item title="Feedback" name="2"> <div>...</div> </el-collapse-item> </el-collapse> </div> </template> <script setup lang="ts"> import { InfoFilled } from '@element-plus/icons-vue' </script> <style scoped> .title-wrapper { display: flex; align-items: center; gap: 4px; } .title-wrapper.is-active { color: var(--el-color-primary); } </style>完整示例见 customization.vue。
源码中标题插槽的渲染逻辑位于 collapse-item.vue:
<span :class="itemTitleKls"> <slot name="title" :is-active="isActive">{{ title }}</slot> </span>可见title插槽的默认内容为title属性值;当提供插槽时,插槽内容完全替代默认标题,并通过作用域插槽参数isActive拿到激活状态。isActive是一个ComputedRef,其计算逻辑为:
const isActive = computed(() => collapse?.activeNames.value.includes(unref(name)))即面板的name是否包含在容器的激活列表中。
五、自定义图标(Custom icon)
自版本^(2.8.3)起,CollapseItem 增加了icon属性与#icon插槽,用于自定义面板展开箭头图标。
<template> <div class="demo-collapse"> <el-collapse v-model="activeNames" @change="handleChange"> <!-- 通过 icon 属性替换为 CaretRight 图标 --> <el-collapse-item title="Consistency" name="1" :icon="CaretRight"> <div>...</div> </el-collapse-item> <!-- 通过 #icon 插槽完全自定义图标区域 --> <el-collapse-item title="Feedback" name="2"> <template #icon="{ isActive }"> <span class="icon-ele"> {{ isActive ? 'Expanded' : 'Collapsed' }} </span> </template> <div>...</div> </el-collapse-item> </el-collapse> </div> </template> <script lang="ts" setup> import { ref } from 'vue' import { CaretRight } from '@element-plus/icons-vue' import type { CollapseModelValue } from 'element-plus' const activeNames = ref(['1']) const handleChange = (val: CollapseModelValue) => { console.log(val) } </script>完整示例见 custom-icon.vue。
icon 属性的两种用法
从源码 collapse-item.ts 可以看到icon的类型为IconPropType(支持字符串或 Vue 组件),默认值为ArrowRight:
icon: { type: iconPropType, default: ArrowRight, }- 传组件:如
:icon="CaretRight",Element Plus 内置的图标组件可以直接复用; - 传字符串:可以传入一个渲染为图标的字符串名称,组件内部通过
<component :is="icon" />动态渲染。
在模板中,#icon插槽同样提供isActive作用域参数(见 collapse-item.vue):
<slot name="icon" :is-active="isActive"> <el-icon :class="arrowKls"> <component :is="icon" /> </el-icon> </slot>默认情况下,图标被包裹在<el-icon>中并应用el-collapse-item__arrow类;激活状态下面板会加上is-active修饰类,图标随之旋转(旋转样式由主题样式控制)。
六、自定义展开图标位置(Custom icon position)
自版本^(2.9.10)起,Collapse 提供了expand-icon-position属性,用于设置展开图标的水平位置,可选值为'left' | 'right',默认'right':
<template> <div class="demo-collapse-position"> <div class="flex items-center mb-4"> <span class="mr-4">expand icon position: </span> <el-switch v-model="position" inactive-value="left" active-value="right" inactive-text="left" active-text="right" /> </div> <el-collapse :expand-icon-position="position"> <el-collapse-item title="Consistency" name="1"> <div>...</div> </el-collapse-item> </el-collapse> </div> </template> <script lang="ts" setup> import { ref } from 'vue' import type { CollapseIconPositionType } from 'element-plus' const position = ref<CollapseIconPositionType>('left') </script>完整示例见 custom-icon-position.vue。
源码中的实现方式
该属性在 collapse.ts 中定义,默认值为'right';在 use-collapse.ts 中,它被转换为容器根节点的修饰类:
export const useCollapseDOM = (props: CollapseProps) => { const ns = useNamespace('collapse') const rootKls = computed(() => [ ns.b(), ns.b(`icon-position-${props.expandIconPosition}`), ]) return { rootKls } }也就是说,根元素会得到el-collapse与el-collapse-icon-position-left(或right)两个类,图标在左还是在右完全由主题样式通过该类控制,而非在组件内部做布局判断——这是一种将布局决策交给 CSS 的轻量实现思路。位置不同还会影响标题文字与图标在行内的排列顺序,实现时建议用真实页面预览确认视觉细节。
七、防止折叠/展开:before-collapse 拦截(Prevent collapsing)
自版本^(2.9.11)起,Collapse 支持before-collapse钩子:在折叠状态即将变化前调用,若返回false,或返回的Promise被 reject,则阻止本次折叠/展开操作。典型场景是:异步保存数据未完成时禁止用户收起面板。
<template> <div v-loading="loading" class="demo-collapse"> <el-collapse v-model="activeNames" :before-collapse="beforeCollapse"> <el-collapse-item title="Consistency" name="1"> <div>...</div> </el-collapse-item> <el-collapse-item title="Feedback" name="2"> <div>...</div> </el-collapse-item> </el-collapse> </div> </template> <script lang="ts" setup> import { ref } from 'vue' const before = ref(true) const activeNames = ref(['1']) const loading = ref(false) const beforeCollapse = (): Promise<boolean> => { loading.value = true return new Promise((resolve) => { setTimeout(() => { loading.value = false return resolve(before.value) }, 1000) }) } </script>完整示例见 prevent-collapsing.vue。
底层调用链解析
before-collapse的类型为(name: CollapseActiveName) => Awaitable<boolean>,即同步返回boolean或返回Promise<boolean>(见 collapse.ts)。
点击面板标题后,整个调用链如下(use-collapse-item.ts → use-collapse.ts):
- 面板的
handleHeaderClick被触发(若面板disabled则直接 return); - 调用注入的
collapse?.handleItemClick(name); handleItemClick中,若未设置beforeCollapse则直接执行handleChange;否则调用beforeCollapse(name)并校验返回值类型——必须是boolean或Promise<boolean>,否则抛出ElCollapse: beforeCollapse must return type Promise<boolean> or boolean错误(见 use-collapse.ts);- 若返回
Promise,则resolve后仅当结果为true时才执行handleChange;reject时仅输出debugWarn警告而不改变状态。
if (isPromise(shouldChange)) { shouldChange .then((result) => { if (result !== false) handleChange(name) }) .catch((e) => { debugWarn(SCOPE, `some error occurred: ${e}`) }) } else if (shouldChange) { handleChange(name) }这意味着拦截能力同时覆盖"展开"与"收起"两个方向,并且支持异步决策(例如先请求后端再决定)。
八、完整的 Collapse API 参考
以下表格完整继承自 Collapse 官方文档,并结合源码补充了类型细节与默认值来源。
Collapse Attributes
| 名称 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| model-value / v-model | 当前激活面板;手风琴模式下为string(或number),否则为array | string/array | [] |
| accordion | 是否开启手风琴模式(同一时间仅展开一个面板) | boolean | false |
| expand-icon-position ^(2.9.10) | 设置展开图标位置 | enum:'left' \| 'right' | right |
| before-collapse ^(2.9.11) | 折叠状态变化前的钩子;返回false或返回被 reject 的Promise时阻止折叠 | Function:(name) => Promise<boolean> \| boolean | — |
Collapse Events
| 名称 | 说明 | 类型 |
|---|---|---|
| change | 激活面板变化时触发;参数在手风琴模式下为string,否则为array | (activeNames: array \| string) => void |
Collapse Slots
| 名称 | 说明 | 子组件 |
|---|---|---|
| default | 自定义默认内容 | Collapse Item |
Collapse Exposes
通过模板 ref 或组件实例可访问以下方法/属性:
| 名称 | 说明 | 类型 |
|---|---|---|
| activeNames | 当前激活的面板名称 | ComputedRef<(string \| number)[]> |
| setActiveNames | 设置激活面板名称 | (activeNames: (string \| number)[]) => void |
这两个暴露项定义在 collapse.vue 的defineExpose中,可用于命令式地控制面板状态。
Collapse Item Attributes
| 名称 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| name | 面板的唯一标识 | string/number | —(不传时由组件基于注入的 id 自动生成,格式为el-collapse-id-{prefix}-{n}) |
| title | 面板标题 | string | '' |
| icon ^(2.8.3) | 面板的展开图标 | string/Component | ArrowRight |
| disabled | 是否禁用该面板 | boolean | false |
关于name的补充:源码 use-collapse-item.ts 显示,未传name时组件会使用useIdInjection生成唯一 id 作为默认名称。但实践上强烈建议为每个面板显式传name,否则 v-model 绑定将难以管理。
Collapse Item Slots
| 名称 | 说明 | 类型 |
|---|---|---|
| default | Collapse Item 的内容 | — |
| title | 面板标题内容 | { isActive: boolean } |
| icon ^(2.8.3) | 面板图标内容 | { isActive: boolean } |
Collapse Item Exposes
| 名称 | 说明 | 类型 |
|---|---|---|
| isActive | 当前面板是否激活 | ComputedRef<boolean \| undefined> |
九、源码级原理:父子通信与展开动画
1. 状态共享:provide / inject
容器通过provide(collapseContextKey, { activeNames, handleItemClick })向所有子面板注入上下文(use-collapse.ts),collapseContextKey是定义在 constants.ts 中的InjectionKey。子面板则通过inject(collapseContextKey)获取该上下文(use-collapse-item.ts)。
这种模式下,面板不直接持有状态,而是全部委托给容器统一管理,这也是多面板互斥、手风琴互斥逻辑能集中实现的前提。
2. 交互细节:可访问性与表单兼容
面板标题渲染为role="button"的可聚焦元素,支持键盘操作:
- 通过
Space或Enter键也能切换展开状态(@keydown.space.enter.stop="handleEnterClick"); - 提供
aria-expanded、aria-controls、aria-describedby等无障碍属性,内容区使用role="region"; - 标题区域内的
input、textarea、select点击不会误触面板切换(通过target.closest('input, textarea, select')判断,见 use-collapse-item.ts); disabled面板不响应点击,且不接收焦点。
3. 展开/收起动画:ElCollapseTransition
面板内容包裹在 collapse-transition 组件内,通过操作max-height实现流畅的高度过渡动画。其核心技巧是:
- 展开(enter)时:先记录原始 padding,将
max-height置 0,再通过requestAnimationFrame把max-height设置为元素的scrollHeight,从而让浏览器为高度变化补间; - 收起(leave)时:先把
max-height设为当前scrollHeight(确保过渡起点正确),再过渡到 0; - 动画结束后统一
reset清理内联样式(max-height、overflow、padding),避免影响后续布局计算。
这套动画同时被 Collapse、Drawer 等组件复用,是 Element Plus 中"高度自适应过渡"的标准实现。
十、实战建议与注意事项
- 显式声明
name:每个el-collapse-item都应设置唯一name,它是 v-model 与change事件的匹配依据;依赖自动生成的 id 会让状态管理不可控。 - 按需选择 v-model 类型:非手风琴模式绑定数组,手风琴模式绑定
string,两种模式下change事件的参数类型也随之变化(数组 / 字符串)。 - 异步拦截结合 loading:
before-collapse返回Promise时,可配合v-loading在等待期间给出视觉反馈(官方示例 prevent-collapsing.vue 即采用此模式)。 - 图标定制优先级:
#icon插槽 >icon属性 > 默认ArrowRight。插槽完全覆盖默认图标,属性仅替换图标组件而保留<el-icon>包裹与旋转动画。 - 与 Element Plus 版本对应:
icon(2.8.3)、expand-icon-position(2.9.10)、title插槽isActive参数(2.9.10)、before-collapse(2.9.11)均有版本门槛,升级组件库时注意查阅 CHANGELOG 确认可用性。 - 命名空间与主题定制:组件 DOM 类名统一基于
el-collapse命名空间(el-collapse-item__header、el-collapse-item__arrow等),如需深度定制样式,可参考主题样式源文件(theme-chalk/src)按类名覆盖。
结语
Collapse 看似简单,实则涵盖了 Element Plus 组件设计的多项核心范式:父子通过 provide/inject 共享状态、props 驱动 CSS 修饰类、before-collapse异步拦截钩子、基于max-height的高度过渡动画,以及完整的键盘与无障碍支持。希望本文从用法到源码的梳理,能帮助你在实际项目中更自信地使用 Collapse,并从中理解 Element Plus 组件的通用设计思路。
【免费下载链接】element-plus🎉 A Vue.js 3 UI Library made by Element team项目地址: https://gitcode.com/GitHub_Trending/el/element-plus
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考