Ошибка: ExpressionChangedAfterItHasBeenCheckedError, когда угловой компонент тестируется с синхронными наблюдаемыми - PullRequest
0 голосов
/ 31 января 2019

У меня есть простой компонент, который использует сервис, возвращающий Observable.Такой Observable используется для управления созданием html-элемента через *ngIf.

Здесь код

@Component({
  selector: 'hello',
  template: `<h1 *ngIf="stuff$ | async as obj">Hello {{obj.name}}!</h1>`,
  styles: [`h1 { font-family: Lato; }`],
})
export class HelloComponent implements AfterViewInit {
  constructor(
    private myService: MyService,
  ) {}

  stuff$;

  ngAfterViewInit() {
    this.stuff$ = this.myService.getStuff();
  }
}


@Injectable({
  providedIn: 'root'
})
export class MyService {
  getStuff() {
    const stuff = {name: 'I am an object'};
    return of(stuff).pipe(delay(100));
  }
}

Как видите, getStuff() возвращает и Observable с delay и все работает как положено.

Теперь я удаляю delay и получаю следующую ошибку на консоли

Ошибка: ExpressionChangedAfterItHasBeenCheckedError: Выражение изменилось после его проверки.Предыдущее значение: 'ngIf: null'.Текущее значение: 'ngIf: [объект Object]'.

Это Stackbliz, который воспроизводит регистр.

Это неизбежно?Есть ли способ избежать этой ошибки?Этот небольшой пример повторяет пример реального мира, где MyService запрашивает серверную часть, поэтому задержка.

Это актуально для меня, потому что я хотел бы иметь возможность синхронно запускать тесты, имитирующие реальный бэкэнд, и, когда я пробую такой подход, я получаю тот же результат.

Ответы [ 4 ]

0 голосов
/ 31 января 2019

Если вам нужно синхронно изменить модель в хуке жизненного цикла ngAfterViewInit, вы можете активировать обнаружение изменений, чтобы избежать ошибки ExpressionChangedAfterItHasBeenCheckedError:

constructor(private changeDetectorRef: ChangeDetectorRef) { }

ngAfterViewInit() {
  this.stuff$ = this.myService.getStuff();
  this.changeDetectorRef.detectChanges();
}

См. этот стек-блиц для демонстрации.

0 голосов
/ 31 января 2019

Почему вы обрабатываете материал $ как @Input, когда пытаетесь получить его и из myService?Это своего рода конфликт при рендеринге.

Возможно, ngAfterViewChecked будет работать лучше, чем ngAfterViewInit.Но решение все еще не очень хорошее.

0 голосов
/ 31 января 2019

Ошибка в основном говорит о том, что одно изменение вызвало другое изменение.Это происходит только без delay(100), потому что RxJS строго последовательный и подписки происходят синхронно.Если вы используете delay(100) (даже если вы используете просто delay(0), это делает излучение асинхронным, поэтому оно происходит в другом кадре и перехватывается другим циклом обнаружения изменений.

Очень простой способ избежать этого - простоприсвоение переменной переменной setTimeout() или NgZone.run().

или более "Rx way" использует оператор subscribeOn(async):

import { asyncScheduler } from 'rxjs';
import { observeOn } from 'rxjs/operators';

...

return of(stuff).pipe(
  observeOn(asyncScheduler)
);
0 голосов
/ 31 января 2019

Вы присваиваете свою наблюдаемую после инициализации представления.Попробуйте:

@Input() name: string;
stuff$ = this.myService.getStuff();

Или:

ngOnInit() { // or even in constructor()
    this.stuff$ = this.myService.getStuff();
}
...