Angular2: How to load data before rendering the component?

TypescriptAngularAngular2 ServicesAngular2 Http

Typescript Problem Overview


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);
}

Typescript Solutions


Solution 1 - Typescript

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;
    }

Solution 2 - Typescript

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}}
////

Solution 3 - Typescript

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionTom AalbersView Question on Stackoverflow
Solution 1 - TypescriptGünter ZöchbauerView Answer on Stackoverflow
Solution 2 - TypescriptAlirezaView Answer on Stackoverflow
Solution 3 - Typescriptuser2965814View Answer on Stackoverflow