Building a CRUD Ionic application with Firestore
Libraries and Versions
Last Reviewed: Sep 13, 2020
Firestore is Firebase’s default database. It’s a managed NoSQL document-oriented database for mobile and web development.
It’s designed to store and sync app data at global scale easily. Some of its key features include:
- Documents and Collections with powerful querying.
- Offline access to the WEB SDK.
- Real-time data sync.
Today we're going to learn how to integrate Firestore with Ionic to create a CRUD work-flow. This sample app will show you how to:
- Showing a list of items from your database (which in Firestore is called displaying a collection of documents).
- Creating a new item and adding it to the list.
- Navigating to that item's detail page.
- Deleting an item from our list.
We will break down this process in 5 steps:
- Step #1: Create and Initialize our Ionic app.
- Step #2: Add items to the list.
- Step #3: Show the list of items.
- Step #4: Navigate to one item's detail page.
- Step #5: Delete an item from the list.
Now that we know what we're going to do let's jump into coding mode.
Step #1: Create and Initialize your app
The goal of this step is to create your new Ionic app, install the packages we'll need (only Firebase and @angular/fire), and initialize our Firebase application.
With that in mind, let's create our app first, open your terminal and navigate to the folder you use for coding (or anywhere you want, for me, that's the Development folder) and create your app:
cd Development/
ionic start firestore-example blank --type=angular
cd firestore-example
After we create the app we'll need to install Firebase, for that, open your terminal again and (while located at the projects root) type:
npm install @angular/fire firebase
That command will install the latest stable versions of both @angular/fire and the Firebase Web SDK.
Now that we installed everything let's connect Ionic to our Firebase app.
The first thing we need is to get our app's credentials, log into your Firebase Console and navigate to your Firebase app (or create a new one if you don't have the app yet).
In the Project Overview tab you'll see the 'Get Started' screen with options to add Firebase to different kind of apps, select "Add Firebase to your web app."
Out of all the code that appears in that pop-up window focus on this bit:
var config = {
apiKey: 'Your credentials here',
authDomain: 'Your credentials here',
databaseURL: 'Your credentials here',
projectId: 'Your credentials here',
storageBucket: 'Your credentials here',
messagingSenderId: 'Your credentials here',
};
That's your Firebase config object, it has all the information you need to access the different Firebase APIs, and we'll need that to connect our Ionic app to our Firebase app.
Go into your src/app
folder and create a file called credentials.ts
the idea
of this file is to keep all of our credentials in one place, this file shouldn't
be in source control so add it to your .gitignore
file.
Copy your config object to that page. I'm going to change the name to something that makes more sense to me:
export var firebasConfig = {
apiKey: 'AIzaSyBJT6tfre8uh3LGBm5CTiO5DUZ4',
authDomain: 'playground.firebaseapp.com',
databaseURL: 'https://playground.firebaseio.com',
projectId: 'playground',
storageBucket: 'playground.appspot.com',
messagingSenderId: '3676553551',
};
We're exporting it so that we can import it into other files where we need to.
Now it's time for the final piece of this step, we need to initialize Firebase,
for that, let's go into app.module.ts
and first, let's import the
@angular/fire packages we'll need and our credential object:
import { AngularFireModule } from '@angular/fire';
import { AngularFirestoreModule } from '@angular/fire/firestore';
import { firebaseConfig } from './credentials';
Since we're only going to use the Firestore database, we import the base angularfire module and the Firestore module. If you also needed Authentication or Storage you'd need to add those modules here.
Inside your @NgModule()
look for your imports
array and add both the
angularfire module and the Firestore module:
imports: [
BrowserModule,
IonicModule.forRoot(MyApp),
AngularFireModule.initializeApp(firebaseConfig),
AngularFirestoreModule,
],
We're calling the .initializeApp(firebaseConfig)
method and passing our
credential object so that our app knows how to connect to Firebase.
And that's it, it might not look like much yet, but our Firebase and Ionic apps can now talk to each other.
Step #2: Add items to the list
It's time to start working with our data, we're going to build a CRUD app, we'll use a song list as an example, but the same principles apply to any Master/Detail work-flow you want to build.
The first thing we need is to understand how our data is stored, Firestore is a document-oriented NoSQL database, which is a bit different from the RTDB (Real-time Database.)
It means that we have two types of data in our database, documents, which are objects we can work with, and collections which are the containers that group those objects.
For example, if we're building a song database, our collection would be called songs, or songList, which would hold all the individual song objects, and each object would have its properties, like the song's name, artist, etc.
In our example, the song object will have five properties, an id, the album, the artist, a description, and the song's name. In the spirit of taking advantage of TypeScript's type-checking features, we're going to create an interface that works as a model for all of our songs.
Go into the src/app
folder and create a folder called models
, then add a
file called song.interface.ts
and populate it with the following data:
export interface Song {
id: string;
albumName: string;
artistName: string;
songDescription: string;
sonName: string;
}
That's the song's interface, and it will make sure that whenever we're working with a song object, it has all the data it needs to have.
To start creating new songs and adding them to our list we need to have a page that holds a form to input the song's data, let's create that page with the Ionic CLI, open the terminal and type:
ionic generate page pages/create
In fact, while we're at it, let's take a moment to create the detail page, it will be a detail view for a specific song, and the Firestore provider, it will handle all of the database interactions so that we can manage everything from that file.
ionic generate page pages/detail
ionic generate service services/data/firestore
Before we start adding code, we need to fix our routes, if you go to the file
app-routing.module.ts
you'll see your application routes:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: 'home', loadChildren: './home/home.module#HomePageModule' },
{
path: 'create',
loadChildren: './pages/create/create.module#CreatePageModule',
},
{
path: 'detail',
loadChildren: './pages/detail/detail.module#DetailPageModule',
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
We want to edit the route for our detail page so that the URL takes a parameter, the song's ID so that we can fetch the song from Firestore:
{ path: 'detail/:id',
loadChildren: './pages/detail/detail.module#DetailPageModule'
},
Now we need a way to go from the home page to the CreatePage, for that open
home.html
and change your header to look like this:
<ion-header>
<ion-toolbar>
<ion-title>Song List</ion-title>
<ion-buttons slot="end">
<ion-button routerLink="/create">
<ion-icon slot="icon-only" name="add"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
We're adding a button to the header that triggers the angular router and navigates to the create page.
Now that we can navigate to the create page, let's add the functionality there,
go ahead and open the create.page.html
file and inside the
<ion-content></ion-content>
tags create the form:
<ion-content padding>
<form [formGroup]="createSongForm" (submit)="createSong()">
<ion-item>
<ion-label stacked>Song Name</ion-label>
<ion-input
formControlName="songName"
type="text"
placeholder="What's this song called?"
>
</ion-input>
</ion-item>
<ion-item>
<ion-label stacked>Artist Name</ion-label>
<ion-input
formControlName="artistName"
type="text"
placeholder="Who sings this song?"
>
</ion-input>
</ion-item>
<ion-item>
<ion-label stacked>Album Name</ion-label>
<ion-input
formControlName="albumName"
type="text"
placeholder="What's the album's name?"
>
</ion-input>
</ion-item>
<ion-item>
<ion-label stacked>Song Description</ion-label>
<ion-textarea
formControlName="songDescription"
type="text"
placeholder="What's this song about?"
>
</ion-textarea>
</ion-item>
<ion-button expand="block" type="submit" [disabled]="!createSongForm.valid">
Add Song
</ion-button>
</form>
</ion-content>
If you're new to angular forms then here's what's going on:
[formGroup]="createSongForm"
=> This is the name of the form we're creating.(submit)="createSong()"
=> This tells the form that on submit it should call thecreateSong()
function.formControlName
=> This is the name of the field.[disabled]="!createSongForm.valid"
=> This sets the button to be disabled until the form is valid.
If you try to run your app it's going to give you an error that says:
Can't bind to 'formGroup' since it isn't a known property of 'form'
You need to open your create.module.ts
file and import the
ReactiveFormsModule
and then add it to the imports array:
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@NgModule({
imports: [
...,
FormsModule,
ReactiveFormsModule,
...,
],
})
Now let's move to the create.page.ts
file, in here, we'll collect the data
from our form and pass it to our provider. First, let's import everything we'll
need:
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { LoadingController, AlertController } from '@ionic/angular';
import { FirestoreService } from '../../services/data/firestore.service';
We're importing:
- Form helper methods from
@angular/forms
. - Loading controller to show a loading widget to our users while the form processes the data.
- Alert controller to display an alert to our user if there are any errors.
- And the Firestore service to call the function that will add the song to the database.
Now we need to inject all those providers to the constructor and initialize our form:
public createSongForm: FormGroup;
constructor(
public loadingCtrl: LoadingController,
public alertCtrl: AlertController,
private firestoreService: FirestoreService,
formBuilder: FormBuilder,
private router: Router
) {
this.createSongForm = formBuilder.group({
albumName: ['', Validators.required],
artistName: ['', Validators.required],
songDescription: ['', Validators.required],
songName: ['', Validators.required],
});
}
And now all we need is the function that collects the data and sends it to the
provider if you remember the HTML part, we called it createSong()
async createSong() { }
The first thing we want to do inside that function is to trigger a loading component that will let the user know that the data is processing, and after that, we'll extract all the field data from the form.
async createSong() {
const loading = await this.loadingCtrl.create();
const albumName = this.createSongForm.value.albumName;
const artistName = this.createSongForm.value.artistName;
const songDescription = this.createSongForm.value.songDescription;
const songName = this.createSongForm.value.songName;
return await loading.present();
}
And lastly, we'll send the data to the provider, once the song is successfully created the user should navigate back to the previous page, and if there's anything wrong while creating it we should display an alert with the error message.
async createSong() {
const loading = await this.loadingCtrl.create();
const albumName = this.createSongForm.value.albumName;
const artistName = this.createSongForm.value.artistName;
const songDescription = this.createSongForm.value.songDescription;
const songName = this.createSongForm.value.songName;
this.firestoreService
.createSong(albumName, artistName, songDescription, songName)
.then(
() => {
loading.dismiss().then(() => {
this.router.navigateByUrl('');
});
},
error => {
loading.dismiss().then(() => {
console.error(error);
});
}
);
return await loading.present();
}
NOTE: As a good practice, handle those errors yourself, instead of showing the default error message to the users make sure you do something more user-friendly and use your custom messages, we're technicians, we know what the error means, most of the time our users won't.
We almost finish this part, you should be seeing an error becase
firestoreService.createSong()
doesnt exists.
So now we have to create the function inside of the service. The function will receive all the form data and store it in the database.
Open firestore.service.ts
and let's do a few things, we need to:
- Import Firestore.
- Import our Song interface.
- Inject firestore in the constructor.
- And write the
createSong()
function that takes all the parameters we sent from our form.
import { Injectable } from '@angular/core';
import { AngularFirestore } from '@angular/fire/firestore';
import 'firebase/firestore';
import { Song } from '../../models/song.interface';
@Injectable({
providedIn: 'root'
})
export class FirestoreService {
constructor(public firestore: AngularFirestore) {}
createSong(
albumName: string,
artistName: string,
songDescription: string,
songName: string
): Promise<void> { }
}
The function is taking all of the parameters we're sending. Now we're going to
do something that might seem unusual. We're going to use the firestore
createId()
function to generate an id for our new song.
createSong(
albumName: string,
artistName: string,
songDescription: string,
songName: string
): Promise<void> {
const id = this.firestore.createId();
}
Firestore auto-generates IDs for us when we push items to a list, but I like to create the ID first and then store it inside the item, that way if I pull an item I can get its ID right there, and don't have to do any other operations to get it.
Now that we created the id, we're going to create a reference to that song and set all the properties we have, including the id.
createSong(
albumName: string,
artistName: string,
songDescription: string,
songName: string
): Promise<void> {
const id = this.firestore.createId();
return this.firestore.doc(`songList/${id}`).set({
id,
albumName,
artistName,
songDescription,
songName,
});
}
That last piece of code is creating a reference to the document identified with
that ID inside our songList
collection, and after it creates the reference, it
adds all the information we sent as parameters.
And that's it. You can now add songs to our list. And once each song is created the user will navigate back to the homepage, where we'll now show the list of songs stored in the database.
Step #3: Show the list of items
To show the list of songs we'll follow the same approach we used for our last functionality, we'll create the HTML view, the TypeScript Class, and the function inside the provider that communicates with Firebase.
Since we have the provider opened from the previous functionality let's start
there, we want to create a function called getSongList()
the function should
return a collection of songs:
getSongList(): Observable<Song[]> {
return this.firestore.collection<Song>(`songList`).valueChanges();
}
The .valueChanges()
method takes the AngularFirestoreCollection and transforms
it into an Observable of type Songs.
Note that for that to work you need to import Observable
from the rxjs
package (Or remove the type checking if you don't care about it).
import { Observable } from 'rxjs';
Now, let's go to the home page and import everything we'll need:
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { FirestoreService } from '../services/data/firestore.service';
import { Song } from '../models/song.interface';
We want the Song
interface for a strict type checking, the FirestoreService
to communicate with the database, and Observable
also for type checking, our
provider will return an AngularFirestoreCollection that we'll turn into an
observable to display on our view.
Then, inside our class we want to create the songList
property, we'll use it
to display the songs in the HTML, and inject the firestore provider in the
constructor.
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage implements OnInit {
public songList: Observable<Song[]>;
constructor(
private firestoreService: FirestoreService
) { }
}
And lastly we want to wait until the page loads and fetch the list from our provider:
ngOnInit() {
this.songList = this.firestoreService.getSongList();
}
Now we can go to home.html
and inside the <ion-content>
we'll loop over our
list of songs to display all the songs in the database.
<ion-content class="padding">
<ion-item
*ngFor="let song of songList | async"
routerLink="/detail/{{song.id}}"
>
<ion-label>
<h2>{{ song.songName}}</h2>
<p>Artist Name: {{ song.artistName }}</p>
</ion-label>
</ion-item>
</ion-content>
We're only showing the song's name and artist's name, and we're adding a router link to send the user to the detail page sending the song's ID through the URL.
For now, grab a cookie or something, you've read a lot, and you're sugar levels might need a boost. See you in a few minutes in the next section :-)
Step #4: Navigate to one item's detail page
In the previous step, we created a function that takes us to the detail page with the song information, and now we're going to use that information and display it for the user to see.
We're passing the song's ID as a navigation parameter and we're going to use that ID to fetch the song's detail from the firestore database.
The first thing we'll do is go to detail.page.html
and create a basic view
that displays all the data we have for our song:
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-title>{{ song?.songName }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<h3>Artist</h3>
<p>The song {{ song?.songName }} was released by {{ song?.artistName }}.</p>
<h3>Album</h3>
<p>It was part of the {{ song?.albumName }} album.</p>
<h3>Description</h3>
<p>{{ song?.songDescription }}</p>
<ion-button expand="block" (click)="deleteSong()"> DELETE SONG </ion-button>
</ion-content>
We're showing the song's name in the navigation bar, and then we're adding the rest of the data to the content of the page.
Now let's jump into detail.page.ts
so we can get song
otherwise this will
error out.
We need to create a property song
of type Song
, for that we need to import
the Song
interface.
Then, you want to get the navigation parameter we sent to the page and assign
its value to the song
property you created.
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Song } from '../../models/song.interface';
import { FirestoreService } from '../../services/data/firestore.service';
@Component({
selector: 'app-detail',
templateUrl: './detail.page.html',
styleUrls: ['./detail.page.scss'],
})
export class DetailPage implements OnInit {
public song: Song;
constructor(
private firestoreService: FirestoreService,
private route: ActivatedRoute
) { }
ngOnInit() {
const songId: string = this.route.snapshot.paramMap.get('id');
this.firestoreService.getSongDetail(songId).subscribe(song => {
this.song = song;
});
}
}
For this to work, we need to go to the firestore.service.ts
and add the
getSongDetail()
function:
getSongDetail(songId: string): Observable<Song> {
return this.firestore.collection('songList').doc<Song>(songId).valueChanges();
}
You should do a test right now running ionic serve
your app should be working,
and you should be able to create new songs, show the song list, and enter a
song's detail page.
Step #5: Delete an item from the list
In the last part of the tutorial we're going to add a button inside the DetailPage, that button will give the user the ability to remove songs from the list.
First, open detail.page.html
and create the button, nothing too fancy, a
regular button that calls the remove function will do, set it right before the
closing ion content tag.
<ion-button expand="block" (click)="deleteSong(song.id, song.songName)">
DELETE SONG
</ion-button>
Now go to the detail.ts
and create the deleteSong()
function, it should take
2 parameters, the song's ID and the song's name:
async deleteSong(songId: string, songName: string): Promise<void> {}
The function should trigger an alert that asks the user for confirmation, and if the user accepts the confirmation, it should call the delete function from the provider, and then return to the previous page (Our home page or list page).
For this, we'll need to first import and inject the alert controller and the router, the alert controller will show the alert, and the router will navigate us to the song list once the song is deleted.
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Song } from '../../models/song.interface';
import { FirestoreService } from '../../services/data/firestore.service';
import { AlertController } from '@ionic/angular';
@Component({
selector: 'app-detail',
templateUrl: './detail.page.html',
styleUrls: ['./detail.page.scss'],
})
export class DetailPage implements OnInit {
public song: Observable<Song>;
constructor(
private firestoreService: FirestoreService,
private route: ActivatedRoute,
private alertController: AlertController,
private router: Router
) { }
}
Now, let's create the delete function:
async deleteSong(songId: string, songName: string): Promise<void> {
const alert = await this.alertController.create({
message: `Are you sure you want to delete ${songName}?`,
buttons: [
{
text: 'Cancel',
role: 'cancel',
handler: blah => {
console.log('Confirm Cancel: blah');
},
},
{
text: 'Okay',
handler: () => {
this.firestoreService.deleteSong(this.songId).then(() => {
this.router.navigateByUrl('');
});
},
},
],
});
await alert.present();
}
NOTE: Make sure to import AlertController for this to work.
Now, all we need to do is go to our provider and create the delete function:
deleteSong(songId: string): Promise<void> {
return this.firestore.doc(`songList/${songId}`).delete();
}
The function takes the song ID as a parameter and then uses it to create a
reference to that specific document in the database. Lastly, it calls the
.delete()
method on that document.
And that's it. You should have a fully functional Master/Detail functionality where you can list objects, create new objects, and delete objects from the database 😁