Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Getting maximum call stack size exceeded for Storybook with Angular with FormGroup #13238

Closed
altbdoor opened this issue Nov 24, 2020 · 7 comments

Comments

@altbdoor
Copy link

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:

  1. Clone the repository https://github.com/altbdoor/storybook-angular-formgroup
  2. Run npm run storybook
  3. Get an error on Maximum call stack size exceeded

Expected 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
@vincentpalita
Copy link

vincentpalita commented Nov 24, 2020

@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.

@shilman
Copy link
Member

shilman commented Nov 25, 2020

Possible dupe to #13242

@Marklb
Copy link
Member

Marklb commented Nov 25, 2020

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' }
  }
}

@stale
Copy link

stale bot commented Dec 25, 2020

Hi everyone! Seems like there hasn't been much going on in this issue lately. If there are still questions, comments, or bugs, please feel free to continue the discussion. Unfortunately, we don't have time to get to every issue. We are always open to contributions so please send us a pull request if you would like to help. Inactive issues will be closed after 30 days. Thanks!

@stale stale bot added the inactive label Dec 25, 2020
@telrock-cuzn
Copy link

@vincentpalita I tried your code :

import { FakeFormBuilder } from './fake-form-builder'
iimport { FormControl } from '@angular/forms'

const formBuilder = new FakeFormBuilder.group({
  address: new FormControl('New York')
})

but it did not have the 'group' available from the class file.
Why does this not work for me?

Screenshot 2021-07-13 at 08 14 56

This is the code:
Screenshot 2021-07-13 at 08 17 22

@stale stale bot removed the inactive label Jul 13, 2021
@telrock-cuzn
Copy link

Stoybook is not much use to Angular developers if it will not work with FormGroups, FormArrays & FormControls.

@valentinpalkovic
Copy link
Contributor

This issue should be solvable using mapped args. I am closing this for now because I cannot reproduce the problem. Please feel free to reopen it with a reproduction for Storybook 7 (https://storybook.new).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants