Stencil组件自嵌套实现动态手风琴,事件监听触发两次求指导
Stencil动态手风琴事件重复触发问题及优化实现
问题原因
你遇到的事件触发两次的核心问题是事件冒泡:内层自嵌套的子组件触发事件后,事件会沿着DOM树向上传递,外层的同类型组件的事件监听器也会捕获到该事件,从而导致重复触发。
自嵌套场景的临时解决方法
如果一定要保留自嵌套的实现方式,可通过阻止事件冒泡来解决:
- 在事件触发时调用
stopPropagation():
// 子组件内的事件触发函数 triggerEvent = () => { const event = new CustomEvent('item-toggle', { detail: { id: this.id } }); event.stopPropagation(); // 阻止事件向上冒泡 this.el.dispatchEvent(event); };
- 或者在监听事件时处理:
// 父组件(同组件)内的监听逻辑 <my-accordion-item onItemToggle={(e) => { e.stopPropagation(); // 你的业务逻辑 }}>
更优的Stencil动态手风琴实现方案
推荐采用容器组件+子项组件的拆分结构,避免自嵌套带来的事件干扰和状态管理混乱,逻辑更清晰易维护:
1. 容器组件:my-accordion
import { Component, h, Prop, State } from '@stencil/core'; @Component({ tag: 'my-accordion', styleUrl: 'accordion.css', shadow: true, }) export class Accordion { // 控制是否允许同时展开多个项 @Prop() singleOpen: boolean = false; // 存储已展开项的ID列表 @State() expandedIds: string[] = []; handleItemToggle = (event: CustomEvent<{ id: string }>) => { const targetId = event.detail.id; let updatedExpandedIds = [...this.expandedIds]; if (updatedExpandedIds.includes(targetId)) { // 收起当前项 updatedExpandedIds = updatedExpandedIds.filter(id => id !== targetId); } else { // 展开当前项,若开启singleOpen则关闭其他项 updatedExpandedIds = this.singleOpen ? [targetId] : [...updatedExpandedIds, targetId]; } this.expandedIds = updatedExpandedIds; }; render() { return ( <div class="accordion-container"> <slot onAccordionItemToggle={this.handleItemToggle} /> </div> ); } }
2. 子项组件:my-accordion-item
import { Component, h, Prop, State, Event, EventEmitter, Element } from '@stencil/core'; @Component({ tag: 'my-accordion-item', styleUrl: 'accordion-item.css', shadow: true, }) export class AccordionItem { @Element() el: HTMLElement; @Prop() id: string; @Prop() initialExpanded: boolean = false; @State() isExpanded: boolean; // 向外触发的切换事件 @Event() accordionItemToggle: EventEmitter<{ id: string }>; componentWillLoad() { this.isExpanded = this.initialExpanded; } toggleItem = () => { this.isExpanded = !this.isExpanded; this.accordionItemToggle.emit({ id: this.id }); }; render() { return ( <div class="accordion-item"> <div class={`accordion-header ${this.isExpanded ? 'expanded' : ''}`} onClick={this.toggleItem} > <slot name="header"></slot> <span class="toggle-icon">{this.isExpanded ? '−' : '+'}</span> </div> {this.isExpanded && ( <div class="accordion-content"> <slot name="content"></slot> </div> )} </div> ); } }
3. 样式文件
accordion.css
.accordion-container { border: 1px solid #e5e7eb; border-radius: 6px; overflow: hidden; }
accordion-item.css
.accordion-item { border-bottom: 1px solid #e5e7eb; } .accordion-item:last-child { border-bottom: none; } .accordion-header { display: flex; justify-content: space-between; align-items: center; padding: 1rem 1.25rem; background-color: #f9fafb; cursor: pointer; transition: background-color 0.2s ease; } .accordion-header.expanded { background-color: #f3f4f6; } .accordion-content { padding: 1rem 1.25rem; background-color: #ffffff; } .toggle-icon { font-size: 1.25rem; font-weight: 600; color: #6b7280; }
4. 使用示例
<my-accordion singleOpen={true}> <my-accordion-item id="item-1" initialExpanded={true}> <div slot="header">基础设置</div> <div slot="content">这里可以放置基础设置的相关内容,比如账号信息、语言偏好等。</div> </my-accordion-item> <my-accordion-item id="item-2"> <div slot="header">通知设置</div> <div slot="content">配置各类通知的接收方式,包括邮件、短信、站内信等。</div> </my-accordion-item> <my-accordion-item id="item-3"> <div slot="header">隐私设置</div> <div slot="content">管理个人数据的可见范围,控制第三方应用的权限访问。</div> </my-accordion-item> </my-accordion>
这种拆分方式将状态管理集中在容器组件,子项仅负责自身的UI渲染和事件触发,避免了自嵌套带来的事件冒泡问题,同时扩展性更强,支持多级别嵌套、自定义样式等需求。
内容的提问来源于stack exchange,提问作者surya
相关产品推荐
相关产品推荐

