AlanObject
AlanObject

Reputation: 9943

RxJS share operator a problem inside *ngIf

I am beginning to get some proficiency in RxJS operators but I still have difficulty with some of them. In implementing a search component I have the following code:

      searchResult$: Observable<LunrDoc []>;
      searchResultLength$: Observable<number>;

      ngAfterViewInit(): void {

        // observable to produce an array of search hits

        this.searchResult$ = fromEvent<Event>(this.searchInput.nativeElement, 'keyup').pipe(
          debounceTime(1000),
          switchMap(e => this.ss.search((e.target as HTMLTextAreaElement).value)),
          share()
        );

        // observable to return the length of the array

        this.searchResultLength$ = this.searchResult$.pipe(
          map(sr => sr ? sr.length : 0),
          share()
        );
      }

And this is used in the template like this:

    <p *ngIf="(searchResultLength$ | async) > 0">
      Total Documents: {{ (searchResultLength$ | async) | number }}
    </p>

    <p *ngFor="let doc of (searchResult$ | async)">
      <span *ngIf="doc.path" [routerLink]="doc.path" style="color: darkblue; font-weight: bold; text-underline: darkblue; cursor: pointer">
        {{ doc.title }}
      </span>
      {{ doc.content.substring(0, 400) }}
    </p>

What happens is when a non-null array is emitted by the searchResult$ observable the rendered result in the first paragraph element as "Total Documents:" without a number after it. The *ngFor decorated paragraph works exactly as expected.

The reason I believe is that the second async pipe gets activated and subscribes to the shared observable after the last value gets emitted. So it never gets a "next" call.

Is there an RxJS operator to use instead of share to fix this situation? Or did I miss something else?

Upvotes: 1

Views: 496

Answers (1)

C.OG
C.OG

Reputation: 6529

As @Ingo Bürk suggested. shareReplay(1) is probably what you are looking for. The 1 refers to the buffer size (how many values it should play back to you).

Share replay docs

What I'll also suggest is to avoid creating multiple observables.

e.g. This creates two different subscribers.

<p *ngIf="(searchResultLength$ | async) > 0">
  Total Documents: {{ (searchResultLength$ | async) | number }}
</p>

The alternative is:

<p *ngIf="(searchResultLength$ | async) as searchResultLength > 0">
  Total Documents: {{ searchResultLength | number }}
</p>

Proposed Solution

Looking at the code and trying to understand the functionality, I think you can make these changes to get your desired output.

Component:

searchResult$: Observable<LunrDoc []>;
    
ngAfterViewInit(): void {
    this.searchResult$ = fromEvent<Event>(this.searchInput.nativeElement, 'keyup')
    .pipe(
        debounceTime(1000),
        switchMap(e => this.ss.search((e.target as HTMLTextAreaElement).value)),
    );
}

You wouldn't need share or shareReplay as they don't seem to offer any functionality that your template will need. Unless there is some more code in the template that isn't visible in your question.

The this.searchResultLength$ observable also seems redundant as that is just the length property on the value returned by this.searchResult$.

The template:

<ng-container *ngIf="{searchResult: searchResult$ | async} as vm">
    <p *ngIf="vm.searchResult.length > 0">
        Total Documents: {{ vm.searchResult }}
    </p>

    <p *ngFor="let doc of vm.searchResult">
        <span *ngIf="doc.path" [routerLink]="doc.path" style="color: darkblue; font-weight: bold; text-underline: darkblue; cursor: pointer">
            {{ doc.title }}
        </span>
        {{ doc.content.substring(0, 400) }}
    </p>
</ng-container>

The wrapper ng-container will always display because the ngIf condition will evaluate to a truthy value.

Note

Consider looking into the ditinctUntilChanged operator, when dealing with inputs that trigger an observable: https://www.learnrxjs.io/operators/filtering/distinctuntilchanged.html

Upvotes: 1

Related Questions