wwarby
wwarby

Reputation: 2051

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();
  }

}

Upvotes: 4

Views: 2366

Answers (2)

Ben Racicot
Ben Racicot

Reputation: 5895

Good question! I had to sync URL params, forms and query results. It led me down the architecture rabbit hole all the way to state management.

TL;DR When there are many elements reliant on most recent data you need to have highly accessible state on that data. The solution is more about architecture than which RXJS method to use.

Here is the service I built as an example stackblitz.com/edit/state-with-simple-service.

Here were my requirements. (directly applicable to the question's)

  1. share the state of all options (received from form components/URL)
  2. sync options with URL params
  3. sync all forms with options
  4. query for results
  5. share results

Here's the gist:

export class SearchService {
    // 1. results object from the endpoint called with the current options
    private _currentResults = new BehaviorSubject<any[]>([]);

    // 2. current state of URL parameters and Form values
    private _currentOptions = new BehaviorSubject<Params>({});

    // 3. private options object to manipulate
    private _options: Params = {};

Then access these with getters:

// Any component can subscribe to the current results from options query
public get results(): Observable<any[]> {
    return this._currentResults.asObservable();
}
// Any component can subscribe to the current options
public get options(): Observable<Params> {
    return this._currentOptions.asObservable();
}

Update the private Subjects anytime with next()

this._currentOptions.next(this._options);

Now you have state management without introducing a huge framework like redux.

Upvotes: 0

Jota.Toledo
Jota.Toledo

Reputation: 28434

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)
    );
  }
}

Upvotes: 1

Related Questions