Search code examples
angulartypescriptrxjsangular-httpclient

How can I re-call an Angular HttpClient observable when another observable changes?


Big picture, what I'm trying to achieve is a set filters my page header that control the input parameters to several analytics pages in my app. I've encapsulated the functionality of the filters into an Angular service that exposes an observable that emits when changes to the filters occur.

What I want is for services that use those filter values in HttpClient requests to subscribe to changes in the filters and re-run their HttpClient requests when the filters change (so if a date range changes for example, any elements on my page that are driven by that date range get updated automatically).

A typical data service in my app would look like below. It seems what I'm trying to do should be simple enough but I'm struggling to get my head around the RxJS library enough to combine observables in the way I'm aiming for.

export class DashboardDataService {

  constructor(
    private readonly http: HttpClient,
    private readonly globalFiltersService: GlobalFiltersService
  ) { }

  public getDashboard(): Observable<DashboardDto> {

    const filtersSubscription = globalFiltersService.filters$.subscribe(...);

    const observable = this.http.get<DashboardDto>(`${environment.apiBase}network/dashboard`, {
      params: this.globalFiltersService.getHttpParams()
    });

    // TODO: when filtersSubscription receives new data, make observable re-run it's HTTP request and emit a new response

    return observable; // Make this observable emit new data 
  }

}

I'm using Angular 8 and RxJS 6, so the most modern way possible would be preferable.

UPDATE: Working implementation

export class GlobalFiltersService {

  private readonly _httpParams$: BehaviorSubject<{ [param: string]: string | string[]; }>;
  private start: Moment;
  private end: Moment;

  constructor() {
    this._httpParams$ = new BehaviorSubject(this.getHttpParams());
  }

  public setDateFilter(start: Moment, end: Moment) {
    this.start = start;
    this.end = end;
    this._httpParams$.next(this.getHttpParams());
  }

  public get httpParams$() {
    return this._httpParams$.asObservable();
  }

  public getHttpParams() {
    return {
      start: this.start.toISOString(),
      end: this.end.toISOString()
    };
  }

}

export class DashboardDataService {

  private _dashboard$: Observable<DashboardDto>;

  constructor(
    private readonly http: HttpClient,
    private readonly globalFiltersService: GlobalFiltersService
  ) { }

  public getDashboard(): Observable<DashboardDto> {
    if (!this._dashboard$) {
      // Update the dashboard observable whenever global filters are changed
      this._dashboard$ = this.globalFiltersService.httpParams$.pipe(
        distinctUntilChanged(isEqual), // Lodash deep comparison. Only replay when filters actually change.
        switchMap(params => this.http.get<DashboardDto>(`${environment.apiBase}network/dashboard`, { params })),
        shareReplay(1),
        take(1)
      );
    }
    return this._dashboard$;
  }

}

export class DashboardResolver implements Resolve<DashboardDto> {

  constructor(private readonly dashboardDataService: DashboardDataService, private readonly router: Router) {}

  public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<DashboardDto> {
    return this.dashboardDataService.getDashboard();
  }

}

Solution

  • Try with the following:

    import {map, switchMap, shareReplay } from 'rxjs/operators';
    
    export class FooComponent {
      readonly dashboard$: Observable<DashboardDto>;
    
      ctor(...){
        this.dashboard$ = this.globalFiltersService.filters$.pipe(
          // map filter event to the result of invoking `GlobalFiltersService#getParams`
          map(_ => this.globalFiltersService.getHttpParams()),
          // maps the params to a new "inner observable" and flatten the result.
          // `switchMap` will cancel the "inner observable" whenever a new event is
          // emitted by the "source observable"
          switchMap(params => this.http.get<DashboardDto>(`${environment.apiBase}network/dashboard`, { params })),
          // avoid retrigering the HTTP request whenever a new subscriber is registered 
          // by sharing the last value of this stream
          shareReplay(1)
        );
      }
    }