RangeError: Maximum call stack size exceeded when using valueChanges.subscribe
Asked Answered
A

7

58

I am using Angular 5 with Reactive forms and need to make use of the valueChanges in order to disable required validation dynamically

component class:

export class UserEditor implements OnInit {

    public userForm: FormGroup;
    userName: FormControl;
    firstName: FormControl;
    lastName: FormControl;
    email: FormControl;
    loginTypeId: FormControl;
    password: FormControl;
    confirmPassword: FormControl;
...

ngOnInit() {
    this.createFormControls();
    this.createForm();
    this.userForm.get('loginTypeId').valueChanges.subscribe(

            (loginTypeId: string) => {
                console.log("log this!");
                if (loginTypeId === "1") {
                    console.log("disable validators");
                    Validators.pattern('^[0-9]{5}(?:-[0-9]{4})?$')]);
                    this.userForm.get('password').setValidators([]);
                    this.userForm.get('confirmPassword').setValidators([]);

                } else if (loginTypeId === '2') {
                    console.log("enable validators");
                    this.userForm.get('password').setValidators([Validators.required, Validators.minLength(8)]);
                    this.userForm.get('confirmPassword').setValidators([Validators.required, Validators.minLength(8)]);

                }

                this.userForm.get('loginTypeId').updateValueAndValidity();

            }

        )
}
createFormControls() {
    this.userName = new FormControl('', [
        Validators.required,
        Validators.minLength(4)
    ]);
    this.firstName = new FormControl('', Validators.required);
    this.lastName = new FormControl('', Validators.required);
    this.email = new FormControl('', [
      Validators.required,
      Validators.pattern("[^ @]*@[^ @]*")
    ]);
    this.password = new FormControl('', [
       Validators.required,
       Validators.minLength(8)
    ]);
    this.confirmPassword = new FormControl('', [
        Validators.required,
        Validators.minLength(8)
    ]);

}

createForm() {
 this.userForm = new FormGroup({
      userName: this.userName,
      name: new FormGroup({
        firstName: this.firstName,
        lastName: this.lastName,
      }),
      email: this.email,
      loginTypeId: this.loginTypeId,
      password: this.password,
      confirmPassword: this.confirmPassword
    });
}

However when I run it I get a browser javascript error

UserEditor.html:82 ERROR RangeError: Maximum call stack size exceeded
    at SafeSubscriber.tryCatcher (tryCatch.js:9)
    at SafeSubscriber.webpackJsonp.../../../../rxjs/_esm5/Subscription.js.Subscription.unsubscribe (Subscription.js:68)
    at SafeSubscriber.webpackJsonp.../../../../rxjs/_esm5/Subscriber.js.Subscriber.unsubscribe (Subscriber.js:124)
    at SafeSubscriber.webpackJsonp.../../../../rxjs/_esm5/Subscriber.js.SafeSubscriber.__tryOrUnsub (Subscriber.js:242)
    at SafeSubscriber.webpackJsonp.../../../../rxjs/_esm5/Subscriber.js.SafeSubscriber.next (Subscriber.js:186)
    at Subscriber.webpackJsonp.../../../../rxjs/_esm5/Subscriber.js.Subscriber._next (Subscriber.js:127)
    at Subscriber.webpackJsonp.../../../../rxjs/_esm5/Subscriber.js.Subscriber.next (Subscriber.js:91)
    at EventEmitter.webpackJsonp.../../../../rxjs/_esm5/Subject.js.Subject.next (Subject.js:56)
    at EventEmitter.webpackJsonp.../../../core/esm5/core.js.EventEmitter.emit (core.js:4319)
    at FormControl.webpackJsonp.../../../forms/esm5/forms.js.AbstractControl.updateValueAndValidity (forms.js:3377)

"log this!" is loggedcalled repeatedly like it is called recursively which is why their is a stack error

If I remove the valueChanges.subscribe the code work apart from removing the validation conditionally.

Why is it calling valueChanges.subscribe recursively?

Albemarle answered 14/12, 2017 at 20:49 Comment(3)
Isn't it because you call updateValueAndValidity() at the end of the event handler?Zildjian
I agree with ConnorsFan, updateValueAndValidity() is probably causing valueChanges to fire again, causing an infinite loopRespire
@ConnorsFan thats the reason for the recursion. I shouldn't update the same field I am monitoring for change. Code was meant to be 'this.userForm.get('loginTypeId').'. You can put it in an answerAlbemarle
Z
29

The problem is that you modify the value of the field inside of the valueChanges event handler for that same field, causing the event to be triggered again:

this.userForm.get('loginTypeId').valueChanges.subscribe(
  (loginTypeId: string) => {
    ...
    this.userForm.get('loginTypeId').updateValueAndValidity(); <-- Triggers valueChanges!
}
Zildjian answered 14/12, 2017 at 21:52 Comment(2)
Yes thanks I had to change to this.userForm.get('password').updateValueAndValidity();this.userForm.get('confirmPassword').updateValueAndValidity();Albemarle
What about if I am using forEach for the formgroup? #63304403Roughcast
E
72

If you want to subscribe to any form changes and still run patchValue inside it, then you could add the {emitEvent: false} option to patchValue, thus the patching will not trigger another change detection

code:

this.formGroup
    .valueChanges
    .subscribe( _ => {
        this.formGroup.get( 'controlName' ).patchValue( _val, {emitEvent: false} );
    } );

PS. This is also less tedious than subscribing to each form control one-by-one to avoid triggering change max call stack exceeded. Especially if you form has 100 controls to subscribe to.

Now to elaborate further, if you still need to updateValueAndValidity inside the subscription, then I suggest you use the distinctUntilChanged rxjs operator, to only run the subscription, when some value changes.

distinctUntilChanged documentation can be found here

https://www.learnrxjs.io/operators/filtering/distinctuntilchanged.html

distinctUntilChanged - Only emit when the current value is different than the last.

Now we will also have to make it a custom validation function, because by default, distinctUntilChanged validates objects by pointer and the pointer is new on every change.

this.formGroup
    .valueChanges
    .distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b))
    .subscribe( _ => {
        this.formGroup.get( 'controlName' ).patchValue( _val, {emitEvent: false} );
        this.formGroup.get( 'controlName' ).updateValueAndValidity();
    } );

And voila, we are patching and updating, without running into the maximum call stack!

Edrei answered 9/6, 2018 at 1:56 Comment(1)
The solution I was looking for. Bettern than having a boolean 'valueChangesRunning'. +1Hyposensitize
A
50

My answer is just development of this one.

By adding distinctUntilChanged() in the pipeline just before subscribe() you avoid the "Maximum call stack size exceeded" because

distinctUntilChanged method only emit when the current value is different than the last.

The usage:

this.userForm.get('password')
  .valueChanges.pipe(distinctUntilChanged())         
  .subscribe(val => {})

Documentation

Agneta answered 27/11, 2018 at 7:56 Comment(2)
What about if I am using forEach for the formgroup? #63304403Roughcast
I tried to subscribe to valueChanges of form. At the task needs been to check one checkbox and disable other and vice versa. I get maximum callstack exceeded.distinctUntilChanged save my time! thanks a lot!Blackbeard
Z
29

The problem is that you modify the value of the field inside of the valueChanges event handler for that same field, causing the event to be triggered again:

this.userForm.get('loginTypeId').valueChanges.subscribe(
  (loginTypeId: string) => {
    ...
    this.userForm.get('loginTypeId').updateValueAndValidity(); <-- Triggers valueChanges!
}
Zildjian answered 14/12, 2017 at 21:52 Comment(2)
Yes thanks I had to change to this.userForm.get('password').updateValueAndValidity();this.userForm.get('confirmPassword').updateValueAndValidity();Albemarle
What about if I am using forEach for the formgroup? #63304403Roughcast
I
23

Try adding distinctUntilChanged() in the pipeline just before subscribe(). It should filter out those "change" events where value was not actually changed.

Immoralist answered 14/12, 2017 at 20:56 Comment(3)
This is universal solution, which works also in the case when you perform valueChanges on multiple and dependent on each others formControls. Thanks!Agneta
Somehow doesn't help me :(Cowardice
It just means that your code is different in some way. Post your own question and someone will answer it.Immoralist
E
5

In my case, it was when I was trying to update the value and validity of a Form Control, it is indeed fixed by the { emitEvent: false } addition:

    // making sure that at least one name is filled in
    this.form.get('firstName').valueChanges.subscribe((value) => {
      if (value) {
        this.form.get('lastName').setValidators(Validators.nullValidator);
      } else {
        this.form.get('lastName').setValidators(Validators.required);
      }
      this.form.get('lastName').updateValueAndValidity({ emitEvent: false }); // <- here
    });
Eld answered 30/11, 2022 at 9:54 Comment(0)
A
2

I faced similar error when doing validation. Error raised when call updateValueAndValidity().in my case I used this overload updateValueAndValidity({emitEvent : false})

try with this

this.userForm.get('loginTypeId').updateValueAndValidity({emitEvent : false});

Aquamanile answered 12/9, 2021 at 15:34 Comment(1)
Thank you so much. You saved hours of debugging.Superabound
B
0
this.userForm.get('loginTypeId').enable({emitEvent: false});

if you need to enable one select of all selects your form

Blackbeard answered 18/2, 2022 at 8:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.