Angular FormGroup in Storybook: Converting circular structure to JSON

Viewed 2373

I'm work with angular and storybook. I have a FormGroup and FormArray in my model but they are not working with storybook.

a.stories.ts ->

import { CommonModule } from '@angular/common';
import { FormArray, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { Meta, moduleMetadata, Story } from '@storybook/angular';

export default {
    title: 'Example/A',
    component: AComponent,
    decorators: [
        moduleMetadata({
            imports: [
                CommonModule,
                ReactiveFormsModule,
            ],
            providers: [],
        }),
    ],
} as Meta;

const Template: Story<AComponent> = (args: AComponent) => ({
    props: args,
});

export const Costs = Template.bind({});
Costs.args = {
    model: {
        questions: [
            {
                ...,
                "question": "lorem ipsum",
                "formGroup": new FormGroup({
                    answers: new FormArray([
                        new FormGroup({
                            "Q020_A001": new FormControl(null, [Validators.required]),
                            "Q020_A002": new FormControl(null, [Validators.required]),
                        }),
                    ]),
                }),
                "answers": [
                    {
                        ...,
                        "answerCode": "Q020_A001",
                    },
                    {
                        ...,
                        "answerCode": "Q020_A002",
                    },
                ],
            }
        ],
    },
};

I get an error in storybook ->

TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'FormGroup'
    |     property 'controls' -> object with constructor 'Object'
    |     property 'answers' -> object with constructor 'FormArray'
    --- property '_parent' closes the circle
    at JSON.stringify (<anonymous>)

It works if "formGroup" is empty. ->

            "formGroup": new FormGroup({
            }),

But if "formGroup" is not empty, it won't work. ->

        "formGroup": new FormGroup({
            asd: new FormControl(''),
        }),

How can I fix this?

2 Answers

The problem is for every args you provided to Storybook, SB gonna call JSON.stringify. This design is required because SB also use your args at runtime and allow you to change value.

In your case, you create a Reactive forms model which cause the object to be not able to convert to string by JSON.stringify.

To fix it, you need to:

  1. Provide args to SB as raw data only
  2. Change logic in your AComponent to create model from data provided above

Sample code:

/// a.stories.ts
Costs.args = {
    model: {
        questions: [
            {
                ...,
                "question": "lorem ipsum",
                "formGroup": {
                    answers: [
                        {
                            "Q020_A001": null,
                            "Q020_A002": null,
                        }),
                    ]),
                },
                "answers": [
                    {
                        ...,
                        "answerCode": "Q020_A001",
                    },
                    {
                        ...,
                        "answerCode": "Q020_A002",
                    },
                ],
            }
        ],
    },
};
/// A.component.ts

export class AComponent {
  @Input() model;

  constructor() {
     // create your reactive forms here with data from model
     this.form = new FormGroup(...)
  }
}

I solved the problem by creating a new AStoryComponent.

// a-story.component.ts

@Component({
    selector: 'a-component-story',
    template: `<a-component [model]="model"></a-component>`,
})
export class AStoryComponent implements OnInit {

    @Input() model!: any;

    constructor() {}

    ngOnInit() {
        // Converts the "formGroup" property I specified in a.stories.ts to the real formGroup object.
        convertStoryModelToRealModel(this.model);
    }

}


// a.stories.ts

import { Validators } from '@angular/forms';
import { Meta, moduleMetadata, Story } from '@storybook/angular';

export default {
    title: 'Example/A',
    component: AStoryComponent,
    decorators: [
        moduleMetadata({
            declarations: [AStoryComponent, AComponent],
            imports: [
                CommonModule,
                ReactiveFormsModule,
            ],
        }),
    ],
} as Meta;

const Template: Story<AStoryComponent> = (args: AStoryComponent) => ({
    props: args,
});

export const Costs = Template.bind({});
Costs.args = {
    model: {
        questions: [
            {
                ...,
                "question": "lorem ipsum",
                "formGroup": {
                    answers: [
                        {
                            "Q020_A001": [null, [Validators.required]],
                            "Q020_A002": [null, [Validators.required]],
                        },
                    ],
                },
                "answers": [
                    {
                        ...,
                        "answerCode": "Q020_A001",
                    },
                    {
                        ...,
                        "answerCode": "Q020_A002",
                    },
                ],
            }
        ],
    } as any,
};
Related