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

Storybook Angular中如何为不同story传递不同FormControl

问题原因

Storybook 默认会对所有 args 配置项做 JSON 序列化,用于 URL 参数同步、Controls 面板状态持久化。Angular 的 FormControl/FormGroup 实例内部存在循环引用(子控件持有父表单组的引用,父表单组又反向持有子控件的引用),直接把这类实例放到 args 中,序列化时就会触发 Error: Converting circular structure to JSON 报错。

实现方案

核心思路是把带循环引用的表单实例从静态 args 配置中剥离,通过标识匹配的方式在 Template 渲染阶段动态注入,同时配置 Storybook 跳过对这类复杂属性的序列化,既支持不同 Story 传入独立 FormControl,也支持同 Story 传入多个 FormControl。

完整实现代码

import { moduleMetadata, Meta, Story } from '@storybook/angular';
import { CommonModule } from '@angular/common';
import {
  FormControl,
  FormsModule,
  ReactiveFormsModule,
  FormGroup,
  Validators
} from '@angular/forms';
import { MyRadiobuttonComponent } from '../../../projects/my-angular/src/lib/radiobutton/radiobutton.component';

// 顶层预定义所有场景需要的表单组、FormControl实例,不要放入args
const formCollection = {
  singleSelect: new FormGroup({
    checked: new FormControl('value1'),
    unchecked: new FormControl('')
  }),
  requiredSelect: new FormGroup({
    required: new FormControl('', [Validators.required])
  }),
  multiSelectGroup: new FormGroup({
    opt1: new FormControl(''),
    opt2: new FormControl('opt2'), // 默认选中第二项
    opt3: new FormControl('')
  })
}

export default {
  title: 'Angular Component Library/Radiobutton',
  component: MyRadiobuttonComponent,
  decorators: [
    moduleMetadata({
      declarations: [MyRadiobuttonComponent],
      imports: [CommonModule, FormsModule, ReactiveFormsModule],
    }),
  ],
  // 关键配置:标记表单相关属性不需要序列化、不绑定Controls面板
  argTypes: {
    ctrl: { control: false, table: { disable: true } },
    parentFormGroup: { control: false, table: { disable: true } }
  }
} as Meta;

// 单控件通用模板:通过formKey、controlKey匹配对应表单实例
const SingleRadioTemplate: Story = (args) => {
  const targetForm = formCollection[args.formKey as keyof typeof formCollection];
  const targetControl = targetForm.get(args.controlKey as string);
  return {
    props: {
      ...args,
      parentFormGroup: targetForm,
      ctrl: targetControl
    }
  }
};

// 选中状态单选
export const Selected = SingleRadioTemplate.bind({});
Selected.args = {
  formKey: 'singleSelect',
  controlKey: 'checked',
  id: 'radio-checked',
  isRequired: false,
  isInline: false,
  label: 'Selected Radio',
  value: 'value1',
  name: 'single-group'
};

// 未选中状态单选
export const Unselected = SingleRadioTemplate.bind({});
Unselected.args = {
  formKey: 'singleSelect',
  controlKey: 'unchecked',
  id: 'radio-unchecked',
  isRequired: false,
  isInline: false,
  label: 'Unselected Radio',
  value: 'value2',
  name: 'single-group'
};

// 多单选同组模板:支持同时传入多个FormControl
const MultiRadioTemplate: Story = (args) => {
  const targetForm = formCollection[args.formKey as keyof typeof formCollection];
  return {
    props: {
      ...args,
      parentFormGroup: targetForm,
      optionList: [
        { key: 'opt1', id: 'opt-1', label: 'Option 1', value: 'opt1' },
        { key: 'opt2', id: 'opt-2', label: 'Option 2', value: 'opt2' },
        { key: 'opt3', id: 'opt-3', label: 'Option 3', value: 'opt3' }
      ]
    },
    template: `
      <div [formGroup]="parentFormGroup">
        <my-radiobutton
          *ngFor="let opt of optionList"
          [id]="opt.id"
          [label]="opt.label"
          [value]="opt.value"
          [ctrl]="parentFormGroup.get(opt.key)"
          [isRequired]="isRequired"
          [isInline]="isInline"
          [name]="groupName"
        ></my-radiobutton>
      </div>
    `
  }
};

// 多单选同组场景
export const InlineRadioGroup = MultiRadioTemplate.bind({});
InlineRadioGroup.args = {
  formKey: 'multiSelectGroup',
  isRequired: true,
  isInline: true,
  groupName: 'inline-group'
};

配置说明

  • 所有表单实例统一在文件顶层定义,和静态args完全隔离,从源头避免被序列化
  • argTypes中给ctrl、parentFormGroup配置control: false,告诉Storybook不需要对这两个属性做序列化、不需要在Controls面板生成编辑控件,彻底解决循环引用报错
  • 不同Story只需要传入不同的formKey、controlKey字符串标识,就可以匹配到完全独立的FormControl实例,状态互不干扰
  • 多控件场景直接在Story配置中写内联模板,遍历渲染多个绑定不同FormControl的组件,不需要额外封装包装组件

额外注意:原组件模板中[name]="ctrl"的写法存在问题,单选按钮同组的name属性需要是统一的字符串值,绑定FormControl实例会导致同组单选无法互斥,建议给组件增加@Input() name: string输入项传入同组名称。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 12:15:44