htmlangularangular-lifecycle-hooks

HTML is rendered in Angular before binding to an observable. Lifecycle issue


I have a component that renders a grid of images bound to an Observable<Image[]> named sorted$.

<ng-container *ngFor="let image of sorted$ | async; index as i">
    <img src="..."/>
</ng-container>

I also have a dropdown that can be used to sort the images. I'm using fromEvent() to create an Observable<InputEvent> from the dropdown's select event called sort$.

this.sort$ = fromEvent<InputEvent>(this.sort.nativeElement, "input");

Finally, I have another Observable<Image[]> named source$ that provides all the images that will sorted:

source$!: Observable<Image[]>;

The idea is to sort the images in response to the user choosing a sort option.

this.sorted$ = combineLatest({ source: this.source$, sort: this.sort$ })
   .pipe(map(images => { *** sort logic here ***}));

And, it works! Except for one thing... When the page is first displayed, no images are rendered because sorted$ hasn't emitted any values - because the user hasn't selected a sort option. When a user does choose an option, everything works - just not on the initial render.

sort$ and sorted$ are created in ngAfterViewInit() because the <select> element isn't yet bound until that point. However, the page has already been rendered.

If I initially assign the sorted$ observable to the value of the source$ observable in the ngOnInit() function, the page will render the images, but when it is later assigned a new value in ngAfterViewInit(), I get the error:

Error: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked.

I have chickens and eggs. How can I render the sorted images before I have access to the <select> element?


Solution

  • You can use startWith rxjs operator

    this.sort$ = fromEvent<InputEvent>(this.sort.nativeElement, "input")
                      .pipe(startWith(null));
    

    Or you can use merge instead of combineLastest (that "dispatch" when any observable change)

    this.sorted$ = merge(this.source$, this.sort$ }).pipe((res:Image[] | string)=>{
       ..res can be the response to this.source$ or to this.sort$..
    })