Angular使用ng-content时如何跨组件通信实现卡片折叠功能
Angular 卡片组件折叠功能实现方案
你提到的全局共享服务多实例冲突问题是真实存在的:根层级注入的单例服务无法区分页面上不同卡片实例的状态,很容易出现状态串扰,不适合这个场景。下面3种方案都可以把折叠逻辑收敛在组件内部,不需要业务侧写多余的状态管理代码,且天然支持多实例隔离:
方案1:组件层级注入服务(优先推荐)
核心逻辑是利用Angular依赖注入的层级特性,让每个卡片容器实例持有独立的服务状态,从根本上避免多实例冲突
- 先创建一个轻量的折叠状态服务,只负责维护单卡片的折叠状态和变更通知:
import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; @Injectable() export class CardCollapseService { private collapsedState = new BehaviorSubject<boolean>(false); readonly state$ = this.collapsedState.asObservable(); toggle() { this.collapsedState.next(!this.collapsedState.value); } setInitialState(state: boolean) { this.collapsedState.next(state); } }
- 在
card-container组件的装饰器里注册这个服务,同时添加控制折叠能力的输入属性:
@Component({ selector: 'app-card-container', templateUrl: './card-container.component.html', providers: [CardCollapseService] // 关键:每个容器实例生成独立的服务实例 }) export class CardContainerComponent { @Input() collapsible = false; @Input() defaultCollapsed = false; constructor(private collapseService: CardCollapseService) {} ngOnInit() { if (this.defaultCollapsed) { this.collapseService.setInitialState(true); } } }
对应的容器模板不需要做任何改动,保持原有结构即可。
- 修改
card-title组件,注入父容器提供的折叠服务,开启折叠能力时点击标题触发状态切换:
@Component({ selector: 'app-card-title', templateUrl: './card-title.component.html' }) export class CardTitleComponent { isCollapsed = false; constructor( @Optional() private collapseService: CardCollapseService, private parentContainer: CardContainerComponent ) {} ngOnInit() { if (this.parentContainer.collapsible && this.collapseService) { this.collapseService.state$.subscribe(state => this.isCollapsed = state); } } handleToggle() { this.collapseService?.toggle(); } }
title模板增加点击事件和状态样式绑定:
<h4 class="cardTitle" [class.collapsible]="parentContainer.collapsible" [class.collapsed]="isCollapsed" (click)="parentContainer.collapsible && handleToggle()" translate > <ng-content></ng-content> <span class="collapse-indicator" *ngIf="parentContainer.collapsible"></span> </h4>
- 修改
card-body组件,同样注入折叠服务,订阅状态控制折叠样式:
@Component({ selector: 'app-card-body', templateUrl: './card-body.component.html' }) export class CardBodyComponent { isCollapsed = false; constructor(@Optional() private collapseService: CardCollapseService) {} ngOnInit() { this.collapseService?.state$.subscribe(state => this.isCollapsed = state); } }
body模板增加折叠类绑定:
<div class="cardBody" [class.collapsed]="isCollapsed"> <ng-content></ng-content> </div>
最后补充对应样式即可实现平滑折叠效果:
.cardTitle.collapsible { cursor: pointer; display: flex; align-items: center; justify-content: space-between; } .collapse-indicator { width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 6px solid currentColor; transition: transform 0.25s ease; } .cardTitle.collapsed .collapse-indicator { transform: rotate(-90deg); } .cardBody { transition: height 0.25s ease, padding 0.25s ease; overflow: hidden; } .cardBody.collapsed { height: 0 !important; padding-top: 0 !important; padding-bottom: 0 !important; }
业务侧使用时完全不需要改动原有基础结构,只需要在需要折叠能力的卡片上加输入属性:
<app-card-container [collapsible]="true" [defaultCollapsed]="false"> <app-card-title>可折叠卡片标题</app-card-title> <app-card-body>卡片内容</app-card-body> </app-card-container>
方案2:ContentChild直接操作子组件实例
如果不想额外创建服务,可以直接在父容器里获取投影的子组件实例,直接操作子组件的状态属性:
- 给
card-title组件添加点击事件的输出器,给card-title和card-body都添加isCollapsed公共属性 - 在
card-container里通过@ContentChild获取title和body的实例,订阅title的点击事件,切换状态后直接修改两个子组件的isCollapsed属性即可
// card-container 核心逻辑 export class CardContainerComponent { @Input() collapsible = false; @Input() defaultCollapsed = false; @ContentChild(CardTitleComponent) titleComp!: CardTitleComponent; @ContentChild(CardBodyComponent) bodyComp!: CardBodyComponent; ngAfterContentInit() { if (!this.collapsible) return; this.titleComp.isCollapsed = this.defaultCollapsed; this.bodyComp.isCollapsed = this.defaultCollapsed; this.titleComp.titleClick.subscribe(() => { const newState = !this.titleComp.isCollapsed; this.titleComp.isCollapsed = newState; this.bodyComp.isCollapsed = newState; }) } }
这个方案代码量更少,但组件间耦合度比层级注入方案高,适合组件结构固定、后续不会扩展额外卡片子部件的场景。
方案3:结构指令控制(不推荐)
也可以封装一个折叠结构指令,直接控制body的渲染/高度,但这个方案需要业务侧在使用时手动绑定事件和状态,折叠逻辑会泄漏到业务模板中,不符合逻辑收敛的需求,只适合临时快速实现的场景。
内容的提问来源于stack exchange,提问作者Peter Boomsma
相关产品推荐
相关产品推荐

