74

I am beginning to learn Angular2. I've been following the Heroes Tutorial provided at angular.io. All was working fine until, being annoyed by the clutter of HTML using the template, I used template URL in its place, and moved the HTML to a file named hero.html. The error that is generated is, "Cannot read property 'name' of undefined". Strangely, the heroes variable which points to an array of objects can be accessed so that ngFor will produce the correct amount of "li" tags according to the number of objects in the array. However, the data of the objects of the array cannot be accessed. Furthermore, even a simple variable holding some text, will not display using {{}} brackets in the HTML (see provided code).

app.component.ts

import { Component } from '@angular/core';
@Component({
  selector: 'my-app',
  templateUrl: './hero.html',
  styleUrls:['./styles.css']
})

export class AppComponent {
  title = 'Tour of Heroes';
  heroes = HEROES;
  selectedHero:Hero;

  onSelect(hero: Hero):void{
      this.selectedHero = hero;
  }
}

export class Hero{
   id: number;
   name: string;
}

const HEROES: Hero[] = [
   { id: 1, name: 'Mr. Nice' },
   { id: 2, name: 'Narco' },
   { id: 3, name: 'Bombasto' },
   { id: 4, name: 'Celeritas' },
   { id: 5, name: 'Magneta' },
   { id: 6, name: 'RubberMan' },
   { id: 7, name: 'Dynama' },
   { id: 8, name: 'Dr IQ' },
   { id: 9, name: 'Magma' },
   { id: 10, name: 'Tornado' }
];

hero.html

<h1>{{title}}</h1>
<h2>My Heroes</h2>
<ul class="heroes">
  <li *ngFor="let hero of heroes">
    <span class="badge">{{hero.id}}</span> {{hero.name}}
  </li>
</ul>
<h2>{{hero.name}} details!</h2>
<div>
    <label>id: </label>{{hero.id}}
</div>
<div>
    <label>name: </label>
    <input [(ngModel)]="selectedHero.name" placeholder="name">
<div>

Here is a photo:

enter image description here

Kashif Faraz Shamsi
  • 513
  • 1
  • 7
  • 21

7 Answers7

159

The variable selectedHero is null in the template so you cannot bind selectedHero.name as is. You need to use the elvis operator ?. for this case:

<input [ngModel]="selectedHero?.name" (ngModelChange)="selectedHero.name = $event" />

The separation of the [(ngModel)] into [ngModel] and (ngModelChange) is also needed because you can't assign to an expression that uses the elvis operator.

I also think you mean to use:

<h2>{{selectedHero?.name}} details!</h2>

instead of:

<h2>{{hero.name}} details!</h2>
JoSSte
  • 2,953
  • 6
  • 34
  • 54
Guilherme Meireles
  • 7,849
  • 2
  • 21
  • 15
  • Thank You. You have generally identified the problem. However, I am still having problems. I changed {{hero.name}} to {{selectedHero.name}} but still am getting the same error. I also updated the input as you instructed but the error persists. When the tag line is commented out everything but

    {{selectedHero.name}} details!

    is working fine. Therefore I know the selectedHero object is being used incorrectly.
    –  Sep 28 '16 at 19:23
  • 2
    Sorry, I forgot the elvis operator on the code. I edited the answer. It should be: {{selectedHero?.name}} – Guilherme Meireles Sep 28 '16 at 19:27
  • 1
    Awesome! I've just added the `?` and my code works perfectly. – Nhan Mar 22 '17 at 07:39
  • Thanks, adding `?` after my property fixed my issue for conditional data binding if the property exists. Can you elaborate what you mean by `because you can't assign to an expression that uses the elvis operator.` I did not do any of that ngModelChange stuff you described and my elvis operator works fine like this `[value]="this.data?.office?.name"` – TetraDev Aug 25 '17 at 21:46
  • 1
    @TetraDev Your example works because it's just a data binding [] directive and not also an event binding (). It would be nice if angular would parse the statement and identify these cases, but it does not currently. See https://github.com/angular/angular/issues/7697 – Ulfius Sep 20 '17 at 23:43
33

You just needed to read a little further and you would have been introduced to the *ngIf structural directive.

selectedHero.name doesn't exist yet because the user has yet to select a hero so it returns undefined.

<div *ngIf="selectedHero">
  <h2>{{selectedHero.name}} details!</h2>
  <div><label>id: </label>{{selectedHero.id}}</div>
  <div>
    <label>name: </label>
    <input [(ngModel)]="selectedHero.name" placeholder="name"/>
  </div>
</div>

The *ngIf directive keeps selectedHero off the DOM until it is selected and therefore becomes truthy.

This document helped me understand structural directives.

Tom
  • 341
  • 4
  • 6
  • This helped me get what I was looking for, thanks +1 – Gman Mar 09 '17 at 22:14
  • This helped me as well. It make sense after the fact since my component is trying to render an object that is filled when an async operation completes. Thanks! – Generaldeep Jun 18 '18 at 14:37
6

In order to avoid this, you could as well initialize the selectedHero member of your component to an empty object (instead of leaving it undefined).

In your example code, that would give something like this :

export class AppComponent {
  title = 'Tour of Heroes';
  heroes = HEROES;
  selectedHero:Hero = new Hero();

  onSelect(hero: Hero):void{
      this.selectedHero = hero;
  }
}
Cédric Françoys
  • 870
  • 1
  • 11
  • 22
5

You were getting this error because you followed the poorly-written directions on the Heroes tutorial. I ran into the same thing.

Specifically, under the heading Display hero names in a template, it states:

To display the hero names in an unordered list, insert the following chunk of HTML below the title and above the hero details.

followed by this code block:

<h2>My Heroes</h2>
<ul class="heroes">
  <li>
    <!-- each hero goes here -->
  </li>
</ul>

It does not instruct you to replace the previous detail code, and it should. This is why we are left with:

<h2>{{hero.name}} details!</h2>

outside of our *ngFor.

However, if you scroll further down the page, you will encounter the following:

The template for displaying heroes should look like this:

<h2>My Heroes</h2>
<ul class="heroes">
  <li *ngFor="let hero of heroes">
    <span class="badge">{{hero.id}}</span> {{hero.name}}
  </li>
</ul>

Note the absence of the detail elements from previous efforts.

An error like this by the author can result in quite a wild goose-chase. Hopefully, this post helps others avoid that.

2

This line

<h2>{{hero.name}} details!</h2>

is outside *ngFor and there is no hero therefore hero.name fails.

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
1

This worked for me:

export class Hero{
   id: number;
   name: string;

   public Hero(i: number, n: string){
     this.id = 0;
     this.name = '';
   }
 }

and make sure you initialize as well selectedHero

selectedHero: Hero = new Hero();
mics
  • 11
  • 3
0

In Angular, there is the support elvis operator ?. to protect against a view render failure. They call it the safe navigation operator. Take the example below:

The current person name is {{nullObject?.name}}

Since it is trying to access name property of a null value, the whole view disappears and you can see the error inside the browser console. It works perfectly with long property paths such as a?.b?.c?.d. So I recommend you to use it everytime you need to access a property inside a template.

JoSSte
  • 2,953
  • 6
  • 34
  • 54