You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 00:39:19