Reputation: 153
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() {}
}
Upvotes: 0
Views: 2480
Reputation: 4897
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>
Upvotes: 1