50

In my Angular 2 application, I get an error:

Cannot read property 'title' of undefined.

This is a very simple component, just trying to get a bare minimum to work here. It hits my API controller (curiously multiple times), and it appears to hit the callback after an object is returned. My console.log outputs the object I would expect. Here is the full error:

TypeError: Cannot read property 'title' of undefined
    at AbstractChangeDetector.ChangeDetector_About_0.detectChangesInRecordsInternal (eval at <anonymous> (http://localhost:55707/lib/angular2/bundles/angular2.dev.js:10897:14), <anonymous>:31:26)
    at AbstractChangeDetector.detectChangesInRecords (http://localhost:55707/lib/angular2/bundles/angular2.dev.js:8824:14)
    at AbstractChangeDetector.runDetectChanges (http://localhost:55707/lib/angular2/bundles/angular2.dev.js:8807:12)
    at AbstractChangeDetector._detectChangesInViewChildren (http://localhost:55707/lib/angular2/bundles/angular2.dev.js:8877:14)
    at AbstractChangeDetector.runDetectChanges (http://localhost:55707/lib/angular2/bundles/angular2.dev.js:8811:12)
    at AbstractChangeDetector._detectChangesContentChildren (http://localhost:55707/lib/angular2/bundles/angular2.dev.js:8871:14)
    at AbstractChangeDetector.runDetectChanges (http://localhost:55707/lib/angular2/bundles/angular2.dev.js:8808:12)
    at AbstractChangeDetector._detectChangesInViewChildren (http://localhost:55707/lib/angular2/bundles/angular2.dev.js:8877:14)
    at AbstractChangeDetector.runDetectChanges (http://localhost:55707/lib/angular2/bundles/angular2.dev.js:8811:12)
    at AbstractChangeDetector.detectChanges (http://localhost:55707/lib/angular2/bundles/angular2.dev.js:8796:12)

The service (about.service.ts):

import {Http} from 'angular2/http';
import {Injectable} from 'angular2/core';
import {AboutModel} from './about.model';
import 'rxjs/add/operator/map';

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

    get() {
        return this._http.get('/api/about').map(res => {
            console.log(res.json()); // I get the error on the line above but this code is still hit.
            return <AboutModel>res.json();
        });
    }
}

The Component (about.component.ts):

import {Component, View, OnInit} from 'angular2/core';
import {AboutModel} from './about.model';
import {AboutService} from './about.service';
import {HTTP_PROVIDERS} from 'angular2/http';

@Component({
    selector: 'about',
    providers: [HTTP_PROVIDERS, AboutService],
    templateUrl: 'app/about/about.html'
})

export class About implements IAboutViewModel, OnInit {
    public about: AboutModel;

    constructor(private _aboutService: AboutService) {}

    ngOnInit() {    
        this._aboutService.get().subscribe((data: AboutModel) => {
            this.about = data;
        });
    }
}

export interface IAboutViewModel {
    about: AboutModel;
}

index.html

<script src="~/lib/systemjs/dist/system.src.js"></script>
<script src="~/lib/angular2/bundles/router.js"></script>
<script src="~/lib/angular2/bundles/http.js"></script>
<script src="~/lib/angular2/bundles/angular2-polyfills.js"></script>
<script src="~/lib/angular2/bundles/angular2.dev.js"></script>
<script src="~/lib/es6-shim/es6-shim.js"></script>
<script>
    System.config({
        packages: {
            app: {
                format: 'register',
                defaultExtension: 'js'
            },
            rxjs: {
                defaultExtension: 'js'
            }
        },
        map: {
            rxjs: "lib/rxjs"
        }
    });
    System.import('app/boot')
            .then(null, console.error.bind(console));
</script>
Steffi Keran Rani J
  • 3,667
  • 4
  • 34
  • 56
Ryan Langton
  • 6,294
  • 15
  • 53
  • 103

3 Answers3

62

Please include your view and model next time (app/about/about.html and about.model).

If you are returning an array, you can use the asyncPipe, which "subscribes to an Observable or Promise and returns the latest value it has emitted. When a new value is emitted, the async pipe marks the component to be checked for changes" hence the view will be updated with the new value.

If you are returning a primitive type (string, number, boolean) you can also use the asyncPipe.

If you are returning an object, I'm not aware of any way to use asyncPipe, we could use the async pipe, in conjunction with the safe navigation operator ?. as follows:

{{(objectData$ | async)?.name}}

But that looks a bit complicated, and we'd have to repeat that for each object property we wanted to display.

As @pixelbits mentioned in a comment, you can subscribe() to the observable in the controller and store the contained object into a component property. Then use the safe navigation operator or NgIf in the template:

service.ts

import {Injectable} from 'angular2/core';
import {Http} from 'angular2/http';
import 'rxjs/add/operator/map';  // we need to import this now

@Injectable()
export class MyService {
  constructor(private _http:Http) {}
  getArrayData() {
    return this._http.get('./data/array.json')
      .map(data => data.json());
  }
  getPrimitiveData() {
    return this._http.get('./data/primitive.txt')
      .map(data => data.text());   // note .text() here
  }
  getObjectData() {
    return this._http.get('./data/object.json')
      .map(data => data.json());
  }
}

app.ts

@Component({
  selector: 'my-app',
  template: `
    <div>array data using '| async':
      <div *ngFor="let item of arrayData$ | async">{{item}}</div>
    </div>
    <div>primitive data using '| async': {{primitiveData$ | async}}</div>
    <div>object data using .?: {{objectData?.name}}</div>
    <div *ngIf="objectData">object data using NgIf: {{objectData.name}}</div>`
  providers: [HTTP_PROVIDERS, MyService]
})
export class AppComponent {
  constructor(private _myService:MyService) {}
  ngOnInit() {
    this.arrayData$     = this._myService.getArrayData();
    this.primitiveData$ = this._myService.getPrimitiveData();
    this._myService.getObjectData()
      .subscribe(data => this.objectData = data);
  }
}

data/array.json

[ 1,2,3 ]

data/primitive.json

Greetings SO friends!

data/object.json

{ "name": "Mark" }

Output:

array data using '| async':
1
2
3
primitive data using '| async': Greetings SO friends!
object data using .?: Mark
object data using NgIf: Mark

Plunker

muttonUp
  • 6,351
  • 2
  • 42
  • 54
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • Mark, here you are using ? (Elvis operator) for the front end. What if you need the data in your service to make another call? I have a service.getId().flatMap( id => return service.getUser(id);).subscribe( data => console.log(data)) but my data is returning undefined even though it gets logged in the service. – Adam Mendoza Jan 08 '17 at 07:55
  • @AdamMendoza, have the service return the flatMap and then subscribe to it in the component. Also use `?` in the view/template, since the data is resolved asynchronously. If this isn't clear, I suggest that you post a new question showing your service and component. – Mark Rajcok Jan 10 '17 at 17:03
  • I have been searching and searching and yours is by far the clearest answer! thanks a million dude – Mike Axle Aug 03 '17 at 12:20
  • if you do not like using the [safe operator](https://angular.io/guide/template-syntax#the-safe-navigation-operator----and-null-property-paths) you take the about object `as` another object - see [angular-ngif-async-pipe](https://toddmotto.com/angular-ngif-async-pipe) – Kieran Apr 13 '18 at 05:21
  • What do you do if you have something like `*ngFor="let post of ((postsFiltered$ | async)['Original-content'])"` ? `)?['Original-content']` is not a valid syntax. – Ambroise Rabier Jun 22 '19 at 20:23
33

It looks like you have referred to about.title in the view about.html but the about variable is instantiated only after the http request is completed. To avoid this error you can wrap about.html with <div *ngIf="about"> ... </div>

TheKojuEffect
  • 20,103
  • 19
  • 89
  • 125
  • I've done this and still get the error. First element in about.html:
    Also added to about.component.ts: import {CORE_DIRECTIVES} from 'angular2/common'; directives: [CORE_DIRECTIVES],
    – Ryan Langton Jan 12 '16 at 03:31
  • 1
    I needed to add the following line in ngInit to get this to work: this.about = new AboutModel(); Seems less than ideal, that and the ngIf solution. Wish I could bind to an Observable from my html template, can I? – Ryan Langton Jan 12 '16 at 03:37
  • @RyanLangton Glad you got it working. Anyway, you can try `OnActivate` https://angular.io/docs/js/latest/api/router/OnActivate-interface.html . Related on that http://stackoverflow.com/questions/34734367/angular-2-router-resolve-server-request-before-opening-a-component – TheKojuEffect Jan 12 '16 at 04:12
  • Thanks for this, it was exactly my problem. – Joe Nov 12 '16 at 18:52
20

The previous answer is correct. You need to check if the variable is defined before use it in your template. Using HTTP request it need time to define it. use *ngIf to check. Example is provided from angular with https://angular.io/docs/ts/latest/tutorial/toh-pt5.html and example is http://plnkr.co/edit/?p=preview

<div *ngIf="hero">
  <h2>{{hero.name}} details!</h2>
<div>

You can check app/hero-detail.component [ts and html]

Zlatko Yankov
  • 201
  • 2
  • 3