Handling Objects from Firebase within Ionic Framework

First Published: 2 July 2018
Updated on: 24 July 2019

This is the third post of a 7 post series that will teach you how to build an app using Ionic Framework and Firebase from scratch.

We learned about authentication and how Promises work, now it is time to add some more functionality to our app, we are going to get to work with objects from the firestore database, while we create a profile page for our users.

I decided to go with a user profile because it can attack two main problems at once, working with Objects and updating the data in our Firebase authentication.

I think that is enough for an intro, so let’s jump into business!

Setup

The first thing we are going to do is to set up everything we will need for this part of the tutorial. We will be creating a profile page and a profile data service.

Remember that we created the actual files in the first chapter, now we need to start building on top of them.

The first thing we will do is to create a link to the profile page, so go to home.page.html and create a button in the header that navigates to the profile page:

<ion-header>
  <ion-toolbar>
    <ion-title></ion-title>
    <ion-buttons slot="end">
      <ion-button routerLink="/profile">
        <ion-icon slot="icon-only" name="person"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

Creating the User Profile service

Now into a bit more complicated pieces, we are going to create our profile service, the idea for this service is that it lets us store our profile information in Firebase’s firestore database and also change our email and password from our profile data.

This service should have a function for:

  • Getting the user profile
  • Updating the user’s name
  • Updating the user’s date of birth (I always save this stuff so I can surprise my users later)
  • Updating the user’s email both in the firestore database and the auth data
  • Changing the user’s password.

The first thing we need to do is to import Firebase, so open services/user/profile.service.ts and add firebase:

import * as firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';

We are going to create and initialize two variables:

public userProfile: firebase.firestore.DocumentReference;
public currentUser: firebase.User;

constructor() {
  firebase.auth().onAuthStateChanged(user => {
    if (user) {
      this.currentUser = user;
      this.userProfile = firebase.firestore().doc(`/userProfile/${user.uid}`);
    }
  });
}

We are going to use userProfile as a document reference to the current logged in user, and currentUser will be the user object.

We’re wrapping them inside an onAuthStateChanged() because if we get the user synchronously, there’s a chance it will return null when the service is initializing.

With the onAuthStateChanged() function we make sure to resolve the user first before assigning the variable.

Now we can start creating our functions, let’s start with a function that returns the user’s profile from the database:

getUserProfile(): firebase.firestore.DocumentReference {
  return this.userProfile;
}

Since we already initialized userProfile, we can return it in this function, and we will handle the result inside the profile page.

Next we’re going to create a function to update the user’s name:

updateName(firstName: string, lastName: string): Promise<any> {
  return this.userProfile.update({ firstName, lastName });
}

We’re using .update() here because we only want to update the firstName and lastName properties, if we were to use .set() to write to the database, it would delete everything under the user’s profile and replace it with the first and last name.

.update() also returns a promise, but it is void, meaning it has nothing inside, so you use it to see when the operation was completed and then perform something else.

Next function in line would be the one to update the user’s birthday, this is pretty much the same thing as the updateName() function, with the slight difference that we are updating a different property:

updateDOB(birthDate: string): Promise<any> {
  return this.userProfile.update({ birthDate });
}

Now is where things get a little trickier, we are going to change the user’s email address, why is it tricky? Because we are not only going to alter the email from the database, we are going to change it from the authentication service too.

That means that we are changing the email the user uses to log into our app, and you cannot call the change email function and have it magically work.

This is because some security-sensitive actions (deleting an account, setting a primary email address, and changing a password) require that the user has recently signed-in.

If you perform one of these actions, and the user signed in too long ago, the operation fails with an error. When this happens, re-authenticate the user by getting new sign-in credentials from the user and passing the credentials to reauthenticate.

I am going to go ahead and create the function and then break it down for you to understand better:

updateEmail(newEmail: string, password: string): Promise<any> {
  const credential: firebase.auth.AuthCredential = firebase.auth.EmailAuthProvider.credential(
    this.currentUser.email,
    password
  );

  return this.currentUser
    .reauthenticateWithCredential(credential)
    .then(() => {
      this.currentUser.updateEmail(newEmail).then(() => {
        this.userProfile.update({ email: newEmail });
      });
    })
    .catch(error => {
      console.error(error);
    });
}

Here’s what’s going on:

  • We are using firebase.auth.EmailAuthservice.credential(); to create a credential object, Firebase uses this for authentication.

  • We are passing that credential object to the re-authenticate function. My best guess is that Firebase does this to make sure the user trying to change the email is the actual user who owns the account. For example, if they see the user added email and password recently they let it pass, but if not they ask for it again to avoid a scenario where the user leaves the phone unattended for a while, and someone else tries to do this.

  • After the re-authenticate function is completed we’re calling .updateEmail() and passing the new email address, the updateEmail() does as its name implies, it updates the user’s email address.

  • After the user’s email address is updated in the authentication service we proceed to call the profile reference from the firestore database and also refresh the email there.

The good thing about that being tricky is that now the updatePassword() function will be smooth for you!

updatePassword(newPassword: string, oldPassword: string): Promise<any> {
  const credential: firebase.auth.AuthCredential = firebase.auth.EmailAuthProvider.credential(
    this.currentUser.email,
    oldPassword
  );

  return this.currentUser
    .reauthenticateWithCredential(credential)
    .then(() => {
      this.currentUser.updatePassword(newPassword).then(() => {
        console.log('Password Changed');
      });
    })
    .catch(error => {
      console.error(error);
    });
}

You should probably get yourself a cookie, that was a lot of code, and your sugar levels need a refill, I am taking a 20-minute break myself to get some food…

…Alright, I am back, let’s analyze what you have now, right now you have a fully functional service that will handle all the profile related interactions between your application and Firebase.

It is great because you can call those functions from anywhere inside your app now, without copy/pasting a bunch of functions to make it work.

Now that it is working, we are going to be creating the profile page, and it is going to be the page where we display, add, and update our user’s profile information.

Creating the Profile Page

We are going to break this down into three parts, the view, the design, and the code.

The first thing we are going to do is the view before we get started I want to explain the logic behind it first.

Instead of having multiple views where you go to update pieces of the profile, I decided to create a single view of it, basically, whenever the user needs to update a property, she can click on it, and a small pop-up appears where she can add the information without leaving the page.

So, with that in mind, here’s the HTML for the header:

<ion-header>
  <ion-toolbar>
    <ion-buttons slot="start">
      <ion-back-button defaultHref="/home"></ion-back-button>
    </ion-buttons>
    <ion-title>Profile Page</ion-title>
    <ion-buttons slot="end">
      <ion-button (click)="logOut()">
        <ion-icon slot="icon-only" name="log-out"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

Like on the home page, we’re creating a header button to handle the logout functionality.

And inside your <ion-content> we’re going to create a list to start adding our update items:

<ion-content padding>
  <ion-list>
    <ion-list-header> Personal Information </ion-list-header>
  </ion-list>
</ion-content>

Right after the list header, let’s add the item to update our user’s name:

<ion-item (click)="updateName()">
  <ion-label>
    <ion-grid>
      <ion-row>
        <ion-col class="text-left" size="5"> Name </ion-col>
        <ion-col
          class="text-center"
          size="7"
          *ngIf="userProfile?.firstName || userProfile?.lastName"
        >
          {{userProfile?.firstName}} {{userProfile?.lastName}}
        </ion-col>
        <ion-col
          size="7"
          class="placeholder-profile text-center"
          *ngIf="!userProfile?.firstName"
        >
          <span> Tap here to edit. </span>
        </ion-col>
      </ion-row>
    </ion-grid>
  </ion-label>
</ion-item>

It should be easy to understand, the labels take the left part of the grid, and the values take the right part.

SIDE-NOTE: The question mark used in the main object userProfile?.firstName is called the Elvis operator, it tells the template first to make sure the object is there before accessing or trying to access any of its properties.

If there’s no value, we will show a placeholder that says: “Tap here to edit” this will let our user know that they need to touch there to be able to select the profile items.

Right after the update name item, we’re going to add an option to update the date of birth:

<ion-item>
  <ion-label class="dob-label">Date of Birth</ion-label>
  <ion-datetime
    displayFormat="MMM D, YYYY"
    pickerFormat="D MMM YYYY"
    [(ngModel)]="birthDate"
    (ionChange)="updateDOB(birthDate)"
  >
  </ion-datetime>
</ion-item>

We could have opened a modal to update the DoB, but using the (ionChange) function allows us to handle everything on the same page.

After updating the date of birth, add another function to update the user’s email & password:

<ion-item (click)="updateEmail()">
  <ion-label>
    <ion-grid>
      <ion-row>
        <ion-col class="text-left" size="5"> Email </ion-col>
        <ion-col class="text-center" size="7" *ngIf="userProfile?.email">
          {{userProfile?.email}}
        </ion-col>
        <ion-col
          size="7"
          class="placeholder-profile text-center"
          *ngIf="!userProfile?.email"
        >
          <span> Tap here to edit. </span>
        </ion-col>
      </ion-row>
    </ion-grid>
  </ion-label>
</ion-item>

<ion-item (click)="updatePassword()">
  <ion-label>
    <ion-grid>
      <ion-row>
        <ion-col class="text-left" size="5"> Password </ion-col>
        <ion-col class="text-center" size="7" class="placeholder-profile">
          <span> Tap here to edit. </span>
        </ion-col>
      </ion-row>
    </ion-grid>
  </ion-label>
</ion-item>

Now that the HTML is in place, we are going to create the styles for it (remember, CSS is not my thing, so if you can, improve upon this!)

ion-list-header {
  background-color: #ececec;
}

.text-center {
  text-align: center;
}

.text-left {
  text-align: left;
}

.placeholder-profile {
  color: #cccccc;
}

.dob-label {
  color: #000000;
  padding: 10px;
  max-width: 50%;
}

Nothing too weird, some margins and colors.

And now we’re ready to start coding the functionalities for this page, the first thing we’ll need to do is import everything we’ll use and inject into the constructor when necessary:

import { Component, OnInit } from '@angular/core';
import { AlertController } from '@ionic/angular';
import { AuthService } from '../../services/user/auth.service';
import { ProfileService } from '../../services/user/profile.service';
import { Router } from '@angular/router';

@Component({
  selector: 'app-profile',
  templateUrl: './profile.page.html',
  styleUrls: ['./profile.page.scss'],
})
export class ProfilePage implements OnInit {
  public userProfile: any;
  public birthDate: Date;
  constructor(
    private alertCtrl: AlertController,
    private authService: AuthService,
    private profileService: ProfileService,
    private router: Router
  ) {}

  ngOnInit() {}
}

We are importing AlertController because we will display alerts to capture the data the user is going to update.

We are importing our profile and authentication services because we need to call functions from both.

The userProfile variable will hold all the data from Firebase, and our birthDate variable will interact with our date picker component.

Now it is time to call our profile service and ask for the user’s profile, so right after our constructor, go ahead and create the function:

ngOnInit() {
  this.profileService
    .getUserProfile()
    .get()
    .then( userProfileSnapshot => {
      this.userProfile = userProfileSnapshot.data();
      this.birthDate = userProfileSnapshot.data().birthDate;
    });
}

Let’s break down what we did here:

  • We are using ngOnInit(), this is part of Angular’s lifecycle events, it is called after the view rendered.

  • We are calling the getUserProfile() function from our ProfileService service, and when it returns we are assigning the value from the object to our userProfile variable.

  • If the userProfile had a birthDate property stored it is going to assign it to the birthDate variable to use it in our date picker component.

It is time to start adding the functions to add, modify or log out our users.

First, we will create a logout function, since it is probably the easiest one:

logOut(): void {
  this.authService.logoutUser().then( () => {
    this.router.navigateByUrl('login');
  });
}

This is what you expected it to be (since you saw it in the previous chapter) it calls the logoutUser function and then it sets the LoginPage as our rootPage, so the user is taken to login without the ability to have a back button.

Now let’s move to updating our user’s name:

async updateName(): Promise<void> {
  const alert = await this.alertCtrl.create({
    subHeader: 'Your first name & last name',
    inputs: [
      {
        type: 'text',
        name: 'firstName',
        placeholder: 'Your first name',
        value: this.userProfile.firstName,
      },
      {
        type: 'text',
        name: 'lastName',
        placeholder: 'Your last name',
        value: this.userProfile.lastName,
      },
    ],
    buttons: [
      { text: 'Cancel' },
      {
        text: 'Save',
        handler: data => {
          this.profileService.updateName(data.firstName, data.lastName);
        },
      },
    ],
  });
  await alert.present();
}

We are creating a prompt here to ask users for their first and last name. Once we get them our “Save” button is going to call a handler, that is going to take those first and last name and send them to the updateName function of Profileservice.

For the birthday we have to do a bit more validation, the (ionChange) can trigger on page load so we want to make sure it’s not undefined.

updateDOB(birthDate: string): void {
  if (birthDate === undefined) {
    return;
  }
  this.profileService.updateDOB(birthDate);
}

Now email and password are going to be the same as the updateName function, keep in mind that we are changing the input types to email & password to get the browser validation for them:

async updateEmail(): Promise<void> {
  const alert = await this.alertCtrl.create({
    inputs: [
      { type: 'text', name: 'newEmail', placeholder: 'Your new email' },
      { name: 'password', placeholder: 'Your password', type: 'password' },
    ],
    buttons: [
      { text: 'Cancel' },
      {
        text: 'Save',
        handler: data => {
          this.profileService
            .updateEmail(data.newEmail, data.password)
            .then(() => {
              console.log('Email Changed Successfully');
            })
            .catch(error => {
              console.log('ERROR: ' + error.message);
            });
        },
      },
    ],
  });
  await alert.present();
}

async updatePassword(): Promise<void> {
  const alert = await this.alertCtrl.create({
    inputs: [
      { name: 'newPassword', placeholder: 'New password', type: 'password' },
      { name: 'oldPassword', placeholder: 'Old password', type: 'password' },
    ],
    buttons: [
      { text: 'Cancel' },
      {
        text: 'Save',
        handler: data => {
          this.profileService.updatePassword(
            data.newPassword,
            data.oldPassword
          );
        },
      },
    ],
  });
  await alert.present();
}

That will create both functions and send the separate email & passwords to ProfileService.

Next Steps

And that is it, for now, at this point you should have a fully functional profile page, not only that, you also should have a better understanding of working with Documents in Firestore.

If you are running into any issues, send me an email, and I will be happy to help you debug it.

Next Lesson: Handling Lists.