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

Angular+Storybook:通用父组件监听子组件事件及状态问题

Angular Storybook 通用状态监听TestWrapper实现方案

针对你在Angular+Storybook环境中封装TestWrapper监听子组件状态的问题,以下是几个可行的通用实现方案:

方案一:基于统一接口的侵入式监听(推荐,稳定可控)

通过定义统一状态接口,让子组件实现该接口后,TestWrapper可通过内容投影API获取并监听子组件状态。

  1. 定义状态接口
export interface HasState {
  stateChange: EventEmitter<any>;
  getCurrentState(): any;
}
  1. 实现TestWrapper组件
import { Component, ContentChildren, QueryList, AfterContentInit } from '@angular/core';
import { HasState } from './has-state.interface';

@Component({
  selector: 'test-wrapper',
  template: `
    <ng-content></ng-content>
    <textarea [value]="currentStateJson" readonly style="margin-top: 1rem; width: 100%; height: 150px;"></textarea>
  `
})
export class TestWrapperComponent implements AfterContentInit {
  @ContentChildren(HasState, { descendants: true }) childComponents!: QueryList<HasState>;
  currentStateJson = '';

  ngAfterContentInit() {
    // 初始化监听已加载的子组件
    this.bindStateListeners(this.childComponents);
    
    // 监听子组件列表动态变化
    this.childComponents.changes.subscribe(() => {
      this.bindStateListeners(this.childComponents);
    });
  }

  private bindStateListeners(components: QueryList<HasState>) {
    components.forEach(child => {
      // 初始化获取一次状态
      this.currentStateJson = JSON.stringify(child.getCurrentState(), null, 2);
      // 订阅状态变更事件
      child.stateChange.subscribe(state => {
        this.currentStateJson = JSON.stringify(state, null, 2);
      });
    });
  }
}
  1. 子组件实现接口
import { Component, EventEmitter, Output } from '@angular/core';
import { HasState } from './has-state.interface';

@Component({
  selector: 'my-custom-component',
  template: `<!-- 组件模板 -->`
})
export class MyCustomComponent implements HasState {
  @Output() stateChange = new EventEmitter<any>();
  private internalState = { isDisabled: false, value: '' };

  // 组件内部状态更新时触发事件
  updateState(updates: Partial<typeof this.internalState>) {
    this.internalState = { ...this.internalState, ...updates };
    this.stateChange.emit(this.internalState);
  }

  getCurrentState() {
    return { ...this.internalState };
  }
}
  1. Storybook中使用
export default {
  title: 'Components/MyCustomComponent',
  component: MyCustomComponent,
  decorators: [
    () => ({
      template: `<test-wrapper><my-custom-component></my-custom-component></test-wrapper>`
    })
  ]
};

export const Default = {};

方案二:无侵入式监听(适配无法修改子组件的场景)

如果无法修改子组件代码,可通过Angular内部上下文获取组件实例,结合轮询或变更检测监听状态变化。

import { Component, ContentChildren, QueryList, AfterContentInit, ElementRef, OnDestroy } from '@angular/core';

@Component({
  selector: 'test-wrapper',
  template: `
    <ng-content></ng-content>
    <textarea [value]="currentStateJson" readonly style="margin-top: 1rem; width: 100%; height: 150px;"></textarea>
  `
})
export class TestWrapperComponent implements AfterContentInit, OnDestroy {
  @ContentChildren(ElementRef, { descendants: true }) childElements!: QueryList<ElementRef>;
  currentStateJson = '';
  private mutationObserver: MutationObserver | null = null;
  private pollIntervals: number[] = [];

  ngAfterContentInit() {
    // 初始化监听现有子组件
    this.childElements.forEach(elRef => this.monitorComponent(elRef.nativeElement));
    
    // 监听DOM变化,处理动态添加的组件
    this.mutationObserver = new MutationObserver(mutations => {
      mutations.forEach(mutation => {
        mutation.addedNodes.forEach(node => {
          if (node instanceof HTMLElement) {
            this.monitorComponent(node);
          }
        });
      });
    });
    this.mutationObserver.observe(document.body, { childList: true, subtree: true });
  }

  ngOnDestroy() {
    this.mutationObserver?.disconnect();
    this.pollIntervals.forEach(interval => clearInterval(interval));
  }

  private monitorComponent(element: HTMLElement) {
    // 获取Angular组件实例(依赖内部__ngContext__属性,需适配Angular版本)
    const instance = this.getComponentInstance(element);
    if (!instance) return;

    let prevState = JSON.stringify(this.extractState(instance));
    // 轮询检测状态变化
    const interval = setInterval(() => {
      const currentState = JSON.stringify(this.extractState(instance));
      if (currentState !== prevState) {
        prevState = currentState;
        this.currentStateJson = currentState;
      }
    }, 100);
    this.pollIntervals.push(interval);

    // 额外监听组件输出事件
    Object.keys(instance).forEach(key => {
      if (instance[key] instanceof EventEmitter) {
        instance[key].subscribe(() => {
          this.currentStateJson = JSON.stringify(this.extractState(instance), null, 2);
        });
      }
    });
  }

  private getComponentInstance(element: HTMLElement): any {
    const ngContext = (element as any).__ngContext__;
    if (!ngContext) return null;
    const componentDef = ngContext.find(item => item?.componentDef);
    return componentDef?.componentDef.instance || null;
  }

  private extractState(instance: any): any {
    // 过滤掉Angular内部属性,只保留组件自定义状态
    return Object.fromEntries(
      Object.entries(instance).filter(([key]) => !key.startsWith('_') && typeof instance[key] !== 'function')
    );
  }
}

方案三:Storybook全局装饰器(批量复用)

通过Storybook全局装饰器,让所有组件故事自动被TestWrapper包裹,无需手动添加:

  1. 编写全局装饰器
import { moduleMetadata } from '@storybook/angular';
import { TestWrapperComponent } from './test-wrapper.component';

export const StateMonitoringDecorator = moduleMetadata({
  declarations: [TestWrapperComponent],
}).decorator((storyFn) => {
  const originalStory = storyFn();
  return {
    ...originalStory,
    template: `<test-wrapper>${originalStory.template}</test-wrapper>`
  };
});
  1. 在.storybook/preview.js中注册
import { StateMonitoringDecorator } from '../src/app/test-wrapper/state-monitoring.decorator';

export const decorators = [StateMonitoringDecorator];

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 13:50:42