Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular2 template trying to render value before value returned from http.get

I have an angular2 project as follows:

package.json:

{
  "name": "self-assessment",
  "version": "1.0.0",
  "scripts": {
    "postinstall": "npm run typings install",
    "lite": "lite-server",
    "gulp": "gulp",
    "start": "concurrent \"npm run gulp\" \"npm run lite\" ",
    "typings" : "typings"
  },
  "license": "ISC",
  "dependencies": {
    "angular2": "2.0.0-beta.3",
    "systemjs": "0.19.6",
    "es6-promise": "^3.0.2",
    "es6-shim": "^0.33.3",
    "reflect-metadata": "0.1.2",
    "rxjs": "5.0.0-beta.0",
    "zone.js": "0.5.11"
  },
  "devDependencies": {
    "autoprefixer": "^6.2.1",
    "cssnano": "^3.4.0",
    "concurrently": "^1.0.0",
    "gulp": "^3.9.0",
    "gulp-ext-replace": "^0.2.0",
    "gulp-imagemin": "^2.4.0",
    "gulp-postcss": "^6.0.1",
    "gulp-sourcemaps": "^1.6.0",
    "gulp-typescript": "^2.10.0",
    "gulp-uglify": "^1.5.1",
    "lite-server": "^2.0.1",
    "postcss": "^5.0.13",
    "postcss-scss": "^0.1.3",
    "precss": "^1.3.0",
    "typings":"^0.6.8",
    "tsd": "^0.6.5-beta"
  }
}

app.component.ts:

import {Component} from 'angular2/core';
import {AssessmentListComponent} from "./assessment-list.component";
import {HomeComponent} from "./home.component";
import {ApiService} from "../services/api.service";
import {RouteConfig, ROUTER_DIRECTIVES, ROUTER_PROVIDERS} from 'angular2/router';
import {AssessmentComponent} from "./assessment.component";

@Component({
    selector: 'my-app',
    templateUrl: 'app/components/app.component.html',
    directives: [
        ROUTER_DIRECTIVES,
    ],
    providers: [
        ApiService,
        ROUTER_PROVIDERS,
    ]
})
@RouteConfig([
    {path: '/home', name: 'Home', component: HomeComponent, useAsDefault: true},
    {path: '/assessments', name: 'Assessments', component: AssessmentListComponent},
    {path: '/assessment/:id', name: 'Assessment', component: AssessmentComponent},

])
export class AppComponent {

    title = "Welcome to Self-Assessment"

}

assessment.component.ts:

import {Component, OnInit} from 'angular2/core';
import {AssessmentModel} from '../models/assessment.model';
import {RouteParams} from 'angular2/router';
import {ApiService} from '../services/api.service';

@Component({
    selector: 'assessment-component',
    templateUrl: 'app/components/assessment.component.html',
})
export class AssessmentComponent implements OnInit{
    assessment: AssessmentModel;
    errorMessage: any;

    constructor(
        private _apiService: ApiService,
        private _routeParams: RouteParams
    ) {}


    ngOnInit():any {
        let id = +this._routeParams.get('id');
        this.getAssessment(id);
    }

    private getAssessment(id: number) {
        this._apiService.getAssessment(id)
            .subscribe(
                assessment => this.assessment = assessment,
                error => this.errorMessage = <any>error
            );
    }

    goBack() {
        console.log(this.assessment);
    }
}

api.service.ts:

import {Injectable} from 'angular2/core';
import {Http, Response} from 'angular2/http';
import {Observable} from 'rxjs/Observable';

@Injectable()
export class ApiService {
    constructor(private _http: Http) {}

    private _baseApiUrl = 'http://localhost:8000/';

    getAssessments() {
        return this.getObjectList('assessments', 'json');
    }

    getAssessment(id: number) {
        return this.getObject('assessments', id, 'json');
    }

    getObjectList(listName:string, listExtension: string) {

        var url =  this._baseApiUrl + listName + '.' + listExtension;

        return this._http.get(url)
            .map(response => response.json())
            .catch(this.handleError);
    }

    getObject(objectName:string, objectId:number, objectExtension: string) {

        var url =  this._baseApiUrl + objectName + '/' + objectId + '.' + objectExtension;

        return this._http.get(url)
            .map(response => response.json())
            .catch(this.handleError);
    }

    private handleError(error: Response) {
        console.error(error);
        return Observable.throw(error.json().error || 'Server error');
    }
}

and finally assessment.component.html:

<h3>{{ assessment.id }}</h3>
<button (click)="goBack()">Go Back 2</button>

When the page is called with the url: http://localhost:3000/assessment/4 it makes the appropriate calls to the back-end twice and generates the following errors in the console:

Uncaught EXCEPTION: TypeError: Cannot read property 'id' of undefined in [{{ assessment.id }} in AssessmentComponent@0:4]
ORIGINAL EXCEPTION: TypeError: Cannot read property 'id' of undefined
ORIGINAL STACKTRACE:
TypeError: Cannot read property 'id' of undefined
    at AbstractChangeDetector.ChangeDetector_AssessmentComponent_0.detectChangesInRecordsInternal (viewFactory_AssessmentComponent:30:28)
    at AbstractChangeDetector.detectChangesInRecords (http://localhost:3000/node_modules/angular2/bundles/angular2.dev.js:8116:14)
    at AbstractChangeDetector.runDetectChanges (http://localhost:3000/node_modules/angular2/bundles/angular2.dev.js:8099:12)
    at AbstractChangeDetector._detectChangesInViewChildren (http://localhost:3000/node_modules/angular2/bundles/angular2.dev.js:8184:14)
    at AbstractChangeDetector.runDetectChanges (http://localhost:3000/node_modules/angular2/bundles/angular2.dev.js:8103:12)
    at AbstractChangeDetector._detectChangesContentChildren (http://localhost:3000/node_modules/angular2/bundles/angular2.dev.js:8178:14)
    at AbstractChangeDetector.runDetectChanges (http://localhost:3000/node_modules/angular2/bundles/angular2.dev.js:8100:12)
    at AbstractChangeDetector._detectChangesInViewChildren (http://localhost:3000/node_modules/angular2/bundles/angular2.dev.js:8184:14)
    at AbstractChangeDetector.runDetectChanges (http://localhost:3000/node_modules/angular2/bundles/angular2.dev.js:8103:12)
    at AbstractChangeDetector.detectChanges (http://localhost:3000/node_modules/angular2/bundles/angular2.dev.js:8088:12)
ERROR CONTEXT:
[object Object]ExceptionHandler.call @ angular2.dev.js:1206(anonymous function) @ angular2.dev.js:12591NgZone._notifyOnError @ angular2.dev.js:13635collection_1.StringMapWrapper.merge.onError @ angular2.dev.js:13539Zone.run @ angular2-polyfills.js:1247NgZone._notifyOnTurnDone @ angular2.dev.js:13450(anonymous function) @ angular2.dev.js:13565zoneBoundFn @ angular2-polyfills.js:1220lib$es6$promise$asap$$flush @ angular2-polyfills.js:262

So it appears as though the {{ assessment.id }} is trying to render before the object is returned from the back-end. If I then click the "Go Back 2" button it will console log the object appropriately:

{"id":4,"title":"Assessment Title here!","description":"Here is a description","questions":["http://localhost:8000/questions/1.json","http://localhost:8000/questions/2.json","http://localhost:8000/questions/3.json","http://localhost:8000/questions/4.json","http://localhost:8000/questions/5.json","http://localhost:8000/questions/6.json","http://localhost:8000/questions/7.json","http://localhost:8000/questions/8.json","http://localhost:8000/questions/9.json","http://localhost:8000/questions/10.json","http://localhost:8000/questions/11.json","http://localhost:8000/questions/12.json","http://localhost:8000/questions/13.json"],"responses":[]}

"Interestingly" I'm not seeing problems in other places in the application (ie. can load and display assessment-lists just fine, etc). Also, I'm having the exact same problem in an Ionic2 app I was working with last night as a learning project.

Have googled my butt off, hopefully somebody knows the answer to this riddle.

Thanks!

like image 408
Gerald Avatar asked Mar 02 '16 18:03

Gerald


1 Answers

You have two options:

  1. use the safe navigation operator (formerly called the Elvis operator), {{ assessment?.id }}, which guards against null and undefined values
  2. use NgIf, <h3 *ngIf="assessment">{{assessment.id}}</h3>, which is more appropriate if you don't want elements added to the DOM until the data is available/populated

You likely don't have the issue with lists because NgFor handles the undefined, null or empty array case for you. Then when the data comes in, NgFor is re-evaluated.

like image 62
Mark Rajcok Avatar answered Sep 19 '22 21:09

Mark Rajcok