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
相关产品推荐
相关产品推荐

