0

I have a data.service.ts where I want to preload 2 arrays:

public aspects: DTO.Aspect[];
public reports: DTO.Report[];

constructor(dao: DaoService){
   // Getting Reports
   this.dao.getReports().subscribe(reps: DTO.Report[] => {
      this.reports = reps;
   })

   //Getting Aspects of each Report
   for(let i = 0; i < this.reports.length; i++){
      this.dao.getAspects(this.reports[i].reportId).subscribe(asps: DTO.Report[] => {
         this.aspects = asps;
      })
   }
}

FYI dao.service.ts

import { Injectable } from "@angular/core";
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { Observable, throwError } from "rxjs";
import { catchError, retry } from "rxjs/operators";
import { DTO } from "./dto";

@Injectable({
  providedIn: "root"
})
export class Dao {
  private static API: String =
    "https://apiapiapiapi.com";
  constructor(private http: HttpClient) {}

getAspects(repId: string): Observable<DTO.Aspect[]> {
    console.log(Dao.API + "aspect/report/" + repId);
    return this.http
      .get<DTO.Aspect[]>(Dao.API + "aspect/report/" + repId)
      .pipe(retry(3), catchError(this.handleErrors));
  }

getReports(): Observable<DTO.Report[]> {
    console.log(Dao.API + "report/current");
    return this.http
      .get<DTO.Report[]>(Dao.API + "report/current")
      .pipe(retry(3), catchError(this.handleErrors));
  }
}

After that I want to use these arrays everywhere in my app. For Example when I click in the sidebar on a report it should route me to a page where all the aspects of that report are displayed:

report.component.ts

routerLink: localhost:4200/report/512

import { Component, OnInit } from "@angular/core";
import { Router, ActivatedRoute, ParamMap } from "@angular/router";
import { DataService } from "@core/data.service";
import { DTO } from "@core/dto";
import { Dao } from "@core/dao.service";

@Component({
  selector: "app-report",
  template: "<div *ngFor='let aspect of aspects'>
                <span>{{aspect.aspectName}}</span>
             </div>",
  styleUrls: ["./report.component.scss"]
})
export class ReportComponent implements OnInit {
aspects: DTO.Aspect[] = [];

constructor(data: DataService, route: ActivatedRoute){
   // Get the Report ID from the URL
   this.route.paramMap.subscribe(params => {
      const reportId = params.get("reportId");

      // Use the Report ID (512) as parameter to find the Aspects of this Report
      this.aspects = this.data.aspects.filter(aspect => {   // aspects in data.service.ts are not loaded yet
         return aspect.reportId == reportId;
      })
   })
}
}

The problem is that the aspects in the data.service.ts are not necessarely loaded yet. I tried a few things by creating my own Observers or using Promises but the performance became even worse. So my question is:

How can I load data once and use them when they are ready?

  • paste also your dao service pls – Pedro Bezanilla Dec 19 '19 at 14:21
  • Your call to subscribe is false.it should be this.dao.getReports().subscribe(reps => this.reports = reps); Does this code even complile? – mwe Dec 19 '19 at 14:31
  • @mwe You were right. I typed this code as an example for my current problem live into my stackoverflow article. I have just edited it. – TheAmygdala Dec 19 '19 at 14:40
  • You could cache your data in localStor or sessionStore. – mwe Dec 19 '19 at 14:45
  • @PedroB. I dont know for what you need the dao service in this context but I added it in the article. There are just 2 methods which return observables. One with reports and one with aspects when givene the reportId as parameter – TheAmygdala Dec 19 '19 at 14:49
  • Are you allowing user to route before all the api calls gets completed? – Siddharth Pal Dec 19 '19 at 14:50
  • @SiddharthPal Yes, I do. And the site is supposed work that way. After a while the data is supposed to fill the gaps on the site. – TheAmygdala Dec 19 '19 at 14:55

2 Answers2

2

First of all I would suggest that you dig into rxjs, as it has a lot of functionality for this kind of stuff.

The issue with your code is that the for might be executed before getReports completes itself, so the for will not have access to those items yet.

This could be solved in a number of ways, this snippet will allow you to just wait for 1 observable instead of one per report and one per aspect.

public aspects: DTO.Aspect[];
public reports: DTO.Report[];

constructor(dao: DaoService) { }

public getAllAspects() {
    return this.dao.getReports().pipe(
        // switchMap, this is to pass in the result of the first observable into the second one 
        switchMap(reports =>
            this.getAspectsForReports(reports)
        )
    ),
}

public getAspectsForReports(reports) {
    let aspectsObservables = [];
    for (let i = 0; i < reports.length; i++) {
        aspectsObservables.push(this.dao.getAspects(reports[i].reportId));
    }

    return forkJoin(aspectsObservables); // Accepts an Array of ObservableInput or a dictionary Object of ObservableInput and returns an Observable that emits either an array of values in the exact same order as the passed array, or a dictionary of values in the same shape as the passed dictionary.  
}

Then I would do this before navigating to the Report page/component, then when page is loaded, the data will be already loaded in DataService

this.data.getAllAspects() // Will return an array of aspects
    .subscribe(aspects => {
        this.data.aspects = aspects;
        this.navigate(['/report/:id']);
    })

The issue with this approach is that the logic from getAspectsForReports will be called whenever you subscribe...

Usefull links:

Keff
  • 989
  • 7
  • 27
  • The problem with this approach is that the logic from getAspectsForReports() is executed every time that he subscribe to this.data.getAllAspects() – WSD Dec 19 '19 at 15:07
  • Although he asks "How can I load data once and use them when they are ready"... so you could just subscribe once when app is loaded. No need to subscribe more than once, if he saves the array in the Service, the he can just reference the property `data.aspects` – Keff Dec 19 '19 at 15:16
0

I would suggest exposing the Observables themselves rather than their values. So your service would look like:

    @Injectable({provideIn: 'root'})
    export class DataService {

        // With a ReplySubject no matter when you subscribe you'll get the last emitted value.
        private reports$: ReplaySubject<Array<DTO.Report>> = new ReplaySubject(1);
        private aspects$: ReplaySubject<Array<DTO.Aspects>> = new ReplaySubject(1);

        constructor(dao: DaoService){
           // Getting Reports
            this.dao.getReports().subscribe(this.reports$);

           this.reports$
                 .pipe(
                   switchMap(reports: Array<DTO.Report>)=> { 
                   // Getting Aspects of first Report only (for simplicity)             
                   return this.dao.getAspects(reports[0]);
                   })
                 .subscribe(this.aspects$);
        }

        getReports(): Observable<Array<DTO.Report>>{    
        return this.reports$.asObservable();
        }

        getAspects(): Observable<Array<DTO.Aspect>>{
        return this.aspects$.asObservable();
        }

   }

Then in the component you must subscribe to the getAspects() of your data.service.ts.

Here, I would also suggest to leverage the subscription to the async pipe instead of manually handle the subscription, but this is just my appreciation.

EDIT 1: If you are wondering how to deal with the for loop you have in your original question, please take a look to the answers posted here: Loop array and return data for each id in Observable

WSD
  • 3,243
  • 26
  • 38
  • 1
    Nice solution, although a bit convoluted for somebody new to Angular and RxJs. I would go for this myself I think! – Keff Dec 19 '19 at 15:03