0

I am new at Angular 5 + Angular Material. I was reading through the docs and trying to get a grip on the best way to load the table. I found that you could create a DataSource class and use the connect method to connect to an observable and load the table.

My task: Parse through a message and get an array of id's, then call the backend to get the corresponding object for each id. Present the list of objects in a data table.

My current solution: In my service I pass the object to getAllPatients(objet) then I get the list of object Id's, I then loop through the array and call getPatient(patient) for each object. I then subscribe to the result of getPatient, I then push the result into a list and sort the list then use a Subject.next to push out an event which contains a list of patients which is a global variable in my patientService. In my data tables DataSource class I pass the subject in the connect method.

My issue: I am not sure if there is any real unsubscribing happening, and also I am not sure if this is the cleanest approach.. as when I leave the page the calls will still continue... My biggest concern is that if you enter the page then leave and go back in quickly will it cause both batches of calls to continue and then have 2 of each object? It seems as if it doesn't happen but i am a bit worried.

Code:

Functions from my service:

getPatientDemographics(id): Observable<any> {
    return this.http.get(this.userUrl + id )
  } 

  getAllPatients(details) {
    this.patients = []
    var membersObj = details.getMembersObj()
    if (membersObj){
      for (var member of membersObj.member) {
        this.getPatientDemographics(details.getMemberId(member)).subscribe(
          data => {
            this.patients.push(new Patient(data))
            this.patients.sort(this.sortingUtil.nameCompare)
            this.patientSubject.next(this.patients)
            console.log(`success id ${details.getMemberId(member)}`)
          },
          error => {
            console.log(`member id ${details.getMemberId(member)}`)
            this.patientSubject.error('errr')
          }
        )
      }
    }else {
      console.log(`member fail ${JSON.stringify(membersObj)}`)
    }

  }

Data Source class of the table:

export class PatientDataSource extends DataSource<any> {
  constructor(private patientService: PatientService) {
    super();
  }
  connect(): Subject<any[]> {
    return this.patientService.patientSubject;
  }
  disconnect() {}
}
6
  • The code you're showing is ok. No need to unsubscribe from a HttpClient call, it completes all observables it returns immediately after emitting first data. Who's calling getAllPatients? Seems that keeps happening after navigating away Commented Mar 28, 2018 at 17:02
  • Ah you just edited. Oki i can write an example on how you can go observables all the way and not need to subscribe yourself, will take a while as i'm restricted to a tablet atm lol Commented Mar 28, 2018 at 17:04
  • Meh, it's too complicated to type this on a tablet. Will be back with a stackblitz link tomorrow if you didn't solve it yet :D tip: Start by making details into a subject in which you next instead of calling getAllPatients, then pipe that to the dataSource through at least map, filter, switchMap, zip from rxjs. Commented Mar 28, 2018 at 17:21
  • if you manage to do that, mat-table will handle unsubscription when it's destroyed, killing the entire observable chain Commented Mar 28, 2018 at 17:22
  • @funkizer I will have to take a look at a bit later but at the moment I'am running in to some other issues at the moment also.. is there anyway we can message each other? Thanks for following up! Commented Mar 28, 2018 at 18:29

1 Answer 1

1

As promised, an example: https://stackblitz.com/edit/angular-enbzms?file=app%2Fsome.service.ts

So what's happening there: In the service, have a method which returns a BehaviorSubject of the details object you needed for making those HTTP calls. Pipe it through SwitchMap, in which you spread and map all the member objects into separate HTTP.get calls (simulated with a timer here). Zip will wait until all the HTTP observables have finished, then return you the result array always in the same order as the original array.

Then you just need to return service.getObservableForDataSource() in the connect -method of your DataSource. MatTable will subscribe on creation and unsubscribe on destruction.

If you look at the console while at stackblitz, you can see that if you click emit details and shortly after click hide table (which perfectly emulates leaving the page), the console logging stops right there, as the entire Observable chain 'dies' when MatTable unsubscribes from it.

In this case, a simple async pipe is there simulating a MatTable, but it works the same way.

To adhere to SO rules, I'll copy the code behind the Stackblitz link here also, but I recommend just following the link to Stackblitz :)

some.service.ts

import { Injectable } from '@angular/core';
import { timer } from 'rxjs/observable/timer';
import { zip } from 'rxjs/observable/zip';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
import { map, switchMap, filter, tap } from 'rxjs/operators';
@Injectable()
export class SomeService {
  constructor() { }
  details$: BehaviorSubject<any> = new BehaviorSubject(null);
  loading$: BehaviorSubject<boolean> = new BehaviorSubject(false);
  getPatientDemographics(id): Observable<any> {
    // Emulate an API call which takes a random amount of time
    return timer(100 + Math.random() * 1500).pipe(
      map((n: number) => {
        return {
          id,
          created: new Date().getTime()
        };
      })
    );
  }
  setDetails(details: any) {
    this.details$.next(details);
  }
  getObservableForDataSource() {
    // In your DataSource.connect(): return service.getObservableForDataSource()
    return this.details$.pipe(
      tap(() => this.loading$.next(true)),
      tap(data => console.log('Details in the pipe', data)),
      map(details => details.getMembersObj()),
      filter(membersObj => !!membersObj), // Leave out possible nulls
      map(membersObj => membersObj.member), // Pass on just the array of members
      switchMap(this.getPatients.bind(this)), // Switch the observable to what getPatients returns
      tap(data => console.log('End of pipe', data)),
      tap(() => this.loading$.next(false)),
    );
  }
  getPatients(members: any[]) {
    return zip(
      ...members.map(member => this.getPatientDemographics(member.id).pipe(
        tap(data => console.log('Received patient demog.', data)),
        // The end result will be in the same order as the members array, thanks to 'zip'
      ))
    );
  }
}

app.component.ts

import { Component } from '@angular/core';
import { SomeService } from './some.service';
import { Observable } from 'rxjs/Observable';
@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  tableVisible = true;
  dataSourceObservable: Observable<any>;
  constructor(public service: SomeService) { }
  start() {
    const mockDetails = {
      getMembersObj: () => {
        return {
          member: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }]
        }
      }
    };
    // In your table datasource class's connect(), you would simply
    // return service.getObservableForDataSource() 
    this.dataSourceObservable = this.service.getObservableForDataSource();
    this.service.setDetails(mockDetails);
  }
}

app.component.html

<h2>Fake Table</h2>
<p>
    <button (click)="start()">Emit the details</button>
</p>
<p>
    <button (click)="tableVisible=!tableVisible">{{tableVisible?'Hide table: emulate navigating away from this route' : 'Show table'}}</button>
</p>
<div *ngIf="tableVisible">
    <div *ngIf="dataSourceObservable | async as data">
        <pre>{{data|json}}</pre>
    </div>
    <i *ngIf="service.loading$|async">Loading...</i>
</div>
Sign up to request clarification or add additional context in comments.

1 Comment

After chatting with OP, he needed the table to update while it's loading each row. Replacing zip with concat enables that. stackblitz.com/edit/angular-enbzms?file=app%2Fsome.service.ts

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.