Reputation: 9943
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
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).
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>
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