web-dev-qa-db-fra.com

RangeError: taille maximale de la pile d'appels dépassée lors de l'utilisation de valueChanges.subscribe

J'utilise Angular 5 avec des formulaires réactifs et je dois utiliser valueChanges pour désactiver dynamiquement la validation requise.

classe de composant:

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
    });
}

Cependant, lorsque je l'exécute, le navigateur génère une erreur javascript.

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)

"connectez ceci!" est consigné à plusieurs reprises comme s'il était appelé récursivement, raison pour laquelle il y a une erreur de pile

Si je supprime les valueChanges.subscribe, le code fonctionne indépendamment de la suppression conditionnelle de la validation.

Pourquoi appelle-t-il valueChanges.subscribe récursivement?

8
dfmetro

Le problème est que vous modifiez la valeur du champ à l'intérieur du gestionnaire d'événements valueChanges pour ce même champ, ce qui provoque le déclenchement de l'événement à nouveau:

this.userForm.get('loginTypeId').valueChanges.subscribe(
  (loginTypeId: string) => {
    ...
    this.userForm.get('loginTypeId').updateValueAndValidity(); <-- Triggers valueChanges!
}
7
ConnorsFan

Essayez d’ajouter distinctUntilChanged() dans le pipeline juste avant subscribe(). Il devrait filtrer ces événements de "changement" où la valeur n'a pas été réellement modifiée.

8
Alexander Leonov

En fait, la vraie réponse est que, si vous souhaitez vous abonner à des modifications de formulaire tout en continuant d'exécuter patchValue, vous devez simplement ajouter l'option {emitEvent: false} à patchValue. Ainsi, le correctif ne déclenchera pas une autre détection de modification

code:

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

PS. Cela est également moins fastidieux que de souscrire à chaque contrôle de formulaire un par un pour éviter de provoquer le dépassement de la pile d'appels de modification max. Surtout si vous avez 100 contrôles auxquels vous vous abonnez.

Pour poursuivre, si vous avez encore besoin de mettre à jour updateValueAndValidity dans l’abonnement, je vous suggère d’utiliser l’opérateur distinctUntilChanged rxjs, pour exécuter uniquement l’abonnement, lorsque certaines valeurs changent. 

Maintenant, nous devrons également en faire une fonction de validation personnalisée, car par défaut, distinctUntilChanged valide les objets par un pointeur et le pointeur est nouveau à chaque modification.

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();
    } );

Et voila, nous corrigeons et mettons à jour, sans courir dans la pile d'appels maximum!

7

Ma réponse est juste le développement de celui-ci .

En ajoutant distinctUntilChanged() dans le pipeline juste avant subscribe(), vous évitez la "Taille maximale de la pile d'appels dépassée" car

La méthode distinctUntilChanged n'est émise que lorsque la valeur actuelle est différente de la dernière.

L'usage:

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

Documentation

5
mpro