Describe the bug
Getting Maximum call stack size exceeded for Storybook with Angular, when passing in a FormGroup into Template.args
To Reproduce
Steps to reproduce the behavior:
npm run storybookMaximum call stack size exceededExpected behavior
Should not get an error, and Storybook gets rendered normally
Code snippets
Relevant code is in 0-child-component.stories.ts in the repo, but the gist of it is:
const fb = new FormBuilder();
const formObj = fb.group({ option: '' })
const Template: Story<ChildFormComponent> = (args: ChildFormComponent) => ({
component: ChildFormComponent,
moduleMetadata: {
imports: [CommonModule, FormsModule, ReactiveFormsModule]
}
})
// this will break
export const SampleBroken = Template.bind({})
SampleBroken.args = {
parentForm: formObj
}
// this does not break
export const SampleWorking: Story<ChildFormComponent> = (args: ChildFormComponent) => ({
component: ChildFormComponent,
moduleMetadata: {
imports: [CommonModule, FormsModule, ReactiveFormsModule],
},
props: {
parentForm: formObj
}
})
System
Environment Info:
System:
OS: Windows 10 10.0.19041
CPU: (16) x64 Intel(R) Core(TM) i7-10875H CPU @ 2.30GHz
Binaries:
Node: 12.19.1 - ~\Downloads\exe\nodejs\node-v12.19.1-win-x64\node.EXE
Yarn: 1.22.10 - ~\Downloads\exe\nodejs\node-v12.19.1-win-x64\yarn.CMD
npm: 6.14.8 - ~\Downloads\exe\nodejs\node-v12.19.1-win-x64\npm.CMD
Browsers:
Edge: Spartan (44.19041.1.0), Chromium (87.0.664.41)
npmPackages:
@storybook/addon-actions: ^6.1.4 => 6.1.4
@storybook/addon-essentials: ^6.1.4 => 6.1.4
@storybook/addon-links: ^6.1.4 => 6.1.4
@storybook/angular: ^6.1.4 => 6.1.4
Additional context
Angular CLI: 11.0.2
Node: 12.19.1
OS: win32 x64
Angular: 11.0.2
... animations, cli, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router
Ivy Workspace: Yes
Package Version
---------------------------------------------------------
@angular-devkit/architect 0.1100.2
@angular-devkit/build-angular 0.1100.2
@angular-devkit/core 11.0.2
@angular-devkit/schematics 11.0.2
@schematics/angular 11.0.2
@schematics/update 0.1100.2
rxjs 6.6.3
typescript 4.0.5
@altbdoor I am facing the same issue.
As of now, I replaced FormBuilder by a Fake class, that works with FormControls from Angular.
import { FormBuilder, FormControl, FormGroup } from '@angular/forms'
export class FakeFormBuilder extends FormBuilder {
private _group: any
private makeGroup(group: any) {
const groupWithFormControls = Object.keys(group).reduce((acc, key) => {
if(group[key] instanceof FormControl || group[key] instanceof FormGroup) {
acc[key] = group[key]
} else {
acc[key] = new FormControl(group[key])
}
return acc
}, {})
return {
...groupWithFormControls,
getRawValue: () => {
return Object.keys(this._group).reduce((acc, key) => {
if (!['getRawValue', 'reset', 'markAsDirty', 'get', 'patchValue'].includes(key)) {
acc[key] = this._group.get(key)?.value
}
return acc
}, {})
},
reset: () => {
Object.keys(this._group.getRawValue()).forEach((key) => {
this._group.get(key)?.patchValue(null)
})
},
markAsDirty: () => {},
get: (control: string) => {
return this._group[control]
},
patchValue: (value) => {
Object.keys(this._group.getRawValue()).forEach((key) => {
if (value && value[key]) {
this._group.get(key)?.patchValue(value[key])
}
})
},
}
}
group(group: any): FormGroup {
this._group = this.makeGroup(group)
return this._group
}
}
Then in your story you can call it like this:
import { FakeFormBuilder } from './fake-form-builder'
import { FormControl } from '@angular/forms'
const formBuilder = new FakeFormBuilder.group({
address: new FormControl('New York')
})
I added some of the methods of the real FormGroup group and if you pass a FormControl you still get all the methods from FormControl.
I don't have the time right now to dig in why the FormBuilder/FormGroup cannot be invoked in storybook so this fake class can make your story work as of now.
Possible dupe to https://github.com/storybookjs/storybook/issues/13242
I haven't looked into why you are getting Maximum call stack size exceeded, but FormGroup shouldn't work in args currently. Args are serialized to a string for sending to the manager through the channel and deserialized back to an object when passed to the storyFn. So, you have to set the object in props, since they don't get serialized.
If the Maximum call stack size exceeded is happening from serialization, then I think @shilman is right and #13263 will hopefully fix it.
The formObj results in a FormGroup that contains a FormControl and a FormControl has a reference to it's FormGroup, so I think that would be a cycle.
I have been waiting on custom controls(#11486) to come up with a way to handle this case, but only common things like Angular's form controls would probably be considered as possible built-in controls, so if someone has a clean way of doing this without waiting on Storybook to support custom controls that would be nice.
The following is a mess, but I will briefly describe the solution I used in my POC FormControl control, in case it helps someone come up with a proper solution.
Since the storyFn is called each time props change, I used storyFormControl to update the FormControl object from args that I was editing from a control in the ArgsTable. I didn't want to keep creating new FormControl instances, since they have references to each other in a FormGroup or FormArray, so I used _mbStoryTmp as my way of maintaining a reference to the same FormControl instance. There are multiple issues with how I did that, such as not deleting when going to another story or possibly repeating the id in multiple stories, but I didn't care for the POC.
// This is was not meant to be thorough, so it is missing multiple cases and could be more organized.
function storyFormControl(id: string, value?: any): FormControl {
const doc = document as any
// NOTE: This would probably be a memory leak, since I never clean up the globally stored objects.
if (!doc._mbStoryTmp) { doc._mbStoryTmp = {} }
if (!doc._mbStoryTmp[id]) {
doc._mbStoryTmp[id] = new FormControl(value?.value)
}
const control = doc._mbStoryTmp[id] as FormControl
if (!!control && !!value) {
if (control.value !== value.value) {
control.setValue(value.value)
}
if (control.disabled !== value.disabled) {
if (control.disabled) {
control.enable()
} else {
control.disable()
}
}
if (control.touched !== value.touched) {
if (control.touched) {
control.markAsUntouched()
} else {
control.markAsTouched()
}
}
if (control.dirty !== value.dirty) {
if (control.dirty) {
control.markAsPristine()
} else {
control.markAsDirty()
}
}
}
return control
}
export const Example = (args) => {
return {
props: {
...args,
exFormControl: storyFormControl('Example-1', args?.exFormControl)
},
template: `
<div class="d-flex flex-column">
<input type="text" [formControl]="exFormControl">
<div>
Disabled: {{ exFormControl.disabled }}
</div>
<div>
Touched: {{ exFormControl.touched }}
</div>
<div>
Dirty: {{ exFormControl.dirty }}
</div>
</div>
`
}
}
Example.argTypes = {
exFormControl: {
// NOTE: 'formControl' is not an implemented control. I had partially implemented one in a POC.
control: { type: 'formControl' }
}
}
Most helpful comment
I haven't looked into why you are getting
Maximum call stack size exceeded, butFormGroupshouldn't work in args currently. Args are serialized to a string for sending to the manager through the channel and deserialized back to an object when passed to the storyFn. So, you have to set the object in props, since they don't get serialized.If the
Maximum call stack size exceededis happening from serialization, then I think @shilman is right and #13263 will hopefully fix it.The
formObjresults in aFormGroupthat contains aFormControland aFormControlhas a reference to it'sFormGroup, so I think that would be a cycle.I have been waiting on custom controls(#11486) to come up with a way to handle this case, but only common things like Angular's form controls would probably be considered as possible built-in controls, so if someone has a clean way of doing this without waiting on Storybook to support custom controls that would be nice.
The following is a mess, but I will briefly describe the solution I used in my POC
FormControlcontrol, in case it helps someone come up with a proper solution.Since the storyFn is called each time props change, I used
storyFormControlto update theFormControlobject from args that I was editing from a control in the ArgsTable. I didn't want to keep creating newFormControlinstances, since they have references to each other in aFormGrouporFormArray, so I used_mbStoryTmpas my way of maintaining a reference to the sameFormControlinstance. There are multiple issues with how I did that, such as not deleting when going to another story or possibly repeating the id in multiple stories, but I didn't care for the POC.