Gilmo
Gilmo

Reputation: 89

*ngFor displaying data but throwing 'undefined' error

I have an *ngFor directive that is functioning and displaying data in my browser as expected. Although my console in chrome is showing 'undefined' errors.

http.service.ts:

getExerciseProgress(exId: number): Observable<Exercise> {
    return this.http.get<Exercise>(this.baseURL + 'exercises/GetProgressionByExerciseId/' + exId)
  }

view-exercise.component.ts:

 exercise: Exercise;

  constructor(private http: HttpService) { }


  ngOnInit() {
    this.http.getExerciseProgress(7).subscribe(ex =>{
      this.exercise = ex;
      console.log(this.exercise);
    });
  }

The 7 passed as a paramater is for testing purposes and when I log the result, the object appears to be what I am looking for. An Exercise object with nested Progress array.

view-exercise.component.html:

<p *ngFor="let p of exercise.progress">{{ p.bpm }}</p>

The above line is the one throwing the following message in my Chrome console window. "ERROR TypeError: Cannot read property 'progress' of undefined". Despite this my browser is displaying the correct information.

Client side models (just in case its relevant):

export class Exercise {

id: number;
description: string;
progress: Progress[];

constructor(id: number, description: string, progress: Progress[]){
    this.id = id;
    this.description = description;
    this.progress = progress;
}

export class Progress {

id: number;
dateAttempted: Date;
bpm: number;

constructor(id: number, dateAttempted: Date, bpm: number){
    this.id = id;
    this.dateAttempted = dateAttempted;
    this.bpm = bpm;
}

}

Thanks in advance

Upvotes: 4

Views: 3107

Answers (3)

msanford
msanford

Reputation: 12227

It's a race condition: when the template first loads, this.exercise is still undefined. Then, once the Observable resolves and a value is assigned, a change detection cycle is triggered, the *ngFor is run and you see the values.

There are two typical patterns to fix this:

Use the elvis operator (probably best in your case because you only have that one accessor):

<p *ngFor="let p of exercise?.progress">{{ p.bpm }}</p>

?. means "access the property to the right of this operator if the operand to the left is defined". Written this way, it's obvious that they can be chained: exercise?.progress?.timer?.started

Or use a guard on a container element, which better when you have many accessors and don't want to repeat ?. everywhere:

<ng-container *ngIf="exercise">
  <p *ngFor="let p of exercise.progress">{{ p.bpm }}</p>
<ng-container>

In the above example I used an <ng-container /> because it's not rendered into the DOM, but you can just as easily use that on a real element, like a <div />. That's commonly used in the *ngIf="exercise; else no-data" pattern, where #no-data is another ng-template that replaces the div while you're loading your data.


FYI Side-note, because Angular uses polyfills, you can safely use template strings in your TypeScript. Meaning, you can write

this.baseURL + 'exercises/GetProgressionByExerciseId/' + exId

as

`${this.baseURL}/exercises/GetProgressionByExerciseId/${exId}`

which some people find easier to read.

Upvotes: 5

nircraft
nircraft

Reputation: 8478

Your DOM seems to be looking for the progress object inside exercise before the service call is finished and you have the data available.

You should loop over it when it's available , that means API call is finished and you have data:

<p *ngFor="let p of exercise?.progress">{{ p.bpm }}</p>

Or use *ngIf to achieve it.

Upvotes: 0

Johan Rin
Johan Rin

Reputation: 1950

Can you try this : <p *ngFor="let p of exercise?.progress">{{ p.bpm }}</p>

My guess is that angular try at first to display something even if your data is not init.

Upvotes: 0

Related Questions