Twitter LogoFacebook Logo
Angular Project 1: Building a Social Media Site Part 6: User Profiles with Cloud Firestore Database
Hello, welcome to another tutorial of the Complete Angular Course. In this video, we’ll focus on creating user profiles.
By: King

In this tutorial, we’ll focus on creating user profiles. 

To begin, have your project opened in the browser from where we left off in the previous video and log out. Then open Visual Studio Code.

localhost:4200

Creating the Profile Component

Open the terminal and create the Profile Component. Run the ng generate component command and then place it in a directory call tools.

ng generate component tools/Profile

Go into the Profile Component TypeScript file, get the selector and add it at the bottom of the App Component HTML page.

@Component({
  selector: 'app-profile',
  templateUrl: './profile.component.html',
  styleUrls: ['./profile.component.css']
})
export class ProfileComponent implements OnInit {

}

...

<router-outlet></router-outlet>

<app-profile></app-profile>

Modifying the Profile Component

1) Go into the Profile Component HTML page. 
2) Add a div element and call it profile
3) Inside the div, add another div and call it profile -content
4) Inside the content div, add an input, an textarea, and a button element.
5) For the button, use the mat-flat-button style and set the color to warn.

<div id="profile">
    <div id="profile-content">

        <input placeholder="Name">
        <textarea placeholder="Description"></textarea>
        <button mat-flat-button color="warn">Continue</button>

    </div>
</div>

Go to the Profile CSS file and add the selector for profile and profile -content.

#profile {
    position: fixed;
    top: 0;
    right: 0;
    height: 100vh;
    background-color: rgb(29, 29, 29);
    z-index: 9;
    transition-duration: .5s;
    overflow: hidden;
}

#profile-content {
    width: 100%;
    max-width: 500px;
    padding: 2em;
    box-sizing: border-box;
    -moz-box-sizing: border-box;
    -webkit-box-sizing: border-box;
}

Next, set the styles for the input and textarea.

input, textarea {
    width: 100%;
    padding: 1em;
    border-radius: 1em;
    margin-bottom: .5em;
    resize: none;
    outline: none;
    border: 1px solid gray;
    box-sizing: border-box;
    -moz-box-sizing: border-box;
    -webkit-box-sizing: border-box;
}

If we go to our app, we are almost there. We need to place the contents in the center and set when we want it to be visible.

Image from Codeible.com

Go to the Profile Component TypeScript file and add an input call show. This property will control when the component will be visible.

@Input() show: boolean;

Go to the Profile Component CSS file. In the profile selector, set display to flex, justify-content to center, and align-items: center to center the items.

#profile {
    position: fixed;
    top: 0;
    right: 0;
    height: 100vh;
    background-color: rgb(29, 29, 29);
    z-index: 9;
    transition-duration: .5s;
    overflow: hidden;

    display: flex;
    justify-content: center;
    align-items: center;
}

Add a class call active and idle. For active, set the width to 100vw. For idle, set the width to 0.

.active {
    width: 100vw;
}

.idle {
    width: 0;
}

Go to the Profile HTML page and add the ngClass directive to the profile div. 


We want to use the active class when show is true and we want to use the idle class when show is false.

<div id="profile"
     [ngClass]="{'active': show, 'idle': !show}">
    <div id="profile-content">

        <input placeholder="Name">
        <textarea placeholder="Description"></textarea>
        <button mat-flat-button color="warn">Continue</button>

    </div>
</div>

If we go back to the app, the profile page will be gone because we did not input a value for show. 

Go to the App Component HTML page and pass in a value for the show input. 


Use the square brackets, then put the input you want to set. We want to show the profile page only if we are logged in. 

<app-profile [show]="loggedIn()"></app-profile>

If we run the app and log in, the profile page will show.

Setting up Firestore

Lets add the function to upload the data into Firebase. 


Go to the Firebase Console for your project. Select Cloud Firestore on the left and then click on "Create database."

Image from Codeible.com

You will see 2 choices, Production Mode and Test Mode. 

The name may be confusing, but both modes are production ready. The only difference is the parameters that are set for the rules.

For production, reading and writing data is set to false so no one can upload or read data from the database, including yourself. 

Image from Codeible.com

For test, reading and writing data is only allowed up to a specified date. 

Image from Codeible.com

Select production mode and click next. For the location, leave it at default and click Enable. 

The Data tab is where we see all the data in our database. If we select the rules tab, you will see that reading and writing data is prohibited. Change false to true and publish it.

Image from Codeible.com

Uploading user profiles

Go into the Profile HTML page and add a template reference for the input and text area. Then attach a click event to the button and pass in the 2 references.

<div id="profile">
    <div id="profile-content">

        <input #nameInput placeholder="Name">
        <textarea #descriptionInput placeholder="Description"></textarea>
        <button (click)="onContinueClick(nameInput, descriptionInput)"
            mat-flat-button color="warn">Continue</button>

    </div>
</div>

Go into the Profile Component TypeScript file and define the function. 

onContinueClick(
    nameInput: HTMLInputElement,
    descriptionInput: HTMLTextAreaElement) {

}

Import the FirebaseTSFirestore and the FirebaseTSAuth classes and create an object for each. 

import { FirebaseTSFirestore } from 'firebasets/firebasetsFirestore/firebaseTSFirestore';
import { FirebaseTSAuth } from 'firebasets/firebasetsAuth/firebaseTSAuth';

constructor() { 
    this.firestore = new FirebaseTSFirestore();
    this.auth = new FirebaseTSAuth();
}

Inside the onContinueClick() function, get the value of the inputs.

onContinueClick(
    nameInput: HTMLInputElement,
    descriptionInput: HTMLTextAreaElement) {

    let name = nameInput.value;
    let description = descriptionInput.value;

}

To upload data to the Cloud Firestore database, grab the firestore object and call the create() method. The method accepts a JSON object with up to 4 properties.

The first 2 properties are path and data. The next 2 are the callback functions onComplete and onFail.

onContinueClick(
    nameInput: HTMLInputElement,
    descriptionInput: HTMLTextAreaElement) {

    let name = nameInput.value;
    let description = descriptionInput.value;


   this.firestore.create(
      {
        path: [],
        data: { },
        onComplete: (docId) => {},
        onFail: (err) => {}
      }
    );

}

The onFail callback function gets called when it fails to upload the data. This can happen if we set the write to false in the rules section of the database.

The onComplete callback function gets call when it successfully put the data in the database.

Data is a JSON object with the data we want to upload.

Path is the location we want to place our data. 

How data are stored in Firestore

How data are stored in Cloud Firestore is simple. They are divided into collections. Each collection contains data objects call documents that are represented by a unique id. The data in each documents are JSON objects. 

Image from Codeible.com

We can have multiple documents in a collection and we can have multiple collections in our database.


For our path, we want to place our user data in a collection call Users, so we put Users for the first element in the array. Then, we want the document id to use the value of our user id so we put the id of the user in the second element.

path: [ "Users", this.auth.getAuth().currentUser.uid ]

For the data, each user will have a publicName and description.

data: {
    publicName: name,
    description: description
},

In the onComplete callback function, print a message and reset the inputs. 

onComplete: (docId) => {
    alert("Profile Created");
    nameInput.value = "";
    descriptionInput.value = "";
}

If we go back to our app on the browser and fill in the information, we should get a pop up that says we created the profile. 


If we go back to our database and refresh the page, we will see a collection call Users with our document.

Image from Codeible.com

Retrieving user profile

Now lets see how we can retrieve the data. 

Go to the App Component TypeScript file, import the FirebaseTSFirestore class and create an Firestore object.

import { FirebaseTSFirestore } from 'firebasets/firebasetsFirestore/firebaseTSFirestore';

export class AppComponent {
  ...

  firestore = new FirebaseTSFirestore();

  ...
}

Define a function call getUserProfile(). 

getUserProfile(){

}

To retrieve the document data, grab the firestore object. 

It contains 2 methods we can use to get documents. 

The first one is getDocument() and the second one is listenToDocument().

The getDocument() method will only retrieve the document once until you make a call to it again. 

The listenToDocument() method will retrieve the document one time but also listen to any changes made to the document. This way, we can continuously get updates for the document which is what we want.

It accepts a JSON object with 3 properties. Name, Path, and the onUpdate callback function. 

getUserProfile(){
    this.firestore.listenToDocument(
      {
        name: "",
        path: [],
        onUpdate: (result) => {

        }
      }
    );
}

The name is the name for the listener. We need this name so we can stop the function at any time.

The Path is the path to the document that we want to retrieve and we want to get the user document from the users collection. 

The onUpdate callback function will get triggered every time it detects an update made to the document data.

The result object, represents the results returned from the database.  

getUserProfile(){
    this.firestore.listenToDocument(
      {
        name: "Getting Document",
        path: [ "Users", this.auth.getAuth().currentUser.uid ],
        onUpdate: (result) => {

        }
      }
    );
}

If we look at our app, even though we have a profile for our user, the profile component is still showing. To fix this, go to the app component TypeScript file. Declare a Boolean call userHasProfile and set it to true.

userHasProfile = true;

In the getUserProfile() function, set userHasProfile to result.exists and then call the function inside the whenSignedInAndEmailVerified callback function.  

getUserProfile(){
    this.firestore.listenToDocument(
      {
        name: "Getting Document",
        path: [ "Users", this.auth.getAuth().currentUser.uid ],
        onUpdate: (result) => {
             this.userHasProfile = result.exists
        }
      }
    );
}

constructor(
      private loginSheet: MatBottomSheet,
      private router: Router
    ){
    this.auth.listenToSignInStateChanges(
      user => {
        this.auth.checkSignInState(
          {
            whenSignedIn: user => {

    
            },
            whenSignedOut: user => {

            },
            whenSignedInAndEmailNotVerified: user => {
              this.router.navigate(["emailVerification"]);
            },
            whenSignedInAndEmailVerified: user => {
              this.getUserProfile();
            },
            whenChanged: user => {

            }
          }
        );
      }
);

Go to the App Component HTML page and locate the show input for the Profile component. We want to show the component only if we are logged in and do not have a profile.

<app-profile [show]="loggedIn() && !userHasProfile"></app-profile>

If we go to the app, the profile creator should not show since we have a profile already. If we delete the data from our database, the profile creator will show up since we no longer have a profile. 

Displaying the User Profile Data

Lets see how we can display the data. 


1) Go to the App Component TypeScript file. 
2) Declare a variable call userDocument 
3) Go to the getUserProfile function

userDocument: any;

To get the data, grab the result object, and call the data() method. Then pass it into the userDocument variable.

getUserProfile(){
    this.firestore.listenToDocument(
      {
        name: "Getting Document",
        path: [ "Users", this.auth.getAuth().currentUser.uid ],
        onUpdate: (result) => {

             this.userDocument = result.data();
             this.userHasProfile = result.exists

        }
      }
    );
}

result.data() will return a JSON object with the document’s data.


To get the values, grab the userDocument object and access the properties.

this.userDocument.publicName;

As best practice, we should define an interface for our documents so we know what properties it has. Right now, when we grab the document object and try to access the properties, nothing is showing up. To change this, define an interface call UserDocument and add its properties.

export interface UserDocument {
  publicName: string;
  description: string;
}

Make sure that the names of each property matches the ones in the database. 

Once we have the interface for the document, set the type of the userDocument variable to UserDocument. Then locate the getUserProfile function and cast the data to UserDocument.

userDocument: UserDocument ;

getUserProfile(){
    this.firestore.listenToDocument(
      {
        name: "Getting Document",
        path: [ "Users", this.auth.getAuth().currentUser.uid ],
        onUpdate: (result) => {

             this.userDocument = <UserDocument >result.data();
             this.userHasProfile = result.exists

        }
      }
    );
}

Go to the App Component HTML page and add another navbar item in the right side. 

1) We want to show it only if we are logged in with a profile. 
2) For the content use interpolation and get the value of publicName from the user document.
3) Remember to have the question mark symbol just in case the data is not there. 

<div id="nav-bar-right">
   <div *ngIf="!loggedIn()" (click)="onLoginClick()" class="nav-bar-items">Login</div>

   <div *ngIf="loggedIn() && userHasProfile" class="nav-bar-items" >
                Hello, {{userDocument?.publicName}}
   </div>

   <div *ngIf="loggedIn()" (click)="onLogoutClick()" class="nav-bar-items" >Logout</div>
</div>

Lastly, go to the App Component CSS file and add a right margin to the nav-bar-items so they will be separated.

.nav-bar-items {
    font-size: 1em;
    display: inline-block;
    cursor: pointer;
    margin-right: 1em;
}

If we go to the app, we should see our name in the navbar.


Sign In