user2304483
user2304483

Reputation: 1644

Wait for Data Before Rendering View in Angular 5

I am new to Angular 5 and was able to create view with access to service methods using subscribe but encounter problem in delay in render. The user see first "undefined" string and after 0.5 sec it change to the correct value.

Here is the .ts file for the component:

 public trips: Trip[];
  public numberOfUsers : Map<string, number> = new Map<string, number>();

  constructor(private tripService: TripService) { }

  ngOnInit() {
   this.getTrips();
  }

  getTrips() {
    this.tripService.getTripForMe().subscribe(
      data => { this.trips = data;},
      err => console.error(err),
      () => this.initNumberOfUsers()
    );
  }

  initNumberOfUsers() {
    for (let i = 0; i < this.trips.length; i++) {
      let count;
      this.tripService.getNumberOfUsers().subscribe(
        data => { count = data},
        err => console.error(err),
        () => this.numberOfUsers.set(this.trips[i].id, count)
      );


      ;
    }
  }

  getNumberOfUsers(data) {
    return this.numberOfUsers.get(data.id);
  }

The method initNumberOfUsers is called after the first subscribe finish which is good, but the getNumberOfUsers(data) called probably before and get "undefined".

Here is the relevant part from the template ("data" is current value of a ngFor loop):

<span [innerText]="getNumberOfUsers(data)"></span>

Is there a way to make sure that both subscribe methods are sequentially finalized before rendering ?

UPDATE

I was able to create a resolver and the concole show the object but, I have difficulties to pass the data to the component.

Reolver code:

import { Injectable } from '@angular/core';
import { Router, Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Trip} from ....
import { TripService } from .....
import { Observable } from 'rxjs/Rx';

@Injectable()
export class TripsResolve implements Resolve<Trip[]> {

   public trips: Trip[];

   constructor(private service: TripService) {}

   resolve(route: ActivatedRouteSnapshot, 
           state: RouterStateSnapshot) {
     this.service.getTrips().subscribe(
        data => { this.trips = data },
        err => console.error(err),
        () => console.log(this.trips)
      );
      return this.trips;
   }
}

From route

resolve: {
         trips: TripsResolve
      }

From module

providers: [
        ...
        TripsResolve,
        ...

Data is not pass/not avilavle in the component:

import { Component, OnInit } from '@angular/core';
import { Trip } from ...
import { Router, ActivatedRoute } from '@angular/router';

@Component({
  selector: ...
  templateUrl: ...
  styleUrls: ...
})
export class MyComponent implements OnInit {

    public trips: Trip[];

    constructor(private route: ActivatedRoute,
                private router: Router) {
    }

    ngOnInit() {
        this.route.data.forEach(data => {
            this.trips = data.trips;
            console.log(this.trips);  //can't see it
       });

    } 
}

Any idea how can I access/retrieve the data in the component ?

Upvotes: 28

Views: 95745

Answers (3)

Pandiyan Cool
Pandiyan Cool

Reputation: 6585

I have faced similar issues when I'm dealing with data through API calls

I have tried two solutions

One is using *ngIf

The DOM will populate only when the data is ready.

<div *ngIf="isLoaded">Text to show</div>

Another one is using resolver, before rendering view itself it will prepare the data. But sometimes resolver will keep your screen as blank if data loads takes too much time.

@Injectable()
export class HeroDetailResolve implements Resolve {
    constructor(private heroService: HeroService, private router: Router) { }

    resolve(route: ActivatedRouteSnapshot): Promise | boolean {
        let id = +route.params['id'];
        return this.heroService.getHero(id).then(hero => {
            if (hero) {
                return hero;
            } else { // id not found
                this.router.navigate(['/dashboard']);
                return false;
            }
        });
    }
}

So you have two options now.

I would recommend *ngIf. While loading of data is in progress, display some loader gif or some message to let the user know that something happening in the background.

Upvotes: 47

a2345sooted
a2345sooted

Reputation: 599

Check out resolvers in Angular.

Here is a link: https://angular.io/api/router/Resolve

Much like guards, they run before the component is fully created and prevent you from having to have if/else logic in your template for waits while loading.

If you don't want to use a resolver, you can also do something like:

<span *ngIf="myVariable; else stillLoading" [innerText]="getNumberOfUsers(data)"></span>

<ng-template #stillLoading><p>Loading...</p></ng-template>

myVariable would be whatever you want to be defined before you render the <span> tag. I just didn't want to look that closely into your map :)

The choice on whether to use a resolver or if/else in templates depends on what you want your UX to be like. There's probably other reasons too, but that one comes to mind first.

Upvotes: 3

iHazCode
iHazCode

Reputation: 771

You could combine the two subscriptions using a mergeMap. This way they will emit once the last sub's completed method is triggered.

Though in your case you may find it easier to simply initialize the numberOfUsers to null. Then in your template use ng-show to only display the span when numberOfUsers is not null.

ex:

<div *ngShow="numberOfUsers">
  <span [innerText]="getNumberOfUsers(data)"></span>
</div>

cheers

Upvotes: 1

Related Questions