RangeError: максимальный размер стека вызовов превышен при использовании valueChanges.subscribe

Я использую Angular 5 с реактивными формами и должен использовать valueChanges для того, чтобы отключить требуемую проверку динамически

класс компонента:

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

Однако, когда я запускаю его, я получаю ошибку браузера 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)

"Запишите это!" неоднократно записывается в журнал, так как он называется рекурсивно, поэтому их ошибка стека

Если я удалю значениеChanges.subscribe, то код работает отдельно от удаления проверки условно.

Почему он вызывает рекурсивно вызов valueChanges.subscribe?

Ответ 1

Проблема в том, что вы изменяете значение поля внутри обработчика события valueChanges для этого же поля, заставляя событие запускаться снова:

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

Ответ 2

На самом деле, правильный ответ таков: если вы хотите подписаться на какие-либо изменения формы и по-прежнему запускать в ней patchValue, то вам просто нужно добавить параметр {emitEvent: false} в patchValue, таким образом, исправление не вызовет другое обнаружение изменений

Код:

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

PS. Это также менее утомительно, чем подписка на каждый элемент управления формы один за другим, чтобы избежать запуска превышения максимального стека вызовов. Особенно, если у вас есть 100 элементов управления для подписки.

Теперь, чтобы уточнить, если вам все еще нужно обновить updateValueAndValidity внутри подписки, я предлагаю вам использовать оператор distinctUntilChanged rxjs, чтобы запускать подписку только при изменении какого-либо значения.

Различную документацию можно найти здесь

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

DifferentUntilChanged - издает только тогда, когда текущее значение отличается чем последний.

Теперь нам также нужно будет сделать ее пользовательской функцией проверки, потому что по умолчанию DifferentUntilChanged проверяет объекты по указателю, и указатель является новым при каждом изменении.

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

И вуаля, мы исправляем и обновляем, не запуская максимальный стек вызовов!

Ответ 3

Мой ответ - просто разработка этого.

Добавляя distinctUntilChanged() в конвейер перед subscribe(), вы избегаете "Превышен максимальный размер стека вызовов", потому что

Метод DifferentUntilChanged генерирует только тогда, когда текущее значение отличается от последнего.

Использование:

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

Документация

Ответ 4

Попробуйте добавить distinctUntilChanged() в конвейере непосредственно перед subscribe(). Он должен отфильтровывать те события "изменения", значения которых фактически не изменялись.