Search code examples
angularrxjsreactive-programmingangular-reactive-forms

With RxJS how to trigger observables by another action stream


So I am trying to build a list component reactively by angular and RxJS. the list has a lot of features like:

  • search by word
  • change page number
  • change page size
  • filter by field(s)
  • sort

search service code:

private seachItemSubject = new BehaviorSubject<string>("");
private pageLimitSubject = new BehaviorSubject<number>(10);
private pageNumberSubject = new BehaviorSubject<number>(0);
private orderingSubject = new BehaviorSubject<ListOrder>({});
private filteringSubject = new BehaviorSubject<any>({});

searchItemAction$ = this.seachItemSubject.asObservable();
pageLimitAction$ = this.pageLimitSubject.asObservable();
pageNumberAction$ = this.pageNumberSubject.asObservable();
orderingAction$ = this.orderingSubject.asObservable();
filteringAction$ = this.filteringSubject.asObservable();

combination$ = Observable.combineLatest(
    this.searchItemAction$,
    this.pageLimitAction$,
    this.pageNumberAction$,
    this.orderingAction$,
    this.filteringAction$
)

changeSeachItem(searchItem: string) {
    this.seachItemSubject.next(searchItem);
}

changePageLimit(pageLimit: number) {
    this.pageLimitSubject.next(pageLimit);
}

changePageNumber(pageNumber: number) {
    this.pageNumberSubject.next(pageNumber);
}

changeOrdering(listOreder: ListOrder) {
    this.orderingSubject.next(listOreder);
}

changeFilters(filters: any) {
    this.filteringSubject.next(filters)
}

Components code

results$: Observable<MyResult> = this.listSearchService.combination$
    .switchMap(([filterItem, pageLimit, pageNumber, ordering, filters]) => this.listService.query({
        offset: pageNumber,
        limit: pageLimit,
        filter: filterItem,
        order: ordering.order,
        descending: ordering.descending,
        ...filters
    }));

the problem that I am facing now is that I want the page number to be changed if the user used the filtering feature.

I think what I want is some sort of an Subject or an event that can be called whenever the user uses the filtering feature.

so it would be something like this.

applyFilters(filters: any) {
    this.listSearchService.changePageNumber(0);
    this.listSearchService.changeFilters(filters);
    this.listSearchService.commit.next() -----> so it would change both then start the combine latest
    console.log(filters)
}

I am not sure that that would be the right answer, so please tell me if you have a better answer.

Thanks


Solution

  • As you have discovered, you cannot use the original solution below as combineLatest will fire both when changePageNumber and changeFilters are called from applyFilters.

    An alternative approach is to use a single state object implemented via the scan operator, and update one or more properties in it at a time. This means that the service firing from it will only be called once per 'action'.

    BTW you are currently storing your 'state' in each BehaviorSubject, and combining it.

    class ListState {
      searchItem: any = "";
      pageLimit: any = 10;
      pageNumber: any = 0;
      ordering: any = "asc";
      filtering: any = "";
    }
    
    const listStateUpdatesSubject = new BehaviorSubject<Partial<ListState>>(null);
    const listStateUpdates$ = listStateUpdatesSubject.asObservable();
    
    // fires once per state update, which can consist of multiple properties
    const currListState$ = listStateUpdates$.pipe(
      filter(update => !!update),
      scan<Partial<ListState>, ListState>(
        (acc, value) => ({ ...acc, ...value }), new ListState)
    );
    
    const callListService$ = currListState$.pipe(map(state => listService(state)));
    
    function changeOrdering(listOreder: string) {
        changeState({ ordering: listOreder });
    }
    
    function changeFilters(filters: any) {
        changeState({ pageNumber: 0, filtering: filters }); // set both page number and filtering
    }
    
    // etc
    

    Here is a working demo: https://stackblitz.com/edit/so-rxjs-filtering-2?file=index.ts, note that a call to changeFilters results in the service being called once only.

    Original Solution

    Create a new observable that listens to filteringAction$, perform the desired actions and use that new observable within the combineLatest.

    ...
    orderingAction$ = this.orderingSubject.asObservable();
    filteringAction$ = this.filteringSubject.asObservable();
    
    filterApplied$ = filteringAction$.pipe(map(filter => applyFilters(filter)));
    
    applyFilters(filters: any) {
      this.listSearchService.changePageNumber(0);
      this.listSearchService.changeFilters(filters);
      return filters;
    }
    
    combination$ = Observable.combineLatest(
      this.searchItemAction$,
      this.pageLimitAction$,
      this.pageNumberAction$,
      this.orderingAction$,
      this.filterApplied$    // instead of filteringAction$
    )