Ionic Firebase
Ionic Firebase
User Authentication 15
Create the Authentication Module . . . . . . . . . . . . . . . . . . . . 15
The Authentication Component . . . . . . . . . . . . . . . . . . . . . . 20
The Authentication Page . . . . . . . . . . . . . . . . . . . . . . . . . 23
The Authentication Guard . . . . . . . . . . . . . . . . . . . . . . . . . 30
What’s next? 60
1
To my wife, Evelyn, and my kids, Emmanuel and Isabella.
You made this happen.
2
Build your First Firebase
powered Ionic app using
@angular/fire
3
• firebase web SDK: version 9
• @angular/fire: version 7
We’ll use AngularFire for a particular reason, they wrap zones and handle a bit
of the behind the scenes for you, so we have fewer things to worry about.
And then, we’ll move to things like creating events, updating them, deleting
them, etc.
4
Figure 2: List of events
5
Next Step
If at any point you have any questions, don’t hesitate to ask, you can reach me
at [email protected].
Once you’re ready, you can move to the next chapter, where we’ll set up our
development environment and install everything we need to get the application
working.
6
Create our first app
The first thing we’ll do is making sure we’ve got everything ready to start
building Ionic and Firebase applications, and for that, we’ll install a few things.
Let’s ensure we have node and npm installed on our computer. We can do that
directly from the nodejs website. Or we can install nvm (node version manager)
to handle different versions of node and npm depending on the needs of our
projects.
To make sure both packages are installed, go to our terminal and type:
ionic --version
firebase --version
we should see the versions (Figure 3) if we see something like command not
found: ionic or command not found: firebase means the package isn’t in-
stalled.
Where party-planner is the application’s name, it will start a helper that will
ask us a few questions.
7
Figure 3: Ionic and Firebase package versions
First, it will ask us the UI framework we’ll use for the application, Angular,
React, or Vue. For this example, we’ll pick Angular.
Then it will ask us the starter template we want to pick. We’ll start from scratch,
so select blank.
Now we need to give the CLI a few moments to create everything. Once it’s
done, we can open the application folder in our preferred code editor, I use VS
Code, but you can use whatever works best for you.
While we’re checking out the app, we’ll open the package.json file. We’ll see
all the packages we have installed and their versions look something like this:
8
{
"name": "party-planner",
"version": "0.0.1",
"author": "Ionic Framework",
"homepage": "https://ionicframework.com/",
"scripts": {
...
},
"private": true,
"dependencies": {
"@angular/common": "~12.1.1",
"@angular/core": "~12.1.1",
...
"@ionic/angular": "^5.5.2",
"rxjs": "~6.6.0",
...
},
"devDependencies": {
...
"typescript": "~4.2.4"
},
"description": "An Ionic project"
}
9
First, if you check the package.json file, you’ll note that it installed the pack-
ages we need:
"dependencies": {
"@angular/fire": "^7.1.0-rc.3",
"firebase": "^9.0.0",
"rxfire": "^6.0.0",
},
After installing the files, it will start a prompt in the CLI asking you a few
10
questions to finish the rest of the process.
It will ask you what Firebase packages you want to initialize. You can use the
arrow keys on your keyboard to move between them and use the space bar to
select them.
For this book, you’ll choose Firebase Authentication and Firestore.
Next, it will ask you to log in to see what Firebase projects you have access.
Please, log into the CLI with the google account you want to use with Firebase.
Next, it will ask you which app you want to use, select the Firebase project
you’ll be using.
Once you’re done answering the questions, it will generate multiple files for us.
Let’s double-check that everything is ready to use.
First, go to your src/environments/environment.ts file, you’ll notice it added
a new property to the environment variable called firebase, and it will have
your firebase credentials:
export const environment = {
firebase: {
projectId: '',
appId: '',
storageBucket: '',
locationId: '',
apiKey: '',
authDomain: '',
messagingSenderId: '',
},
production: false,
};
Next, it will go into the app.module.ts file and initialize Firebase for you, so
let’s open that file and review it.
First, it will import what you need:
import { initializeApp, provideFirebaseApp } from
'@angular/fire/app';
import { environment } from '../environments/environment';
import { provideAuth, getAuth } from '@angular/fire/auth';
import { provideFirestore, getFirestore } from
'@angular/fire/firestore';
11
Next, it will provide our firebase initializations to the imports array of our
NgModule:
@NgModule({
declarations: [AppComponent],
entryComponents: [],
imports: [
...,
provideFirebaseApp(() => initializeApp(environment.firebase)),
provideAuth(() => getAuth()),
provideFirestore(() => getFirestore()),
],
providers: [{ provide: RouteReuseStrategy, useClass:
IonicRouteStrategy }],
bootstrap: [AppComponent],
})
export class AppModule {}
12
Figure 7: Firebase Credentials
13
To create your Firestore database, you need to go to the Firestore option in
the menu, click on Create Firestore Database and select to start with Test
Mode.
At this point, we should be able to run ionic serve in the terminal and have
our application running without any compile or build errors.
If you have errors here and want some help, remember you can send me a quick
email at [email protected].
Once all of that is done, we’re ready to start coding. In the next chapter, we’ll
build the authentication module of our application, where we’ll learn how to
create and authenticate users in our app.
14
User Authentication
One of the most common (an imoprtant) parts of a modern application is user
authentication. Throughout the next few pages, we’ll learn:
• How to create a page to handle user authentication.
• How to create and reuse a component for login, signup, and reset password.
• How to talk to Firebase Authentication functions.
• How to create a guard to prevent unauthenticated users from accessing a
page.
If you want to know what that script does before typing it, you can append the
flag --dry-run, and call it like this:
ionic generate page authentication --dry-run
Then you can see the files it will generate, and the files it will update like this:
CREATE src/app/authentication/authentication-routing.module.ts (379
bytes)
CREATE src/app/authentication/authentication.module.ts (528 bytes)
CREATE src/app/authentication/authentication.page.scss (0 bytes)
CREATE src/app/authentication/authentication.page.html (133 bytes)
CREATE src/app/authentication/authentication.page.spec.ts (717 bytes)
CREATE src/app/authentication/authentication.page.ts (288 bytes)
UPDATE src/app/app-routing.module.ts (643 bytes)
15
Once we’re comfortable with the result on the console, we can run the script
without the --dry-run flag and generate those files.
Before we move into the authentication files, let’s open the src/app/app-routing.module.ts,
it should look something like this:
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from
'@angular/router';
@NgModule({
imports: [RouterModule.forRoot(routes, { preloadingStrategy:
PreloadAllModules })],
exports: [RouterModule],
})
export class AppRoutingModule {}
We want to change it, to remove the home module and to add proper routes for
our application, make the changes so that it looks like this:
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from
'@angular/router';
16
import('./authentication/authentication.module').then(m =>
m.AuthenticationPageModule),
},
{
path: 'signup',
loadChildren: () =>
import('./authentication/authentication.module').then(m =>
m.AuthenticationPageModule),
},
{
path: 'reset',
loadChildren: () =>
import('./authentication/authentication.module').then(m =>
m.AuthenticationPageModule),
},
];
@NgModule({
imports: [RouterModule.forRoot(routes, { preloadingStrategy:
PreloadAllModules })],
exports: [RouterModule],
})
export class AppRoutingModule {}
You can also remove the src/app/home/ entire folder since we won’t use it.
The changes we made in the app-routing.module.ts file means that every
time the user goes to the urls: /login, signup, or reset, we’re handling the
responsibility to the AuthenticationPageModule.
Now, we also want to create the authentication form component. We’ll use it
to load the authentication form depending on the page we’re on.
For that, let’s open the terminal and type:
ionic generate component authentication/auth-form --dry-run
Where you can see in the output the files it will generate:
CREATE src/app/authentication/auth-form/auth-form.component.scss (0
bytes)
CREATE src/app/authentication/auth-form/auth-form.component.html (28
bytes)
CREATE src/app/authentication/auth-form/auth-form.component.spec.ts
(711 bytes)
CREATE src/app/authentication/auth-form/auth-form.component.ts (279
bytes)
17
After reviewing that those are the files and paths we were expecting, we can
run that command again without the --dry-run flag.
Next, let’s go to the authentication.module.ts file and we need to do two
things, first, let’s import the authentication form component that we just cre-
ated:
import { AuthFormComponent } from './auth-form/auth-form.component';
@NgModule({
imports: [CommonModule, FormsModule, IonicModule,
AuthenticationPageRoutingModule],
declarations: [AuthenticationPage, AuthFormComponent],
})
export class AuthenticationPageModule {}
Now it’s an excellent time to test that everything is working, for that, let’s open
the src/app/authentication/authentication.page.html and it should look
something like this:
<ion-header>
<ion-toolbar>
<ion-title>authentication</ion-title>
</ion-toolbar>
</ion-header>
18
<ion-content> </ion-content>
Let’s change the page title, and add our authentication component:
<ion-header>
<ion-toolbar>
<ion-title>authentication</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<app-auth-form></app-auth-form>
</ion-content>
It will create a local development server to run the application, pay attention
to the port so that we can run the app in the browser.
By default, it uses the port 8100, so you can go to localhost:8100/login and
you can see a message that says auth-form works!.
19
The Authentication Component
Now that we have our authentication-form component, it is time to create
the form itself so that our users can login, signup, or reset their passwords.
First, we want to import the ReactiveFormsModule, an angular forms module
that lets us build reactive forms.
You can add it to the authentication.module.ts in the imports array of our
NgModule:
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@NgModule({
imports: [..., ReactiveFormsModule]
})
export class AuthenticationPageModule {}
Now, we can go into the auth-form.component.ts file, and start setting up the
functionality we need for the form, for that, we want to import a few things, so
go into the auth-form.component.ts file and import the following packages:
import { Component, Input, Output, EventEmitter, OnInit } from
'@angular/core';
import { FormGroup, Validators, FormBuilder } from '@angular/forms';
We’re importing:
• Input to take on properties from the parent component.
• Output, and EventEmitter to send the user credentials back to the au-
thentication page.
• FormGroup, Validators, FormBuilder are part of the angular reactive
forms and we’ll use them to handle the forms in our component.
Then, we’ll create our class variables, and inject the Angular’s form builder into
our constructor:
import { Component, Input, Output, EventEmitter, OnInit } from
'@angular/core';
import { FormGroup, Validators, FormBuilder } from '@angular/forms';
import { UserCredential } from '../authentication.model';
@Component({
selector: 'app-auth-form',
templateUrl: './auth-form.component.html',
styleUrls: ['./auth-form.component.scss'],
})
export class AuthFormComponent implements OnInit {
@Input() actionButtonText = 'Sign In';
20
@Input() isPasswordResetPage = false;
@Output() formSubmitted = new EventEmitter<any>();
public authForm: FormGroup;
Where:
• actionButtonText is the text our form’s button will have.
• isPasswordResetPage is a flag to determine if we’re in the password reset
page or not.
• formSubmitted is an angular event emitter, this will send the value of our
form back to the parent page.
• authForm this is out form.
Next, we’ll initialize our form, for that we’ll create a function to initialize it and
call that function from the Angular’s OnInit lifecycle hook.
ngOnInit() {
this.initializeForm(!this.isPasswordResetPage);
}
21
const credentials = {
email: authForm.value.email,
password: authForm.value.password,
};
this.formSubmitted.emit(credentials);
}
}
Now our component is ready to be designed, for that, let’s move to the
auth-form.component.html, right now it should probably look like this:
<p>auth-form works!</p>
We’re binding the formGroup property to our authentication form, next, we want
to add a couple of fields, one for the email, and another one for the password:
<form [formGroup]="authForm">
<ion-item>
<ion-label position="stacked">Email</ion-label>
<ion-input formControlName="email" type="email"
placeholder="Your email address"> </ion-input>
</ion-item>
<ion-item *ngIf="!isPasswordResetPage">
<ion-label position="stacked">Password</ion-label>
<ion-input formControlName="password" type="password"
placeholder="Your password"> </ion-input>
</ion-item>
</form>
Those are regular Ionic inputs, the one particular thing here is the
formControlName, this binds the fields to the properties we created for
our form, email, and password.
And lastly, we’ll add a button that will call the submitCredentials() function
and send the result of the form:
<form [formGroup]="authForm">
<ion-item>
<ion-label position="stacked">Email</ion-label>
<ion-input formControlName="email" type="email"
placeholder="Your email address"> </ion-input>
</ion-item>
22
<ion-item *ngIf="!isPasswordResetPage">
<ion-label position="stacked">Password</ion-label>
<ion-input formControlName="password" type="password"
placeholder="Your password"> </ion-input>
</ion-item>
p {
font-size: 0.8em;
color: #d2d2d2;
}
ion-label {
margin-left: 5px;
}
ion-input {
padding: 5px;
}
23
@Component({
selector: 'app-authentication',
templateUrl: './authentication.page.html',
styleUrls: ['./authentication.page.scss'],
})
export class AuthenticationPage implements OnInit {
url: string; // The URL we're at: login, signup, or reset.
pageTitle = 'Sign In';
actionButtonText = 'Sign In';
constructor(private readonly router: Router) {}
ngOnInit() {
// First we get the URL, and with that URL we send the
// proper information to the authentication form component.
this.url = this.router.url.substr(1);
if (this.url === 'signup') {
this.pageTitle = 'Create your Account';
this.actionButtonText = 'Create Account';
}
handleUserCredentials(userCredentials) {
// This method gets the form value from the authentication
component
// And depending on the URL, it calls the respective method.
const { email, password } = userCredentials;
switch (this.url) {
case 'login':
this.login(email, password);
break;
case 'signup':
this.signup(email, password);
break;
case 'reset':
this.resetPassword(email);
break;
}
}
24
console.log(email, password);
}
Now let’s visit the authentication.page.html file, it looks something like this:
<ion-header>
<ion-toolbar>
<ion-title>authentication</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<app-auth-form></app-auth-form>
</ion-content>
The first thing we’ll do is to replace the header with something more custom:
<ion-header>
<ion-toolbar>
<ion-buttons *ngIf="url && url !== 'login'" slot="start">
<ion-back-button defaultHref="/login"></ion-back-button>
</ion-buttons>
<ion-title>{{ pageTitle }}</ion-title>
</ion-toolbar>
</ion-header>
It will add a dynamic page title, and it will add the Ionic back button if we’re
on a page different than the login page.
Next, we want to send all the correct properties to the authentication form:
<ion-content>
<app-auth-form
*ngIf="url"
(formSubmitted)="handleUserCredentials($event)"
[actionButtonText]="actionButtonText"
[isPasswordResetPage]="url === 'reset'"
25
></app-auth-form>
</ion-content>
We’re telling our component what function to use to handle the form value, and
sending the correct properties depending on the page we’re on.
And lastly, we want to add two links to the page, one to create a new account,
and the other one to navigate to the password reset page, in the end, the page
should look like this:
<ion-header>
<ion-toolbar>
<ion-buttons *ngIf="url && url !== 'login'" slot="start">
<ion-back-button defaultHref="/login"></ion-back-button>
</ion-buttons>
<ion-title>{{ pageTitle }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<app-auth-form
*ngIf="url"
(formSubmitted)="handleUserCredentials($event)"
[actionButtonText]="actionButtonText"
[isPasswordResetPage]="url === 'reset'"
></app-auth-form>
Remember, at any point, you can run your app to make sure it’s all coming along
nicely, and you don’t have compile/build errors, for that open the terminal and
type:
ionic serve
26
Figure 11: Login Page
27
I’ll go over all the @angular/fire/auth as we encounter them. Once everything
is imported, let’s inject auth into the constructor, this is the AngularFire’s auth
instance and initialization.
constructor(private readonly auth: Auth) {}
And then let’s start adding the functions we’d like to have. First, we want a
function to return the current user:
getUser(): User {
return this.auth.currentUser;
}
getUser$(): Observable<User> {
return of(this.getUser());
}
The getUser() function returns the current user logged into the application.
This is a synchronous function, so we need to make sure the object is ready to
be used before calling it.
For example, if we call it first thing when loading the service, we risk it returning
undefined/null even when there’s a logged-in user.
That’s where the getUser()$ function comes in. It creates an observable of
that response and subscribes to the result so that we can call it more safely.
Next, we need a function to log our users in, for that, we can use a Firebase func-
tion called signInWithEmailAndPassword(), this function takes 3 parameters:
the current authentication instance, the email, and password.
login(email: string, password: string): Promise<UserCredential> {
return signInWithEmailAndPassword(this.auth, email, password);
}
Next, we want a function to reset our users’ passwords when needed, for that,
we’ll use the sendPasswordResetEmail() function, which takes the authentica-
tion instance, and the email.
28
resetPassword(email: string): Promise<void> {
return sendPasswordResetEmail(this.auth, email);
}
And lastly, we want to add a function to log our users out. For that, we can use
the signOut() function, which only takes the current authentication instance.
logout(): Promise<void> {
return signOut(this.auth);
}
Now we’re ready to go back to our auth page and connect that functionality.
Open your authentication.page.ts file, and inject the auth service into the
constructor:
constructor(
private readonly router: Router,
private readonly auth: AuthenticationService
) {}
Next, go into the login function. Right now is a placeholder that doesn’t have
anything. Let’s add the functionality we need.
For login, we want to call the authentication service login function, and after
login, redirect our user to the home page.
async login(email: string, password: string) {
try {
await this.auth.login(email, password);
// This will give you an error since we don't have the / URL in
our routes yet.
// No worries, we'll add it soon enough.
this.router.navigateByUrl('');
} catch (error) {
console.log(
`Either we couldn't find your user or there was a problem with
the password`
);
}
}
For signup and reset password, we want to do the same. The idea is to call the
authentication service, and if the operation is successfull, send the user to the
home page. If not, log an error to the console:
async signup(email: string, password: string) {
try {
await this.auth.signup(email, password);
29
this.router.navigateByUrl('');
} catch (error) {
console.log(error);
}
}
You should see the output in the console saying that it created the file:
CREATE src/app/authentication/authentication.guard.ts (468 bytes)
@Injectable({
providedIn: 'root',
})
export class AuthenticationGuard implements CanActivate {
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> |
boolean | UrlTree {
return true;
}
}
30
We’re going to change the imports a bit, to import something from angular fire,
and to import the angular router:
import { Injectable } from '@angular/core';
import { CanActivate, Router, UrlTree } from '@angular/router';
import { Auth, onAuthStateChanged } from '@angular/fire/auth';
Then we’ll add a constructor injecting both the authentication instance and the
router.
constructor(private readonly auth: Auth, private readonly router:
Router) {}
Next, we’ll remove everything from the canActivate() method and return an
empty promise.
canActivate(): Promise<boolean | UrlTree> {
return new Promise((resolve, reject) => {});
}
And inside the promise, we’ll give the functionality we need. First, we want
to use Firebase’s onAuthStateChanged() function, it adds an observer to the
user’s sign-in state:
canActivate(): Promise<boolean | UrlTree> {
return new Promise((resolve, reject) => {
onAuthStateChanged(this.auth, (user) => {
// TODO
});
});
}
31
And that’s it. We now have a fully functional authentication system that will
take care of most of the needs of an application. In the next chapter, we’ll start
using the Firestore database, and we’ll create some new pages. I’ll show you
how to use this guard to protect those pages.
32
Interacting with your
Firestore Database
You can inspect the terminal’s output and see the files it generated and the files
it updated:
CREATE src/app/party/party-routing.module.ts (343 bytes)
CREATE src/app/party/party.module.ts (465 bytes)
CREATE src/app/party/party.page.scss (0 bytes)
CREATE src/app/party/party.page.html (124 bytes)
CREATE src/app/party/party.page.spec.ts (654 bytes)
CREATE src/app/party/party.page.ts (252 bytes)
UPDATE src/app/app-routing.module.ts (923 bytes)
Note the last line: it updated the app-routing.module.ts file adding a route
for our module it looks like this:
{
path: 'party',
loadChildren: () => import('./party/party.module').then( m =>
m.PartyPageModule)
},
33
We’ll make three changes in the app-routing.module.ts file:
• We’ll add a new route for party/:partyId so that we can go to the detail
page of an event.
• We’ll protect both routes with the AuthenticationGuard we created in
the previous chapter.
• We’ll create a redirect to take us to the /party page when the user doesn’t
add any URL.
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from
'@angular/router';
import { AuthenticationGuard } from
'./authentication/authentication.guard';
34
loadChildren: () => import('./party/party.module').then(m =>
m.PartyPageModule),
canActivate: [AuthenticationGuard],
},
];
@NgModule({
imports: [RouterModule.forRoot(routes, { preloadingStrategy:
PreloadAllModules })],
exports: [RouterModule],
})
export class AppRoutingModule {}
Once we have that, let’s understand what we need for our module to work:
• We need the Party page where we’ll show a list of parties (this is the one
we just created).
• A component to show the details of a specific party.
• A component to create a new party.
• A service to interact with Firebase.
Let’s create the files themselves now so that we can serve our application and
focus solely on coding instead of switching from coding to config back and forth.
For that, open your terminal and type:
ionic generate service party/party
ionic generate component party/detail-party
ionic generate component party/create-party
It will generate a bunch of files. You can see the output in the terminal:
CREATE src/app/party/detail-party/detail-party.component.scss (0
bytes)
CREATE src/app/party/detail-party/detail-party.component.html (31
bytes)
CREATE src/app/party/detail-party/detail-party.component.spec.ts
(732 bytes)
CREATE src/app/party/detail-party/detail-party.component.ts (291
bytes)
CREATE src/app/party/create-party/create-party.component.scss (0
bytes)
CREATE src/app/party/create-party/create-party.component.html (31
bytes)
CREATE src/app/party/create-party/create-party.component.spec.ts
(732 bytes)
CREATE src/app/party/create-party/create-party.component.ts (291
bytes)
35
CREATE src/app/party/party.service.spec.ts (352 bytes)
CREATE src/app/party/party.service.ts (134 bytes)
Now we can start coding. First, let’s open the party.module.ts file and add the
newly created components to the declarations array inside of the NgModule:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [CommonModule, FormsModule, IonicModule,
PartyPageRoutingModule],
declarations: [PartyPage, CreatePartyComponent,
DetailPartyComponent],
})
export class PartyPageModule {}
Now that both components are available for use, let’s open the party-routing.module.ts.
It should look like this:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { PartyPage } from './party.page';
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
36
export class PartyPageRoutingModule {}
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class PartyPageRoutingModule {}
Now, you can run your application with ionic serve, and when you log in,
you’ll be redirected to the main route, where you’ll see the party page, it’s
empty right now, but don’t worry about it, our next step will be to add the
functionality to show the list of parties there.
37
Listing Parties from Firestore
We’ll start with listing items from the database, in this case, the parties. A
party has several properties:
{
id: string; // The ID of the document.
name: string; // The user friendly name.
date: number; // The date it is happening.
ticketPrice: number; // The price for people to go into the party.
cost: number; // The $$ you're spending throwing the party.
revenue: number; // The income - the expenses.
}
In the PartyPage we want to show a list of the parties we have in the application.
For that, let’s open the party.page.ts file, and let’s create a class variable
called partyList. Also, remember you can create an interface for the party
properties so that it is properly typed.
By now, the party page class should look like this:
import { Component } from '@angular/core';
import { Party } from './party.model'; // You can add this
party.model.ts file in the same /party folder.
@Component({
selector: 'app-party',
templateUrl: './party.page.html',
styleUrls: ['./party.page.scss'],
})
export class PartyPage {
readonly partyList$: Party[] = [];
constructor() {}
}
We’ll initialize the partyList property as an empty array, we don’t have any-
thing to show in our database at this point, so we’ll leave it like that while we
work on adding items to the database.
Now, we can create a simple (but elegant) template to display the list of parties
once it’s ready.
First, we want to change the page’s header, so open party.page.html file and
replace the header with this:
<ion-header>
<ion-toolbar>
<ion-title>Your Events</ion-title>
</ion-toolbar>
38
</ion-header>
<ion-content class="ion-padding"></ion-content>
Next, we want to create a list, for that we can use Ionic’s ion-list compo-
nent, the idea is that we loop through partyList and display the most relevant
information of each event:
<ion-header>
<ion-toolbar>
<ion-title>Your Events</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-list>
<ion-item tappable *ngFor="let party of partyList$"
routerLink="/party/{{ party.id }}">
<ion-label>
<h2>{{party?.name}}</h2>
<p>Ticket: <strong>${{party?.ticketPrice}}</strong></p>
<p>Date: <strong>{{party?.date | date }}</strong></p>
</ion-label>
</ion-item>
</ion-list>
</ion-content>
Note that when you click on an item on the list, it will take you to the /party/id
URL, which will trigger the DetailPartyComponent.
Then, we want a way for the user to add new parties, for that, we’re go-
ing to create a floating action button the users can click and navigate to the
CreatePartyComponent.
To create the Floating Action Button, Ionic has a handy component called
ion-fab where you pass the position, and they take care of the rest :)
<ion-header>
<ion-toolbar>
<ion-title>Your Events</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-list>
<ion-item tappable *ngFor="let party of partyList$"
routerLink="/party/{{ party.id }}">
<ion-label>
39
<h2>{{party?.name}}</h2>
<p>Ticket: <strong>${{party?.ticketPrice}}</strong></p>
<p>Date: <strong>{{party?.date | date }}</strong></p>
</ion-label>
</ion-item>
</ion-list>
@Component({
selector: 'app-create-party',
templateUrl: './create-party.component.html',
styleUrls: ['./create-party.component.scss'],
})
export class CreatePartyComponent implements OnInit {
name: string;
ticketPrice: number;
cost: number;
date: any;
ngOnInit() {}
40
isValidForm(): boolean {
return this.name && this.ticketPrice && this.cost && this.date;
}
}
<ion-content></ion-content>
Now it is time to start adding our fields, for the previous lesson we used the
angular ReactiveFormsModule, for this one, we’ll try something a bit different
so that you’re aware of the options you have.
We’ll use the angular’s FormsModule, which lets us bind the inputs with the
[(ngModel)] property (that’s why we created the class variables for the proper-
ties inside the component’s class).
The first one we’ll create will be a text input we can bind to the name property:
<ion-content class="ion-padding">
<ion-item class="ion-no-padding">
<ion-label position="stacked">Event Name</ion-label>
<ion-input [(ngModel)]="name" type="text" placeholder="What's
your event's name?"> </ion-input>
</ion-item>
</ion-content>
41
Next, we can add two number inputs, one for the ticketPrice and another one
for the cost:
<ion-content class="ion-padding">
<ion-item class="ion-no-padding">
<ion-label position="stacked">Event Name</ion-label>
<ion-input [(ngModel)]="name" type="text" placeholder="What's
your event's name?"> </ion-input>
</ion-item>
<ion-item class="ion-no-padding">
<ion-label position="stacked">Price</ion-label>
<ion-input [(ngModel)]="ticketPrice" type="number"
placeholder="How much will guests pay?"> </ion-input>
</ion-item>
<ion-item class="ion-no-padding">
<ion-label position="stacked">Cost</ion-label>
<ion-input [(ngModel)]="cost" type="number" placeholder="How
much are you spending?"> </ion-input>
</ion-item>
</ion-content>
Next, we’ll use Ionic’s new datetime component to show a calendar view inside
the form, so that our users can easily pick the date of their events:
<ion-content class="ion-padding">
<ion-item class="ion-no-padding">
<ion-label position="stacked">Event Name</ion-label>
<ion-input [(ngModel)]="name" type="text" placeholder="What's
your event's name?"> </ion-input>
</ion-item>
<ion-item class="ion-no-padding">
<ion-label position="stacked">Price</ion-label>
<ion-input [(ngModel)]="ticketPrice" type="number"
placeholder="How much will guests pay?"> </ion-input>
</ion-item>
<ion-item class="ion-no-padding">
<ion-label position="stacked">Cost</ion-label>
<ion-input [(ngModel)]="cost" type="number" placeholder="How
much are you spending?"> </ion-input>
</ion-item>
42
</ion-content>
And lastly, we’ll add a button to call the createEvent() function we created
and pass the properties:
<ion-content class="ion-padding">
<ion-item class="ion-no-padding">
<ion-label position="stacked">Event Name</ion-label>
<ion-input [(ngModel)]="name" type="text" placeholder="What's
your event's name?"> </ion-input>
</ion-item>
<ion-item class="ion-no-padding">
<ion-label position="stacked">Price</ion-label>
<ion-input [(ngModel)]="ticketPrice" type="number"
placeholder="How much will guests pay?"> </ion-input>
</ion-item>
<ion-item class="ion-no-padding">
<ion-label position="stacked">Cost</ion-label>
<ion-input [(ngModel)]="cost" type="number" placeholder="How
much are you spending?"> </ion-input>
</ion-item>
At this point, you should be able to run the application with ionic serve,
navigate to the create page, and see the form, if you add properties there and
click on Create Event you’ll be able to see the properties in the browser’s
console.
So far we’ve managed to get the UI ready, but we still have no way to add those
parties to the database, for that, let’s head to the party.service.ts file, and
import things we’ll need:
import { Injectable } from '@angular/core';
import { addDoc, collection, Firestore } from
'@angular/fire/firestore';
import { AuthenticationService } from
'../authentication/authentication.service';
import { Party } from './party.model';
43
Figure 12: Create Party Form
44
Here’s what we’re importing:
• AuthenticationService to connect with the auth service, we’ll use this
to get the user’s ID.
• addDoc, collection, Firestore are for adding documents to the
database and connecting to Firestore.
Now, let’s inject both the authentication service and the Firestore instance into
the constructor:
constructor(
private readonly auth: AuthenticationService,
private readonly firestore: Firestore
) {}
And lastly, let’s create our createParty() function, it will take the party as a
parameter, and the first thing that it needs to do is to get the user’s ID.
createParty(party: Partial<Party>) {
const userId: string = this.auth.getUser().uid;
}
We’re getting the user’s ID to store the parties under the user’s document in the
Firestore database. Then, we want to get a reference to the party collection.
Think of it like the location where we want to add the new document.
createParty(party: Partial<Party>) {
const userId: string = this.auth.getUser().uid;
const partyCollection = collection(
this.firestore,
`users/${userId}/party/`
);
return addDoc(partyCollection, party);
}
The firestore collection method takes two parameters, the Firestore instance,
and the path to where we’ll store the document.
And lastly, we want to add the addDoc function and pass that collection refer-
ence, and the object we’ll be storing:
createParty(party: Partial<Party>) {
const userId: string = this.auth.getUser().uid;
const partyCollection = collection(
this.firestore,
`users/${userId}/party/`
);
return addDoc(partyCollection, party);
}
45
You can test this right now, let’s go to the create-party.component.ts and
inject this party service into the constructor:
constructor(
private readonly router: Router,
private readonly partyService: PartyService
) {}
And let’s see our createEvent() function, right now it is a placeholder that
looks like this:
async createEvent(party: Partial<Party>) {
// Save the party to the database
console.log(party);
await this.router.navigateByUrl('party');
}
And we’ll do two things, first, we want to initialize the revenue as 0, since no
party will start with tickets already sold:
async createEvent(party: Partial<Party>) {
party.revenue = 0;
// Save the party to the database
console.log(party);
await this.router.navigateByUrl('party');
}
And next, we’ll replace our comment with a call to the party service’s new
function we just created, the createParty() function:
async createEvent(party: Partial<Party>) {
party.revenue = 0;
await this.partyService.createParty(party);
console.log(party);
await this.router.navigateByUrl('party');
}
You can go ahead and fill your form now, and you can check your
database to see if the document was created or not. If there’s any is-
sues and the document wasn’t created feel free to shoot me an email at
([email protected])[mailto:[email protected]].
Now that we’re adding items to the database would be a good time to connect
a function that can list them, for that, let’s get back to the party.service.ts
file and create a function called getPartyList.
This function needs to get the user’s ID, and then get the collection of parties
from the database.
First, let’s create the function:
46
getPartyList() {}
And then we’re going to use a bit of rxjs to get everything as an observable,
instead of trying to get the user’s ID, and then getting the path and so on.
First, let’s import the rxjs operators we’ll need:
// If you're on rxjs 7+
import { map, switchMap } from 'rxjs';
// If you're on rxjs 6
import { map, switchMap } from 'rxjs/operators';
Now we can go back to the PartyPage and connect this new function so that we
can see the list of parties in our application, so let’s open the party.page.ts
file, and first, inject the part service into the constructor:
constructor(private readonly partyService: PartyService) {}
And replace it with the initialization of the parties from the part service:
readonly partyList$ = this.partyService.getPartyList();
At this point, you’ll get an error in the template, something like this:
47
Error: Cannot find a differ supporting object '[object Object]' of
type 'object'. NgFor only supports binding to Iterables such as
Arrays
This is happening because in the template we’re trying to look through an array
of parties, but the service is not returning an array, it’s returning an Observable
of an array.
To fix this, we’ll go into the party.page.html page, and look for the *ngFor
directive we added, it should look like this:
<ion-item *ngFor="let party of partyList$" routerLink="/party/{{
party.id }}"></ion-item>
And right after the partyList$ variable, we’ll add the async pipe provided by
Angular, it will handle observable unwrapping for us, as well as unsubscribing
from it when we leave the component:
<ion-item *ngFor="let party of partyList$ | async"
routerLink="/party/{{ party.id }}"></ion-item>
If you navigate to /party now you should be able to see the list of parties we
created
48
<p>detail-party works!</p>
We’re going to start by initializing the current object we’ll see, the event or party
we’re navigating to, for that, let’s go into the detail-party.component.ts file,
and make it look like this:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Party } from '../party.model';
@Component({
selector: 'app-detail-party',
templateUrl: './detail-party.component.html',
styleUrls: ['./detail-party.component.scss'],
})
export class DetailPartyComponent implements OnInit {
// We're creating a class variable for the currentParty
currentParty: Party;
// We're injecting Angular's ActivatedRoute into the constructor.
constructor(private readonly route: ActivatedRoute) {}
ngOnInit() {
// We're using Angular's ActivatedRoute to get the PartyId from
the URL.
const partyId: string =
this.route.snapshot.paramMap.get('partyId');
// We're passing the partId to the initialize party function
this.initializeParty(partyId);
}
49
We’ll use the same approach to when we got the list of parties, we’ll get the
userId, to create the path to the database, then use rxjs to chain those together
and get the document from the database:
getPartyDetail(partyId: string) {
return this.auth.getUser$().pipe(
map(({ uid: userId }: User) => doc(this.firestore,
`users/${userId}/party/${partyId}`)),
switchMap(partyDocument => docData(partyDocument))
);
}
Now we can start working on the view. After all, what good does it to have all
this data if we can’t see it hehe.
For that, let’s go to the detail-party.component.html, and the first thing we
want to add is a header:
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
50
<ion-back-button defaultHref="/party"></ion-back-button>
</ion-buttons>
<ion-title>{{ currentParty?.name }}</ion-title>
</ion-toolbar>
</ion-header>
We’re adding the regular Ionic header, with a back button and using the party’s
name as the page title.
One thing you’ve probably noticed is that we’re using defaultHref in our back
buttons, this is because we want these specific back buttons to work even if the
user directly navigates to this page with the URL.
For example, if the user grabs the URL /party/4567uikmnbvfr6u and pastes
it in the browser, you won’t see the back button because there’s no page to go
back to, that’s the first page the user opened.
But, if you add the defaultHref, Ionic will show the back button and when
clicked it will take the user to the URL we’re pointing out there.
After the header, let’s add some content to display the properties of the party:
<ion-content class="ion-padding">
<ion-card>
<ion-card-header>
Event's Profits:
<span
[class.profitable]="currentParty?.revenue -
currentParty?.cost > 0"
[class.no-profit]="currentParty?.revenue -
currentParty?.cost <= 0"
>
{{ currentParty?.revenue - currentParty?.cost | currency }}
</span>
</ion-card-header>
<ion-card-content>
<p>Ticket: <strong>${{ currentParty?.ticketPrice
}}</strong></p>
<p>Date: <strong>{{ currentParty?.date | date }}</strong></p>
</ion-card-content>
</ion-card>
</ion-content>
We’re showing whatever properties we have in the party, and we have a card
header that shows the current profits. Profit is the revenue minus the cost, and
if we’re on negative values we’re adding a CSS class called no-profit; if we’re
on positive values, we’re adding the class profitable.
You can go ahead and add both to the CSS file:
51
.profitable {
color: #22bb22;
}
.no-profit {
color: #ff0000;
}
Now, if you run your app and navigate to the detail page, you should be able
to see all the properties right there in the page.
52
Refund Ticket
<ion-icon slot="end" name="remove"></ion-icon>
</ion-button>
<ion-button (click)="addTicketOperation('add')">
Sell Ticket
<ion-icon slot="end" name="add"></ion-icon>
</ion-button>
Now, let’s move to the party.service.ts file and create the method there too:
async addTicketOperation(
partyId: string,
ticketCost: number,
type: string = 'add'
) {
try {
// We'll add the logic here
} catch (error) {
console.log('Transaction failed: ', error);
throw error;
}
}
Right now, it’s not doing much, it takes the parameters you’re sending, and it
will run the business logic inside a try/catch block in case something fails.
53
Next, we want to get the user’s ID, and create a reference to the document for
that party in Firestore:
async addTicketOperation(
partyId: string,
ticketCost: number,
type: string = 'add'
) {
try {
const userId: string = this.auth.getUser().uid;
const partyDocRef = doc(this.firestore,
`users/${userId}/party/${partyId}`);
} catch (error) {
console.log('Transaction failed: ', error);
throw error;
}
}
And lastly, we’ll use the runTransaction() function to update the revenue.
async addTicketOperation(
partyId: string,
ticketCost: number,
type: string = 'add'
) {
try {
const userId: string = this.auth.getUser().uid;
const partyDocRef = doc(this.firestore,
`users/${userId}/party/${partyId}`);
54
The runTransaction() function takes the Firestore instance and runs a trans-
action in the database. A transaction will make sure you update the proper
value. Let me explain:
What happens if you have 10 people selling tickets?
What happens when they all click the sell button at the same time?
Without a transaction, they would get the current revenue, let’s say they all get
0, and add the new ticket value, so when they all update the document, they’re
all setting the new revenue as 15, for example.
With a transaction, things are a bit different. Here’s how it happens:
• Firestore runs the transaction.
• You get the document ready to update whatever property you want to
update.
• Firestore checks if the document has changed, if not, you’re good, and
your update goes through.
• If the document has changed, then Firestore gets you the new version of
the document, then it runs your updates again.
That way, if they all sell a ticket at the same time, Firestore will start process-
ing those transactions, and instead of they overwrite each other’s work, it will
update the proper values.
In the end, you can start clicking the sell and refund buttons and you’ll be able
to see how the application is working.
Then, we’ll do what we’ve been doing for a while, we get the user’s ID, and we
create a reference to the document we want to delete:
deleteParty(partyId: string) {
const userId: string = this.auth.getUser().uid;
const documentReference = doc(this.firestore,
`users/${userId}/party/${partyId}`);
}
And lastly, we will call Firestore’s deleteDoc() function, which takes the doc-
ument’s reference as a parameter.
55
Figure 15: Detail page Showing Profitable Party
56
deleteParty(partyId: string): Promise<void> {
const userId: string = this.auth.getUser().uid;
const documentReference = doc(this.firestore,
`users/${userId}/party/${partyId}`);
return deleteDoc(documentReference);
}
Remember that we have to add the deleteDoc() function to the firestore im-
ports:
import {
...,
deleteDoc,
} from '@angular/fire/firestore';
Now let’s move to the detail-party.component.ts file and create the function
that will call the firestore delete functionality:
async removeParty() {
try {
await this.partyService.deleteParty(this.currentParty.id);
this.router.navigateByUrl('party');
} catch (error) {
console.log(error);
}
}
constructor(
private readonly route: ActivatedRoute,
private readonly partyService: PartyService,
private readonly router: Router
) {}
Now, before moving into the view to create the button to call this function, we
need to talk about something. This is a horrible practice.
Deleting an item from the database is a destructive operation. There’s no
recovering from that one, so what happens when your user clicks by mistake?
Do you want to remove the item without hesitation?
57
Instead of doing that, a better practice is to introduce a confirmation step, where
you ask the user if they’re sure they want to delete that item.
For that, we can use Ionic’s alerts, first, let’s import the alert service and inject
it into the constructor:
import { AlertController } from '@ionic/angular';
constructor(
private readonly route: ActivatedRoute,
private readonly partyService: PartyService,
private readonly router: Router,
private readonly alertCtrl: AlertController
) {}
await alert.onDidDismiss();
}
And that’s it, you can click that button and see your alert in action, and once
you confirm it, it will delete the item and take you back to the list page.
58
Figure 16: Delete Party Confirmation Alert
59
What’s next?
60