简体   繁体   中英

Angular Table: piping observable data from a date range subject

I've a code that is working as expected but nonetheless I doubt whether the code is wrong and it is only working by accident due to an undocumented feature or the code is right and it is working as expected by design.

What I want is a material date range picker updating the datasource of another component's table.

I assume that the part passing the values through the template app.component.html

  <app-mytable ID="MyTable1" 
    [Start]="rangeFormGroup.value.start" 
    [End]="rangeFormGroup.value.end" ></app-mytable>

and then forwarding such values from the table mytable.component.ts to the mytable-datasource.ts

  private _Start: Date | undefined;
  @Input()
  public get Start(): Date | undefined {
    return this._Start;
  }
  public set Start(value: Date | undefined) {
    this._Start = value;
    this.dataSource.start.next(value);
  }
  private _End: Date | undefined;
  @Input()
  public get End(): Date | undefined {
    return this._End;
  }
  public set End(value: Date | undefined) {
    this._End = value;
    this.dataSource.end.next(value);
  }

is working and correct.

Now in the datasource I have the 2 dates/subject

export class MytableDataSource extends DataSource<MytableItem> {
  data: MytableItem[] = EXAMPLE_DATA;
  start: Subject<Date> = new Subject<Date>();
  end: Subject<Date> = new Subject<Date>();

and finally here is my doubt and my question, all about the part where I trigger the changes.

  let _start : Date 
  let _end : Date
  this.start.subscribe(update => _start = update);
  this.end.subscribe(update => _end = update);
  return merge(
      observableOf(this.data) ,this.paginator.page, this.sort.sortChange, 
      this.start.pipe(), this.end.pipe())
    .pipe(map(() => {
      console.log('merge piped');
      return this.getPagedData(
        this.getSortedData(
        this.filterByDateRange(_start, _end,
          [...this.data ])));
    }));

Even if my tests say the results are correct and the changes are propagated fine, I can't see in the code what guarantees the updates to _start and _end are always done before they are passed to filterByDateRange .

I think that those inside the parenthesis are two asynchronous events, therefore how is the sequence of the first action ( update =>... following the start/end's subscribe ) and the second action ( this.filterByDateRange(... following the start/end's pipe() in the merge ) really guaranteed to happen in the right order, the latter always after the former?

I'm much more convinced that the following is instead the correct approach by replacing the subscriber with a map, as explained in a similar answer .

You can't return an observable from subscribe but if you use map instead of subscribe then an Observable is returned.

  let _start : Date 
  let _end : Date
  return merge(
      // observableOf(this.data) ,
      // BTW you don't need this above
      this.paginator.page, this.sort.sortChange, 
      this.start.pipe(map(update => {
        _start = update;
        return update;})), 
      this.end.pipe(map(update => {
        _end = update;
        return update;})))
    .pipe(map(() => {
      console.log('merge piped');
      return this.getPagedData(
        this.getSortedData(
        this.filterByDateRange(_start, _end,
          [...this.data ])));
    }));

But

Since the _start/_end updates are side effects, I believe it's better to make them happen only in the unique final, merged stage:

  let _start : Date 
  let _end : Date
  return merge(
      this.paginator.page, this.sort.sortChange, 
      this.start.pipe(map(update => {
        return {'start':update};})), 
      this.end.pipe(map(update => {
        return {'end':update};})))
    .pipe(map((merged) => {
      if ('start' in merged ) {
        _start = merged.start;
      }
      if ('end' in merged ) {
        _end = merged.end;
      }
      console.log('merge piped');
      return this.getPagedData(
        this.getSortedData(
        this.filterByDateRange(_start, _end,
          [...this.data ])));
    }));

Lossy backpressure

Another reason why mapping could be better than the original subscriber is to apply a debounce (to user input), so that the consumer (maybe running queries) does not get overwhelmed (but that's outside the scope of this question).

IO Effect

Another real-world example is the http IO Effect, that would require (in functional language dictionary) a flatMap (obsolete, it's mergeMap ) composed with a cache (when the date range filter is included in the previous fetched data)

  let _start : Date 
  let _end : Date
  return merge(
      this.paginator.page, this.sort.sortChange, 
      this.start.pipe(map(update => {
        return {'start':update};})), 
      this.end.pipe(map(update => {
        return {'end':update};})))
    .pipe(map((merged) => {
      var mustLoad = false;
      if ('start' in merged ) {
        if (merged.start) {
          if (!_start || _start.getTime() > merged.start.getTime()) {
            mustLoad = true;
          }
          _start = merged.start;  
        }
      }
      if ('end' in merged ) {
        if (merged.end) {
          if (!_end || _end.getTime() < merged.end.getTime()) {
            mustLoad = true;
          }
          _end = merged.end;  
        }
      }
      if (mustLoad && _start && _end) {
        return {"cache":[], "mustLoad":true}
      } else {
        mustLoad = false;
      }
      return {"cache":this.data, "mustLoad":false};
    })).pipe(mergeMap ((a) => {
      if (a.mustLoad) {
        if (!this.database) {
          console.log("Database not initialized!");
          return observableOf([]);
        }
        console.log('firing API call');
        return this.database.getBrokerFees(_start,_end);
      }
      return observableOf( a.cache);
    })
      ).pipe(map(a => {
        const filtered = this.filterByDateRange(_start, _end, a);
        this.data = filtered;
        const sorted = this.getSortedData(filtered);
        var paged = [...sorted] // needs copy of sorted 
        paged  = this.getPagedData(paged); // paged works inline and modify input
        return paged;
      }));

Other Higher-Order Operators

See also this Comprehensive Guide to Higher-Order RxJs Mapping Operators: switchMap, mergeMap, concatMap (and exhaustMap).

Furthermore, since you are dealing with Reactive Forms , it's important that you realize that:

Every control in the form (select, input, checkbox, etc…) has a valueChanges property, an Observable that we can subscribe to in order to observe changes over time and transform values with RxJS operators in a functional way.

Hence I guess you could avoid the custom date subjects in your question if you directly feed the standard form observable into the merge map, though you might prefer the already defined subjects to implement your specific caching strategy.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM