Ionic view rendering (and erroring) before data has chance to load

I’m writing my first Ionic 2 app and am struggling to get my head around something that I didn’t have to worry about in Ionic 1.

I have a list view which lists items fetched from an API via an ajax call. This works fine, the template doesn’t break whilst waiting for the data to load. But and here is my issue, when you click on an item in the list view, you go to a detail view (which makes another ajax call to fetch additional details). This view renders before the ajax call has completed, all the template vars error and I’m really not sure why. :frowning:

Here’s my Service which handles the ajax calls:

import {Injectable} from '@angular/core';
import {Http} from '@angular/http';
import 'rxjs/Rx';

@Injectable()
export class DataService {
    http: any;
    baseUrl: String;
    apiKey: String;

    constructor(http:Http) {
        this.http = http;
        this.baseUrl = 'http://localhost:8100/api';
        this.apiKey = 'xxx';
    }

    getList(token, page) {
        return this.http.get(this.baseUrl + '/list?key=' + this.apiKey +  '&token=' + token + '&page=' + page + '&per_page=20')
            .map(res => res.json());
    }

    getDetails(token, ref) {
        return this.http.get(this.baseUrl + '/details/' + ref + '?key=' + this.apiKey + '&token=' + token)
            .map(res => res.json());
    }
}

1st page (list) which contains the nav push to the details page (this all works fine):

import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';
import { Storage } from '@ionic/storage';
import { DataService } from '../../app/services/data.service';
import { ItemPage } from '../item/item';

@Component({
	templateUrl: 'list.html'
})
export class ListPage {
    token: string;
    page: number = 1;
    items: any;

    constructor(public navCtrl: NavController, private dataService:DataService, private storage: Storage) {
	}

    getItems(token, page) {
        this.dataService.getItemsByFilter(token, page).subscribe(
            data => {
                this.items = data.items;
            },
            err => {
                console.log(err);
                // handle expired token and redirect
            }
          );
    }

    ngOnInit() { 
        this.storage.get('token').then((val) => {
            this.token =  val;
            this.getItems(this.token, this.page);
        });
    }

    viewDetailsPage(ref) {
        // I could perform the second ajax call (to get the details) within this function but this doesn't feel right...
        this.navCtrl.push(ItemPage, {
            ref: ref
        })
    }
}

2nd page (details) which has the issue:

import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';
import { Storage } from '@ionic/storage';
import { DataService } from '../../app/services/data.service';

@Component({
    selector: 'details',
    templateUrl: 'details.html'
})
export class DetailsPage {
    token: string;
    item: any;
    ref: string;

    constructor(public navCtrl: NavController, private dataService:DataService, private storage: Storage, public params:NavParams) {
        this.ref = params.get('ref');
    }

    getDetail(token, ref) {
        this.dataService.getDetails(token, ref).subscribe(
            data => {
                // this completes after the view has rendered, so the view errors
                this.item = data;
                console.log('this.item = ');
                console.log(this.item);
            },
            err => {
                console.log(err);
                // handle expired token and redirect
            }
          );
    }

    ngOnInit() {
        console.log('on init ran...');

        this.storage.get('token').then((val) => {
            this.token =  val;
            this.getDetail(this.token, this.ref);
        });
    }
}

Every var in my template errors, for example, the first is:

> Runtime Error
> Cannot read property 'reference' of undefined
Am I missing something obvious or is there a better way to go about this? All the tutorials I find have the ajax call in the class and not in a service which can't be right...

Here’s the list page template:

<ion-header>
    <ion-navbar>
        <ion-title>Items</ion-title>
    </ion-navbar>
</ion-header>

<ion-content>
    <ion-list>
        <button ion-item *ngFor="let item of items" (click)="viewDetailsPage(item.reference)" icon-start>
            <h2>{{item.reference}}</h2>
            <p>{{item.type.title}}</p>
            <ion-badge item-end>{{item.status.title}}</ion-badge>
        </button>
    </ion-list>
</ion-content>

and the details page template:

<ion-header>
    <ion-navbar>
        <ion-title>Item</ion-title>
    </ion-navbar>
</ion-header>

<ion-content>
        <h4>{{item.reference}}</h4>
</ion-content>

what am I missing, why is the second (details) template erroring?

Give your item a useful “empty” state in the constructor.

2 Likes

Could you give me an example? I’ve tried: this.item = {} which makes no difference.

The data returned by the ajax call and used for item is a similar structure to:

{
    reference: "AA12344",
    something: "else",
    status: {
        color: "#ffffff",
        text: "some text"
    }
}

I’ve noticed that by changing the type in the item page to item: any = {} that the data in the first level of the object resolves correctly and is displayed when loaded without error. Things in nested objects are not though and contain to error.

Are you sure this is an error in the template? I don’t see ref being used in the “details page” template.

Sorry typo on copy and paste - have corrected. It should and does indeed say reference.

I think I should be using the ? safe navigation operator. I’m guessing it works without error in the list page due to *ngFor?

1 Like

Correct, in the list page that code is only executed if items is not empty any more.

and the correct way to do the same thing in details is to use the ? or an *ngIf`?

That are ways not to render it. You could also initiate the variable with an object that has the property with an empty value.

1 Like

Just my personal opinion, but the three major options in descending order of preference:

  • initialize all component properties so that the template does not have to care at all about any of this
  • wall off potentially problematic elements with *ngIf
  • ?.

It is my belief that as part of the contract between template and controller, the controller is responsible for all bound properties being in a sane state. The template should not have to think defensively on this front.

1 Like