177

I am trying to load an event from my API before the component gets rendered. Currently I am using my API service which I call from the ngOnInit function of the component.

My EventRegister component:

import {Component, OnInit, ElementRef} from "angular2/core";
import {ApiService} from "../../services/api.service";
import {EventModel} from '../../models/EventModel';
import {Router, ROUTER_DIRECTIVES, ROUTER_PROVIDERS, RouteConfig, RouteParams, RouterLink} from 'angular2/router';
import {FORM_PROVIDERS, FORM_DIRECTIVES, Control} from 'angular2/common';

@Component({
    selector: "register",
    templateUrl: "/events/register"
    // provider array is added in parent component
})

export class EventRegister implements OnInit {
    eventId: string;
    ev: EventModel;

    constructor(private _apiService: ApiService, 
                        params: RouteParams) {
        this.eventId = params.get('id');
    }

    fetchEvent(): void {
        this._apiService.get.event(this.eventId).then(event => {
            this.ev = event;
            console.log(event); // Has a value
            console.log(this.ev); // Has a value
        });
    }

    ngOnInit() {
        this.fetchEvent();
        console.log(this.ev); // Has NO value
    }
}

My EventRegister template

<register>
    Hi this sentence is always visible, even if `ev property` is not loaded yet    
    <div *ngIf="ev">
        I should be visible as soon the `ev property` is loaded. Currently I am never shown.
        <span>{{event.id }}</span>
    </div>
</register>

My API service

import "rxjs/Rx"
import {Http} from "angular2/http";
import {Injectable} from "angular2/core";
import {EventModel} from '../models/EventModel';

@Injectable()
export class ApiService {
    constructor(private http: Http) { }
    get = {
        event: (eventId: string): Promise<EventModel> => {
            return this.http.get("api/events/" + eventId).map(response => {
                return response.json(); // Has a value
            }).toPromise();
        }     
    }     
}

The component gets rendered before the API call in the ngOnInit function is done fetching the data. So I never get to see the event id in my view template. So it looks like this is a ASYNC problem. I expected the binding of the ev (EventRegister component) to do some work after the ev property was set. Sadly it does not show the div marked with *ngIf="ev" when the property gets set.

Question: Am I using a good approach? If not; What is the best way to load data before the component is starting to render?

NOTE: The ngOnInit approach is used in this angular2 tutorial.

EDIT:

Two possible solutions. First was to ditch the fetchEvent and just use the API service in the ngOnInit function.

ngOnInit() {
    this._apiService.get.event(this.eventId).then(event => this.ev = event);
}

Second solution. Like the answer given.

fetchEvent(): Promise<EventModel> {
    return this._apiService.get.event(this.eventId);
}

ngOnInit() {
    this.fetchEvent().then(event => this.ev = event);
}
Tom Aalbers
  • 4,574
  • 5
  • 29
  • 51

3 Answers3

152

update

original

When console.log(this.ev) is executed after this.fetchEvent();, this doesn't mean the fetchEvent() call is done, this only means that it is scheduled. When console.log(this.ev) is executed, the call to the server is not even made and of course has not yet returned a value.

Change fetchEvent() to return a Promise

     fetchEvent(){
        return  this._apiService.get.event(this.eventId).then(event => {
            this.ev = event;
            console.log(event); // Has a value
            console.log(this.ev); // Has a value
        });
     }

change ngOnInit() to wait for the Promise to complete

    ngOnInit() {
        this.fetchEvent().then(() =>
        console.log(this.ev)); // Now has value;
    }

This actually won't buy you much for your use case.

My suggestion: Wrap your entire template in an <div *ngIf="isDataAvailable"> (template content) </div>

and in ngOnInit()

    isDataAvailable:boolean = false;

    ngOnInit() {
        this.fetchEvent().then(() =>
        this.isDataAvailable = true); // Now has value;
    }
Akash Kumar Verma
  • 3,185
  • 2
  • 16
  • 32
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • 4
    Actually, your first comment worked beautifully. I only removed the entire `fetchEvent` function and just put the `apiService.get.event` function in the `ngOnInit` and it worked as I intendend. Thanks! I will accept you answer as soon it allows me too. – Tom Aalbers Feb 26 '16 at 15:29
  • For some reason, I used this approach on Angular 1.x. IMHO, this should be taken as a hack instead of a proper solution. We should do better in angular 5. – windmaomao May 02 '18 at 13:28
  • What exactly do you not like about that? I think both approaches are perfectly fine. – Günter Zöchbauer May 02 '18 at 13:39
66

You can pre-fetch your data by using Resolvers in Angular2+, Resolvers process your data before your Component fully be loaded.

There are many cases that you want to load your component only if there is certain thing happening, for example navigate to Dashboard only if the person already logged in, in this case Resolvers are so handy.

Look at the simple diagram I created for you for one of the way you can use the resolver to send the data to your component.

enter image description here

Applying Resolver to your code is pretty simple, I created the snippets for you to see how the Resolver can be created:

import { Injectable } from '@angular/core';
import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router';
import { MyData, MyService } from './my.service';

@Injectable()
export class MyResolver implements Resolve<MyData> {
  constructor(private ms: MyService, private router: Router) {}

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<MyData> {
    let id = route.params['id'];

    return this.ms.getId(id).then(data => {
      if (data) {
        return data;
      } else {
        this.router.navigate(['/login']);
        return;
      }
    });
  }
}

and in the module:

import { MyResolver } from './my-resolver.service';

@NgModule({
  imports: [
    RouterModule.forChild(myRoutes)
  ],
  exports: [
    RouterModule
  ],
  providers: [
    MyResolver
  ]
})
export class MyModule { }

and you can access it in your Component like this:

/////
 ngOnInit() {
    this.route.data
      .subscribe((data: { mydata: myData }) => {
        this.id = data.mydata.id;
      });
  }
/////

And in the Route something like this (usually in the app.routing.ts file):

////
{path: 'yourpath/:id', component: YourComponent, resolve: { myData: MyResolver}}
////
Alireza
  • 100,211
  • 27
  • 269
  • 172
  • 1
    I think you missed the route configuration entry under the `Routes` array (usually in the _app.routing.ts_ file): `{path: 'yourpath/:id', component: YourComponent, resolve: { myData: MyData}}` – Voicu Jan 19 '18 at 07:05
  • 1
    @Voicu, it's not supposed to cover all parts, but I agree with, it makes the answer more comprehensive, so I added... Thanks – Alireza Jan 19 '18 at 16:17
  • 5
    Just to correct the last part, the resolve parameter in the route needs to point to the resolver itself, not the data type, i.e. `resolve: { myData: MyResolver }` – Chris Haines Feb 23 '18 at 09:00
  • 1
    Is that MyData a model object that defines in MyService? – Isuru Madusanka Sep 13 '18 at 13:41
  • 1
    this solution is good for fetching data before the page loads but not for check logged user nor user roles. For that you should use Guards – Angel Q Nov 27 '19 at 09:51
65

A nice solution that I've found is to do on UI something like:

<div *ngIf="isDataLoaded">
 ...Your page...
</div

Only when: isDataLoaded is true the page is rendered.

user2965814
  • 735
  • 1
  • 6
  • 14
  • 3
    I think this solution is only for Angular 1 not Angular >= 2 because Angular's unidirectional data flow rule forbids updates to the view after it has been composed. Both of these hooks fire after the component's view has been composed. – zt1983811 Nov 16 '17 at 21:18
  • 3
    The div doesn't get rendeded at all. We don't intent to stop it from rendering, we want to delay it. The reason it doesn't work is because there is no two way binding in angular2. @phil can you explain how it worked for you ? – ishandutta2007 Nov 21 '17 at 06:51
  • 1
    ok I was using `ChangeDetectionStrategy.Push`, changing it to `ChangeDetectionStrategy.Default` makes it two way binded with template. – ishandutta2007 Nov 21 '17 at 07:16
  • 1
    angular 6. Works. – TDP Oct 23 '18 at 14:57