Pull Data from Firebase and create list


#1

I am trying to pull in a userProfile list from my Firebase Database. You can see here the main branch is the user profile and I want to create a page that will display the list of userProfiles which will exist within my admin page.

I am running into this issue which states "Cannot read property ‘on’ of undefined at admin.ts:21 aka admin.ts within pages at line 21.

I am having trouble understanding what is causing my problem since the list will not load and in comparison to other examples I have not found anything that sticks out since it appears to be similar.

appData

Here is my AdminPage.ts

import { Component } from '@angular/core';
import { IonicPage, NavController } from 'ionic-angular';
import { AdminProvider } from '../../providers/admin/admin';

@IonicPage()
@Component({
  selector: 'page-admin',
  templateUrl: 'admin.html',
})
export class AdminPage {
  public employeeList: Array<any>;
  constructor(
    public navCtrl: NavController, 
    public adminProvider: AdminProvider,
  ) {
  
  }

  
  ionViewDidLoad() {
    this.adminProvider.getEmployeeList().on('value', employeeListSnapshot => {
      this.employeeList = [];
      employeeListSnapshot.forEach(snap => {
        this.employeeList.push({
          id: snap.key,
          name: snap.val().name,
          email: snap.val().email,
        });
        return false;
      });
    });     
  }
  

  goToEmployeeDetail(employeeId): void {
    this.navCtrl.push('EmployeeDetailPage', { employeeId: employeeId });
  }

}

Admin Page Html

<ion-header>
  <ion-navbar>
    <ion-title>Employee List</ion-title>
  </ion-navbar>
</ion-header>


<ion-content padding>
  <ion-list>
    <ion-item *ngFor="let employee of employeeList" (click)="goToEmployeeDetail(employee.id)">
      <h2>{{employee?.name}}</h2>
      <p>Email:
        <strong>{{employee?.email}}</strong>
      </p>
    </ion-item>
  </ion-list>
</ion-content>

And my Admin Provider.ts

import { Injectable } from '@angular/core';
import firebase from 'firebase';
import { Reference, ThenableReference } from '@firebase/database-types';
import { AngularFireDatabase, AngularFireList } from 'angularfire2/database'

@Injectable()
export class AdminProvider {

  public employeeListRef: Reference;
  constructor() {
     firebase.auth().onAuthStateChanged(user => {
      if (user) {
        this.employeeListRef = firebase.database().ref(`userProfile`);
      }
    });
  }

  getEmployeeList(): Reference {
    return this.employeeListRef;
  }

  getEmployeeDetail(employeeId: string): Reference {
    return this.employeeListRef.child(employeeId);
  }

}


#2

Generally, you always want to initialize all object and array properties referenced by components to something sensible like {} and []. Incidentally, make a proper Employee interface and use it instead of abusing any.


#3

Aren’t I doing such when I initialize the employeeList within the ionViewdidLoad()?

And any documentation you would recommend on creating a proper interface?


#4

No. You’re firing off an asynchronous task and not assigning to employeeList until it finishes. That’s too late. It must be done before the template tries to access it, which in practical terms means either constructor or inline at definition time (which I prefer for readability).

This, while informative, is probably overkill. You probably just want something like:

export interface Employee {
  id: string;
  name: string;
  title: string;
  department: Department;
  petName?: string;
}

It might seem like extra work at first, but it will pay for itself in reduced stupid typo bugs that are so easy to make in JavaScript, as well as allowing your IDE to do more intelligent code completion.


#5

I am still having trouble getting this code to work. I keep on running into this issue.

Error: Uncaught (in promise): TypeError: Cannot read property 'on' of undefined

I created a simple Employee interface.

export interface Employee {
    id: string,
    name: string,
    email: string
}

Then in my Admin Provider I am setting the reference to the branch of my database that I desire.

//Provider

import { Injectable } from '@angular/core';
import firebase from 'firebase';
import { Reference } from '@firebase/database-types';
import { Employee } from '../../interfaces/employee'


@Injectable()
export class AdminProvider {
  
  
 // employeeList: Array<Employee>;

  public employeeListRef: Reference;
  constructor() {
     firebase.auth().onAuthStateChanged(user => {
      if (user) {
        this.employeeListRef = firebase.database().ref(`userProfile`);
      }
    });
  }

  getEmployeeList(): Reference {
    return this.employeeListRef;
  }

  getEmployeeDetail(employeeId: string): Reference {
    return this.employeeListRef.child(employeeId);
  }
  
}

Then within my Admin Page TS I am importing the employee interface to use for my array type and the Provider to get the reference. I have updated it to initialize the array in the constructor. This is where I am receiving the error when I call the Provider to get the reference to the database branch to get the snapshots of each object within the branch.

//Page

import { Component } from '@angular/core';
import { IonicPage, NavController } from 'ionic-angular';
import { AdminProvider } from '../../providers/admin/admin';
import { Employee } from '../../interfaces/employee';

import firebase from 'firebase';

@IonicPage()
@Component({
  selector: 'page-admin',
  templateUrl: 'admin.html',
})
export class AdminPage {

  public employeeList: Array<Employee>;
 
  constructor(public adminProvider: AdminProvider) {
    this.adminProvider.getEmployeeList().on('value', employeeListSnapshot => {
      this.employeeList = [];
      employeeListSnapshot.forEach(snap => {
        this.employeeList.push(snap.val());
        return false;
      });
    });     
  }
  
}

And my Admin html to display it.

<ion-header>
  <ion-navbar>
    <ion-title>Employee List</ion-title>
  </ion-navbar>
</ion-header>


<ion-content padding>
  <ion-list>
    <ion-item *ngFor="let employee of employeeList" (click)="goToEmployeeDetail(employee.id)">
      <h2>{{employee}}</h2>
      <p>Email:
        <strong>{{employee.email}}</strong>
      </p>
    </ion-item>
  </ion-list>
</ion-content>


#6

You still have literally exactly the same problem.

getEmployeeList returns a variable which has not yet been assigned, so it is undefined.


#7

Am I not assigning it in my constructor as a reference to that branch in my database?


#8

Deja vu

Besides that, let me ask you an important base question. Why do you only fetch employees on auth change? Isn’t security handled by your server, so that you could fetch employees at any time and only employees relevant to your user would be returned? All you do is check if user exists, so I can’t understand the point of that.


#9

Didn’t placing it in the constructor accomplish that? Sorry I am new to ionic and firebase.

Admin Page ts

//Page

import { Component } from '@angular/core';
import { IonicPage, NavController } from 'ionic-angular';
import { AdminProvider } from '../../providers/admin/admin';
import { Employee } from '../../interfaces/employee';

import firebase from 'firebase';

@IonicPage()
@Component({
  selector: 'page-admin',
  templateUrl: 'admin.html',
})
export class AdminPage {

  public employeeList: Array<Employee> = [];
 
  constructor(public adminProvider: AdminProvider) {
    this.adminProvider.getEmployeeList().on('value', employeeListSnapshot => {
      //this.employeeList = [];
      employeeListSnapshot.forEach(snap => {
        this.employeeList.push(snap.val());
        return false;
      });
    });     
  }
  
}

And removed the auth change from the provider. I thought I could use it for authorization to prevent a user who does not have the required role from entering this section of the app but I am not sure if that is necessary after the initial login.

Admin Provider ts

//Provider

import { Injectable } from '@angular/core';
import firebase from 'firebase';
import { Reference } from '@firebase/database-types';
import { Employee } from '../../interfaces/employee'


@Injectable()
export class AdminProvider {
  
 public employeeListRef: Reference;
 constructor() {
    this.employeeListRef = firebase.database().ref(`userProfile`);
 }

  getEmployeeList(): Reference {
    return this.employeeListRef;
  }

  getEmployeeDetail(employeeId: string): Reference {
    return this.employeeListRef.child(employeeId);
  }
  
}


#10

So, let’s address the role thing. You aren’t doing any role checking there, you’re just checking if user exists. This also isn’t secure, you need to lock that down on the server side. I honestly don’t know how to do that in firebase, but I’m sure it’s a thing.

Constructor vs ionPageDidLoad vs ngOnInit is (mostly) irrelevant. The time between a constructor call and a lifecycle event is like one one billionth of a second or something. What actually takes time is calling a server and sending data over the internet. Even if it’s only 100ms, that’s insanely slower than your code executes. When you call firebase.auth(), that’s exactly what is happening. The event onAuthStateChanged only fires after your app calls out to firebase and returns the user. You put code that runs after that in that code block because it runs asynchronously. You don’t want all your code to just sit and wait for 100ms, otherwise your app would be insanely slow.

This has absolutely nothing to do with Ionic or Firebase, this is basic asynchronous programming, which is a core part of just vanilla javascript. I’d recommend reading up on that a bit especially promises, callbacks, and async in general in JS.


#11

A metaphor I find useful in thinking about how asynchronous/reactive programming works is the proverbial “message in a bottle”. Every time you type the following construct:

foo => {
  helpIamShipwreckedOnADesertIsland();
}

Think of the inside of the {} as being sealed in a bottle and tossed into the sea. The bottle-tosser (the surrounding function and anybody reliant on what goes on inside the {}) has no intrinsic clue as to when (or even if, in most cases) the message will get received.

So you have a couple of choices, which each make more or less sense in different situations. One we’ve covered: preinitializing things defensively like employeeList: Array<Employee> = [];. That tends to be my preferred choice when dealing with templates, because Angular’s change detection takes care of doing what needs to be done as soon as the bottle washes ashore.

The second is to expose futures instead of raw data. That tends to be more appropriate for use in providers, so I would try to refactor getEmployeeList to return Observable<Employee[]>. This should have the added benefit of abstracting implementation details (such as the very existence of Firebase) away from all the page classes, so if you end up switching to a different storage solution (or adding caching or whatnot), all you have to modify is the single provider.


#12

I figured it out a few days ago but figured I’d post my updated code. If you have any recommendations for improvement if you see it I’d appreciate it. Plus thanks for the help and patience my friends.


  export class AdminPage {

  employeeListRef: AngularFireList<Profile>; 

  employeeList: Observable<Profile[]>;

  constructor(
    public navCtrl: NavController,
    public navParams: NavParams,
    public adminProvider: AdminProvider,
    public database: AngularFireDatabase,
    private afAuth: AngularFireAuth,
    private actionSheetCtrl: ActionSheetController
  ) {

      this.employeeListRef = this.database.list('userProfile', 
        ref=> ref.orderByChild('lastName'));

      this.employeeList = this.employeeListRef.snapshotChanges()
        .map(
          changes => {
            return changes.map(c => ({
              key: c.payload.key, ...c.payload.val()
            }))
          }
        );
    }

#14

Howdy BigVinDieselFan,

Something bugs me with your code, I think it’s about new ionic standards.

Have fun with @ionic