[SOLVED] Ionic 3 and Angular 4 - Uncaught (in promise): TypeError: Cannot read property 'title' of undefined

#10

Wait, what? This makes no sense when the error is the iterator item in a for-each loop (or better, ngFor), right?

I’m getting errors on objects that indeed exist and are created on the go, how am i supposed to initialize them first?

this is an example:

<ion-content padding>
  <ion-card class="paragraph-style" padding *ngFor="let i of lastWeekData">
    <img src="{{ i.url }}"/>
    <h1 style="color: white">{{ i.title }}</h1>
    <h6 style="color: lightgray">{{ i.date }}</h6>
    <p style="color: antiquewhite" text-left>{{ i.explanation }}</p>
  </ion-card>
</ion-content>

I indeed initialize the item in the constructor before populating it:

this.lastWeekData = [];

I’ll omit how i fill the array, it’s something really basic like this:

this.lastWeekData[0] = result;
...
this.lastWeekData[6] = result;

The array has contents, as you can see:
image

But i still get the ‘url’ undefined error:
error

Everything works as expected when using the Elvis operator in the view, for all the variables.

#11

Please don’t. It’s extremely germane.

Maybe it does at that point, but not when the template sees it. That’s why how and especially when you are filling the array with what is so important. If it’s done in stages, so that at some point the template is iterating across array elements that are not objects (e.g. null or undefined), you would see this error.

This isn’t how I would phrase this. The error is being triggered by whatever the iterator is pointing at at a particular moment, not the iterator itself.

#12

An easy fix for your initialization problem is to first check if thereŕe any items in your array, that’s replacing

<ion-card class="paragraph-style" padding *ngFor="let i of lastWeekData">

with
<ion-card class="paragraph-style" padding *ngFor="let i of lastWeekData" *ngIf="lastWeekData.length !== 0">

Another way is to apply some old dirty tricks, like iterating over the result of a function:
<ion-card class="paragraph-style" padding *ngFor="let i of retriveLastWeekData()">

not recommended because depending on your use case, it’s most likely adds unnecessary watchers.
Another dirty trick is initializing your array inside a setTimeout, like:

setTimeout(() => { 
  /* initialize your array below this comment */ 
}, 0);

remember that even though your using Typescript for developing Ionic apps, it finally compiles to plain old Javascript with some nice extras, so all old Javascript tricks do work.

Best regards.

#13

Unnecessary. ngFor handles empty arrays properly. You mentioned that you wouldn’t actually recommend either of your other suggestions, and I’ll strengthen that to say I have never come across a situation where either was warranted.

#14

Okay: i’m filling the array with data from a web service, i didn’t really think about the template loading before i get the data. This is the code (and it’s repeated 7 times, for each day of the week)

        
        this.HttpProvider.getJsonData('https://api.nasa.gov/planetary/apod?api_key=<privateAPIkey>&date='+this.formatDate(this.beforeYesterdayDate))
            .then(result => {

                this.lastWeekData[0] = result;

            }).catch((error) => {

            //DEBUGGING****
            console.log('Errore loading data from the API!', error);
            //*************
        });

Yeah you’re 100% correct. I’m kinda new to both Angular and Ionic so take my explanations with a grain of salt.

What’s the ideal solution here, then?

#15

@Fieel It seems to me that your error is not because the array is empty, but because some item inside the array is undefined.

You could log your array where you make changes to it to see the items inside it:

.then(result => {
    this.lastWeekData[0] = result; // you only change the array here? you only assign an item to index 0?
    console.log('lastWeekData', this.lastWeekData); // log the whole array
 })

Also, make sure result is defined before you add it to the array:

if (result) {
    this.lastWeekData[0] = result;
}

Or you can filter the array to return only the existing items:

this.lastWeekData[0] = result;
this.lastWeekData = this.lastWeekData.filter(item => !!item);

If you assign an item to an array index, make sure the new index is the one right after the (current) last. If you do the following it could give you problems in certain circumstances:

let a = [];
a[0] = { id: 1 };
a[2] = { id: 3 };
// a.length === 3, a[1] === undefined

Prefer to use push, if possible:

this.lastWeekData.push(result);

Instead of

this.lastWeekData[index] = result;

2 Likes
Dynamically pass parameters into functions directly from the view
#16

@lucasbasquerotto nailed this. All I can add is that if for whatever aesthetic reason you want the array to always have 7 slots, then you can initialize it like so:

lastWeekData = [ {}, {}, {}, {}, {}, {}, {} ];

…and then infill each individual slot with results from the backend, and you should not see any errors.

1 Like
#17

Despite all the suggestions from @lucasbasquerotto and @rapropos my view is still loading way faster than my data. I can’t basically use double curly brackets without the elvis parameter otherwise i get the same old error.

I can’t even do a comparison because i have no data to compare when the view loads, thus another error: this time i can’t use elvis operators so i’m basically blocked.

    <small><span *ngIf="i.copyright">{{i?.copyright}}</span></small>
#18

This happens only when iterating more than one element in the view. If i fetch only “one day” of data i’m not getting the error, if i insert in the array also new data the error shows as soon as it’s iterating the second time. No idea what’s happening.

week.html ( not using Elvis notation and working at the moment )

<ion-content padding>
  <ion-card class="stile-paragrafi testo-paragrafi spacing-carte" padding *ngFor="let i of data">
    <img (click)="showFullScreenImage(i.hdUrl)" class="stile-immagine" src="{{ i.url }}"/>
    <h1 class="stile-titolo">{{ i.title }}</h1>
    <h6 class="stile-data">{{ i.date }}</h6>
    <small><span *ngIf="i.copyright">{{i.copyright}}</span></small>
    <p class="stile-testo">{{ i.explanation }}</p>
  </ion-card>
</ion-content>

week.ts ( works only when this.data is declared this.data = [ {}, {}, {}, {}, {}, {}, {} ]; )

@Component({
    selector: 'page-week',
    templateUrl: 'week.html',
})
export class WeekPage {
    titolo: any;

    todayDate: any;
    pastDates: any;

    //APOD data container
    data: any[];

    constructor(public navCtrl: NavController,
                public navParams: NavParams,
                private HttpProvider: HttpProvider,
                private loading: LoadingProvider,
                private photoViewer: PhotoViewer) {

        this.titolo = "Last week";
        this.todayDate = new Date();
        this.pastDates = [];

        //This works ONLY if i use this annotation..
        //this.data = [] will not work
        this.data = [ {}, {}, {}, {}, {}, {}, {} ];

        loading.showLoading();
        this.getAPOD(7);
        loading.hideLoading();
    }

    showFullScreenImage(hdUrl){
        this.photoViewer.show(hdUrl);
    }

    getAPOD(days){
        //fill the past dates in the array
        for (var _i = 0; _i < days; _i++) {
            this.pastDates.push(this.todayDate.setDate(this.todayDate.getDate() - 1));
        }

        //fill the data array with APOD data according to past dates
        for (let $i in this.pastDates){
            this.HttpProvider.getSpecificAPOD(this.pastDates[$i])
                .subscribe(data => {
                    this.data[$i] = data;
                });
        }
    }
}

http.ts ( i use these 2 methods to retrieve data from the API )

    //get APOD data from a specific date
    getSpecificAPOD(date) {
        return this.http.get('https://api.nasa.gov/planetary/apod?api_key='+this.APIkey+'&date='+this.formatDate(date))
            .map(res => res)
            .catch(error => Observable.throw(error.json() || 'Server Error'));
    }

    //converts from number(nr of seconds) to date(YYYY-MM-DD)
    public formatDate(date: number):string {
        let d = new Date(date),
            month = '' + (d.getMonth() + 1),
            day = '' + d.getDate(),
            year = d.getFullYear();

        if (month.length < 2) month = '0' + month;
        if (day.length < 2) day = '0' + day;

        return [year, month, day].join('-');
    }

This works at the moment but my mail goal was to implement a setting to edit how many days of data you want to show in this page, so the this.data array size should be dynamic and using this kind of notation won’t work.

#19

Push isn’t “pushing” stuff in my array in the correct order so i can’t use it. Why is this happening? I thought push would add the new element to the tail of the array.

If push worked correctly it would fix all my problems, i can declare this:
this.data = [];
and use this

for (let $i in this.pastDates){
            this.HttpProvider.getSpecificAPOD(this.pastDates[$i])
                .subscribe(data => {
                    this.data.push(data);
                });
        }

and this is the problem i’m talking about:
CORRECT DATE ORDER (data[$i]=*)
correct
WRONG DATE ORDER (push)
wrong

#20

@Fieel you could use 2 arrays. One can have undefined objects inside it and you use it to put the objects in the correct order, and then use another one that you use in your html without the undefined objects, filtering the defined ones from the first array:

Change the following:

.subscribe(data => {
    this.data[$i] = data;
});

into:

.subscribe(data => {
    this.orderedData[$i] = data;
    this.data = this.orderedData.filter(item => !!item);
});

and declare the attributes in your class/component:

private orderedData: T[] = [];
public data: T[] = []; // use this in your html, as you are already using
1 Like
#21

@lucasbasquerotto nice, it’s working! I’d love to understand what i just did though, i’m not familiar with these notations:

private orderedData: T[] = [];
this.data = this.orderedData.filter(item => !!item);

i started using typescript for the first time with Ionic, this might be the cause…

#22

Incidentally, date-fns provides a format function that would eliminate the need for you to deal with all the gnarliness of formatDate().

1 Like
#23

Thanks @rapropos, i’ll see if it’s convenient to implement the library!

@lucasbasquerotto now that i used that weird notation i can’t build for android anymore! When i run ionic cordova build android i get these errors

            Cannot find name 'T'.

      L32:  private orderedData: T[] = [];
      L33:  public data: T[] = []; // use this in your html, as you are already using

#24

@Fieel T is just the type of object, it could be some interface, like Item, SomeObjectType, it could be any:

private orderedData: any[] = [];
public data: any[] = []

or just:

private orderedData = [];
public data = []

But I advise to use interfaces insteady of any to make the code more legible.

2 Likes
#25

Okay, well, looks like i need to understand how to properly use typescript before asking more questions here.

Thank you so much!

#26

@Fieel About this code:

this.data = this.orderedData.filter(item => !!item);

This applies Array.filter to create a new array with only the defined items in orderedData.

Because orderedData has objects or undefined as items, if it is an object the value !!item will be true and if it is undefined it will be false.

Javascript objects when used in boolean contexts are seen as true, but when a context needs a real boolean object, you can just use !! before it (negation of negation) resulting in the original value as a real boolean, otherwise you could receive typescript errors about incorrect type (if you used filter(item => item) it would work in javascript, but in typescript it would be a type error).

Update

Now that I tried, filter(item => item) seems to work too (I don’t know if the problem happened only in previous versions of typescript, or if I was just mistaken).

Update 2

It seems it was an error in older versions, but not now:

#27

What I think I would do in this situation is to move all the scheduling logic into the provider. I always try to design providers so that they give pages exactly what they want, instead of making the pages cobble stuff together from pieces exposed by the provider. With that viewpoint in mind, I’m thinking something like this: (untested and needs better error handling, though)

interface Payload {
  // idk what goes in here
}

interface DayRecord {
  when: Date;
  displayWhen: string;
  payload: Payload;
}

declare function require(mn: string): any;
let format = require('date-fns/format');
let addDays = require('date-fns/addDays');

export class Provider {
  getDay(day: Date): Observable<DayRecord> {
    return this.getSpecificAPOD(day).pipe(
      map((apod) => {
        return {
          when: day,
          displayWhen: format(day, "YYYY-MM-DD"),
          payload: apod,
        };
      }));
    }

  getDays(starting: Date, ndays: number): Observable<DayRecord[]> {
    return forkJoin(range(0, ndays).pipe(
      mergeMap((doff) => this.getDay(addDays(starting, doff)))));
  }
}
1 Like
#28

Thanks for useful answer. That’s worked for me.

#29

Thank You, You are right.