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

自定义*ngIf结构指令:点击面板显示其主体并隐藏其他面板主体

嘿,我来帮你搞定这个可复用面板的折叠/展开逻辑,同时满足你自定义结构指令的需求!下面给你两种方案,你可以根据场景灵活选择:

方案1:父组件统一管理激活状态(简单直接)

这种方式不需要自定义指令,通过父组件跟踪当前打开的面板索引,子面板根据这个索引判断是否显示内容,是最常用的实现方式。

子面板组件(panel.component.ts)

import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-panel',
  template: `
    <div class="panel panel-default" (click)="onPanelClicked()">
      <div class="panel-heading">
        <span class="glyphicon glyphicon-chevron-{{ opened ? 'down' : 'right' }} chevronIcon"></span>
        <h3 class="panel-title">{{ header }}</h3>
      </div>
      <div class="panel-body" *ngIf="opened">
        <ng-content></ng-content>
      </div>
    </div>
  `,
  styles: [`
    .panel-heading { cursor: pointer; }
    .chevronIcon { margin-right: 8px; }
  `]
})
export class PanelComponent {
  @Input() header!: string;
  @Input() index!: number;
  @Input() activeIndex!: number;
  @Output() panelClicked = new EventEmitter<number>();

  // 通过计算属性判断当前面板是否激活
  get opened(): boolean {
    return this.activeIndex === this.index;
  }

  onPanelClicked(): void {
    this.panelClicked.emit(this.index);
  }
}

父组件(比如app.component.ts)

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <app-panel 
      *ngFor="let panel of panels; let i = index"
      [header]="panel.header"
      [index]="i"
      [activeIndex]="currentActiveIndex"
      (panelClicked)="setActiveIndex(i)"
    >
      {{ panel.content }}
    </app-panel>
  `
})
export class AppComponent {
  currentActiveIndex = 0; // 默认打开第一个面板
  panels = [
    { header: '面板1', content: '这是面板1的唯一内容' },
    { header: '面板2', content: '这是面板2的专属内容' },
    { header: '面板3', content: '这是面板3的独有内容' }
  ];

  setActiveIndex(index: number): void {
    // 点击已打开的面板可以切换折叠/展开
    this.currentActiveIndex = this.currentActiveIndex === index ? -1 : index;
  }
}

方案2:自定义结构指令(满足你的*ngIf自定义需求)

如果你确实需要通过自定义结构指令来控制显示逻辑,我们可以创建一个*panelBody指令,配合全局状态服务来管理所有面板的显示/隐藏。

第一步:创建状态服务(panel-state.service.ts)

用来全局跟踪当前激活的面板ID:

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class PanelStateService {
  private _activePanelId = new BehaviorSubject<string | null>(null);
  activePanelId = this._activePanelId.asObservable();

  setActivePanel(id: string): void {
    // 点击同一个面板可以切换折叠/展开状态
    const currentId = this._activePanelId.value;
    this._activePanelId.next(currentId === id ? null : id);
  }
}

第二步:自定义结构指令(panel-body.directive.ts)

这个指令会根据全局激活状态来决定是否渲染面板内容:

import { Directive, Input, TemplateRef, ViewContainerRef, OnInit, OnDestroy } from '@angular/core';
import { PanelStateService } from './panel-state.service';
import { Subscription } from 'rxjs';

@Directive({
  selector: '[panelBody]'
})
export class PanelBodyDirective implements OnInit, OnDestroy {
  @Input() set panelBody(panelId: string) {
    this._panelId = panelId;
    this.updateView();
  }

  private _panelId!: string;
  private subscription!: Subscription;

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef,
    private panelStateService: PanelStateService
  ) {}

  ngOnInit(): void {
    // 监听激活状态变化,实时更新视图
    this.subscription = this.panelStateService.activePanelId.subscribe(() => {
      this.updateView();
    });
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }

  private updateView(): void {
    this.panelStateService.activePanelId.subscribe(activeId => {
      const isActive = activeId === this._panelId;
      if (isActive) {
        this.viewContainer.createEmbeddedView(this.templateRef);
      } else {
        this.viewContainer.clear();
      }
    });
  }
}

第三步:更新面板组件

import { Component, Input } from '@angular/core';
import { PanelStateService } from './panel-state.service';

@Component({
  selector: 'app-panel',
  template: `
    <div class="panel panel-default" (click)="togglePanel()">
      <div class="panel-heading">
        <span class="glyphicon glyphicon-chevron-{{ isActive ? 'down' : 'right' }} chevronIcon"></span>
        <h3 class="panel-title">{{ header }}</h3>
      </div>
      <!-- 使用自定义指令替代*ngIf -->
      <ng-template [panelBody]="panelId">
        <div class="panel-body">
          <ng-content></ng-content>
        </div>
      </ng-template>
    </div>
  `,
  styles: [`
    .panel-heading { cursor: pointer; }
    .chevronIcon { margin-right: 8px; }
  `]
})
export class PanelComponent {
  @Input() header!: string;
  @Input() panelId!: string; // 每个面板必须有唯一ID

  constructor(private panelStateService: PanelStateService) {}

  get isActive(): boolean {
    let active = false;
    this.panelStateService.activePanelId.subscribe(id => {
      active = id === this.panelId;
    });
    return active;
  }

  togglePanel(): void {
    this.panelStateService.setActivePanel(this.panelId);
  }
}

第四步:父组件使用示例

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <app-panel 
      header="面板1"
      panelId="unique-panel-1"
    >
      这是面板1的唯一内容
    </app-panel>
    <app-panel 
      header="面板2"
      panelId="unique-panel-2"
    >
      这是面板2的专属内容
    </app-panel>
    <app-panel 
      header="面板3"
      panelId="unique-panel-3"
    >
      这是面板3的独有内容
    </app-panel>
  `
})
export class AppComponent {}

两种方案对比

  • 方案1更轻量,不需要额外服务,适合简单的面板组场景;
  • 方案2通过自定义指令实现了类似*ngIf的控制逻辑,更灵活,适合需要全局控制或者复杂交互的场景。

内容的提问来源于stack exchange,提问作者Yash Chitneni

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.25 04:18:27