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.