简体   繁体   中英

Angular - RxJS : afterViewInit and Async pipe

I tried to do the following in my component which uses changeDetection: ChangeDetectionStrategy.OnPush,

@ViewChild('searchInput') input: ElementRef;

ngAfterViewInit() {
    this.searchText$ = fromEvent<any>(this.input.nativeElement, 'keyup')
      .pipe(
        map(event => event.target.value),
        startWith(''),
        debounceTime(300),
        distinctUntilChanged()
      );
}

And in the template

<div *ngIf="searchText$ | async as searchText;">
  results for "<b>{{searchText}}</b>"
</div>

It doesn't work, however if I remove the OnPush , it does. I am not too sure why since the async pipe is supposed to trigger the change detection.

Edit :

Following the answers, I have tried to replace what I have by the following:

this.searchText$ = interval(1000);

Without any @Input , the async pipe is marking my component for check and it works just fine. So I don't get why I haven't got the same behavior with the fromEvent

By default Whenever Angular kicks change detection, it goes through all components one by one and checks if something changes and updates its DOM if it's so. what happens when you change default change detection to ChangeDetection.OnPush ?

Angular changes its behavior and there are only two ways to update component DOM.

  • @Input property reference changed

  • Manually called markForCheck()

If you do one of those, it will update DOM accordingly. in your case you don't use the first option, so you have to use the second one and call markForCheck() , anywhere. but there is one occasion, whenever you use async pipe, it will call this method for you.

The async pipe subscribes to an Observable or Promise and returns the latest value it has emitted. When a new value is emitted, the async pipe marks the component to be checked for changes . When the component gets destroyed, the async pipe unsubscribes automatically to avoid potential memory leaks.

so there is nothing magic here, it calls markForCheck() under the hood. but if it's so why doesn't your solution work? In order to answer this question let's dive in into the AsyncPipe itself. if we inspect the source code AsyncPipes transform function looks like this

transform(obj: Observable<any>|Promise<any>|null|undefined): any {
    if (!this._obj) {
      if (obj) {
        this._subscribe(obj);
      }
      this._latestReturnedValue = this._latestValue;
      return this._latestValue;
    }
    ....// some extra code here not interesting
 }

so if the value passed is not undefined, it will subscribe to that observable and act accordingly (call markForCheck() , whenever value emits)

Now it's the most crucial part the first time Angular calls the transform method, it is undefined, because you initialize searchText$ inside ngAfterViewInit() callback (the View is already rendered, so it calls async pipe also). So when you initialize searchText$ field, the change detection already finished for this component, so it doesn't know that searchText$ has been defined, and subsequently it doesn't call AsyncPipe anymore, so the problem is that it never get's to AsyncPipe to subscribe on those changes, what you have to do is call markForCheck() only once after the initialization, Angular ran changeDetection again on that component, update the DOM and call AsyncPipe, which will subscribe to that observable

ngAfterViewInit() {
    this.searchText$ =
     fromEvent<any>(this.input.nativeElement, "keyup").pipe(
      map((event) => event.target.value),
      startWith(""),
      debounceTime(300),
      distinctUntilChanged()
    );
    this.cf.markForCheck();
  }

The changeDetection: ChangeDetectionStrategy.OnPush allow to the component to not triggered the changeDetection all the time but just when an @Input() reference is updated. So if you do all your stuff in the same component, no @Input() reference are updated so the view is not updated.

I propose you to Create your dumb component with your template code above, but give it the searchText via an @Input(), and call your dumb component in your smart component

<my-dumb-component [searchText]="searchText$ | async"></my-dumb-component>

@Input() searchText: SearchText

template

<div *ngIf="searchText">
  results for "<b>{{searchText}}</b>"
</div>

This is because Angular is updates DOM interpolations before ngAfterViewInit and ngAfterViewChecked . I know this sounds confusing a bit. It's because of the first change detection cycle Angular does. Referring to Max Koretskyi's article about change detection algorithm of Angular , in a change detection cycle these happens sequentially:

  1. sets ViewState.firstCheck to true if a view is checked for the first time and to false if it was already checked before
  2. checks and updates input properties on a child component/directive instance
  3. updates child view change detection state (part of change detection strategy implementation)
  4. runs change detection for the embedded views (repeats the steps in the list)
  5. calls OnChanges lifecycle hook on a child component if bindings changed
  6. calls OnInit and ngDoCheck on a child component (OnInit is called only during first check)
  7. updates ContentChildren query list on a child view component instance
  8. calls AfterContentInit and AfterContentChecked lifecycle hooks on child component instance (AfterContentInit is called only during first check)
  9. updates DOM interpolations for the current view if properties on current view component instance changed
  10. runs change detection for a child view (repeats the steps in this list)
  11. updates ViewChildren query list on the current view component instance
  12. calls AfterViewInit and AfterViewChecked lifecycle hooks on child component instance (AfterViewInit is called only during first check)
  13. disables checks for the current view (part of change detection strategy implementation)

As you see, Angular updates DOM interpolations (at step 9) after AfterContentInit and AfterContentChecked hooks are called, so if you call rxjs subscriptions in AfterContentInit or AfterContentChecked lifecycle hooks (or earlier, like OnInit etc.) your DOM will be updated because Angular updates DOM at step 10, and when you change something in ngAfterViewInit() and you are using OnPush, Angular won't update DOM because you are at step 12 on ngAfterViewInit() and Angular has already updated DOM before you change something!

There are workaround solutions to avoid this to subscribe it in ngAfterViewInit. First, you can call markForCheck() function, so you basically say by using it on the first cycle that "hey Angular, you updated DOM on step 9, but I have something to change at step 12, so please be careful, have a look at ngAfterViewInit I have still something to change". Or as a second solution, you can trigger a change detection manually again (by triggering and event handler or using detecthanges() function of ChangeDetectorRef) so that Angular repeats all these steps again, and when it reaches at step 9 again, Angular updates your DOM.

I have created a Stackblitz example that you can try these out. You can uncomment the lines of subscriptions placed in lifecycle hooks 1 by 1, so that you can see after which lifecycle hook Angular updates DOM. Or you can try triggering an event or triggering change detection cycle manually and see that Angular updates DOM on the next cycle.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM