3
votes

I have a simple setup to show a loading spinner when the async pipe is null:

<div *ngIf="(searchResults$ | async) as searchResults; else loading">
</div>
<ng-template #loading>
    loading..
</ng-template>

However, when the user searches again for a second time, the loading.. doesn't show, I suppose I need this searchResults$ observable to emit null to show the spinner again, or have a separate isLoading variable.

what's the best way of doing that?

if it matters, i've got a debounce and a switchMap (i.e. tricky using finalize etc)

this.searchResults$ = this.filters$
      .pipe(
        debounceTime(200),
        distinctUntilChanged(),
        switchMap((f) => {
            return httpGet(f)
        })
      )

also, I tried *ngIf="!isLoading && (searchResults$ | async) as searchResults but found it problematic, e.g. searchResults$ not subscribed to, or angular complaining about changes after change detection

2
Can you supply your code using the isLoading solution? The issues you mentioned should be solvable and I think it makes for a clearer solution than have the searchResults observable emit nulls to indicate loading - rh16
@rh16 inside the switchmap, set this.isLoading = true, then httpGet(f).pipe( set isLoading=false ). tried putting isLoading in the template both before and after the pipe to async. - robert king
The problem may be that returning httpGet(f).pipe(), depending on what is inside the pipe, may be discarding the return of httpGet(f). Like I said, I am not a fan of having searchResults$ emit nulls as I think it could erode code clarity, but if you're happy with that solution I will leave it at that - rh16
@rh16 thanks - yeah it's not ideal having to emit nulls I agree. I'd like to see a nicer solution one day in the future, as this is something I end up needing multiple times per project. - robert king

2 Answers

2
votes

I've faced the same issue and solved distinguishing the "ask" stream and the "result" stream, merging both for the component result observable. Something like this (based on your code):

this.searchResults$ = merge(
      this.filters$.pipe(map(f => null)),
      this.filters$.pipe(
        debounceTime(200),
        distinctUntilChanged(),
        switchMap((f) => {
            return httpGet(f)
        })
      )
    );
3
votes

You could try setting the isLoading variable using the tap operator like so:

this.searchResults$ = this.filters$
      .pipe(
        debounceTime(200),
        distinctUntilChanged(),
        tap(() => {this.isLoading = true}),
        switchMap((f) => {
            return httpGet(f)
        }),
        tap(() => {this.isLoading = false})
      );

You can then get around angular not subscribing to your observable by hosting it in a different *ngIf inside a ng-container element.

<ng-container *ngIf="(searchResults$ | async) as searchResults">
  <div *ngIf="!isLoading"></div>
</ng-container>
<ng-template *ngIf="isLoading">
    loading..
</ng-template>