Issue
I have 3 observables:
query$: Observable<string> = this.q$.pipe(tap(() => this.page$.next(0));
filter$: Observable<number> = this.f$.pipe(tap(() => this.page$.next(0));
page$ = new BehaviorSubject<number>(0);
combinetLatest({
query: this.query$,
filter: this.filter$,
page: this.page$
})
.pipe(
switchMap(({ query, filter, page }) => {
...
})
);
I have to pass their values down the stream whenever any of them emit. However, using combineLatest results in at least two more redundant emits. I attempted to wire in distinctUntilChanged(), but this also produces one redundant emit at some point.
Please assist :( or perhaps my entire approach/thinking is incorrect, and I should not change page$ as a side-effect of query$, filter$?
Update: In a sense, I don't care about the "page" value, as it's always 0 in the case of "query" or "filter" emissions. But I do care about "query" and "filter" values when "page" emits. So it seems that it becomes two separate observables?.. But I wanted to assign it to a component property in order to use it with the "async" pipe in the template, so with two different observables, I can't...
Update: Sorry for the fuss; I should have specified more details.
The query$ is being emitted on input field input.
The filter$ is being emitted on select box changes.
The page$, which stands for "page number," is being emitted on the "Load More" button click.
So my idea was to have a component property:
objects$: Observable<object[]> = combineLatest({
query: this.query$,
filter: this.filter$,
page: this.page$
}).pipe(
switchMap(({ query, filter, page }) => this.someService.sendApiRequest(query, filter, page)),
scan((acc, { objects}) => {
if (page !== 0) {
return [...acc, ...objects];
} else {
return objects;
}
}, [])
);
And use it in a template with the async pipe:
<div *ngFor="let x of objects$ | async">...</div>
Basically, to fetch results whenever the user types in a query, changes filters, or loads more items.
Solution
Maybe something like this would work:
combineLatest({ query: this.q$, filter: this.f$ }).pipe(
switchMap(({query, filter}) => this.page$.pipe(
map((p, i) => i === 0 ? 0 : p)),
map(page => ({query, filter, page}))
))
);
The idea is that your combineLatest starts with only the query and the filter, then switchMaps to the page. The second argument in the map operator indicates the "emission index" of the subscription to page$, which will start at 0, and increase each time page$ emits. So, we emit 0 when it's the first emission and emit the value from page$ after that. Whenever either the filter$ or query$ emit, the switchMap will create a new subscription, which means the a new inner subscription to page$ is made meaning the index will start back at 0.
You could tack on another switchMap to make your api call or just turn the second map into switchMap:
combineLatest({ query: this.q$, filter: this.f$ }).pipe(
switchMap(({query, filter}) => this.page$.pipe(
map((p, i) => i === 0 ? 0 : p)),
switchMap(page => this.callApi(query, filter, page))
))
);
Update:
If you don't need to specify an actual page number and only need the next page to load, instead of page$ you can use:
private loadMore$ = new BehaviorSubject<void>(undefined);
And your observable for the view could look like:
items$ = combineLatest({ query: this.q$, filter: this.f$ }).pipe(
switchMap(({query, filter}) => this.loadMore$.pipe(
switchMap((_, page) => this.callApi(query, filter, page)),
scan((all, items) => all.concat(items), [])
)),
);
Here we simply use loadMore$ as a trigger, and use the "emission index" of the inner observable as the page number. Since loadMore$ is a BehaviorSubject, it will emit once upon subscription, so it doesn't require a button click to emit the first time. Then each subsequent time loadMore$ emits (when your button is clicked), it will emit again and switchMap will execute the api call with the incremented index (page number).
Here's a StackBlitz example.
Answered By - BizzyBob
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.