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?
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$..
})