Building a CRUD Ionic application with Firestore

First Published: 13 April 2018
Updated on: 18 September 2018

Firestore is Firebase’s new 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.

screenshot showing the different views for CRUD firestore

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 AngularFire2), 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 angularfire2 firebase

That command will install the latest stable versions of both AngularFire2 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: 'javebratt-playground.firebaseapp.com',
  databaseURL: 'https://javebratt-playground.firebaseio.com',
  projectId: 'javebratt-playground',
  storageBucket: 'javebratt-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 AngularFire2 packages we’ll need and our credential object:

import { AngularFireModule } from 'angularfire2';
import { AngularFirestoreModule } from 'angularfire2/firestore';
import { firebaseConfig } from './credentials';

Since we’re only going to use the Firestore database, we import the base AF2 (I’m going to refer to AngularFire2 as AF2 from now on) 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 AF2 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 the createSong() 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,
  public firestoreService: FirestoreService,
  formBuilder: FormBuilder
) {
  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 => {
        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, all we need now is to create the function inside the provider that receives all the form data we’re sending and uses it to create a song in our 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 'angularfire2/firestore';
import { Song } from '../../models/song.interface';

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.

Firestore Form

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(): AngularFirestoreCollection<Song> {
  return this.firestore.collection(`songList`);
}

Note that for that to work you need to import AngularFirestoreCollection from the angularfire2/firestore package (Or remove the type checking if you don’t care about it).

Now, let’s go to the home page and import everything we’ll need:

import { Observable } from 'rxjs';
import { Song } from '../../models/song.interface';
import { FirestoreService } from '../../services/data/firestore.service';

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.

public songList;
constructor(
  private firestoreService: FirestoreService,
  private router: Router
) {}

And lastly we want to wait until the page loads and fetch the list from our provider:

ngOnInit() {
  this.songList = this.firestoreProvider.getSongList().valueChanges();
}

The .valueChanges() method takes the AngularFirestoreCollection and transforms it into an Observable of type Songs.

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 padding>
  <ion-card
    *ngFor="let song of songList | async"
    routerLink="/detail/{{song.id}}"
  >
    <ion-card-header>
      {{ song.songName }}
    </ion-card-header>
    <ion-card-content>
      Artist Name: {{ song.artistName }}
    </ion-card-content>
  </ion-card>
</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.

List of data from Firestore

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 | async)?.songName }}</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content padding>
  <h3>Artist</h3>
  <p>
    The song {{ (song | async)?.songName }} was released by {{ (song |
    async)?.artistName }}.
  </p>

  <h3>Album</h3>
  <p>
    It was part of the {{ (song | async)?.albumName }} album.
  </p>

  <h3>Description</h3>
  <p>
    {{ (song | async)?.songDescription }}
  </p>
</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 } from '@angular/router';
import { Song } from '../../models/song.interface';
import { FirestoreService } from '../../services/data/firestore.service';
import { Observable } from 'rxjs';

@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
  ) {}

  ngOnInit() {
    const songId: string = this.route.snapshot.paramMap.get('id');
    this.song = this.firestoreService.getSongDetail(songId).valueChanges();
  }
}

For this to work, we need to go to the firestore.service.ts and add the getSongDetail() function:

getSongDetail(songId: string): AngularFirestoreDocument<Song> {
  return this.firestore.collection('songList').doc(songId);
}

You’ll need to import AngularFirestoreDocument for that to work.

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.

Firestore Detail View

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()">
  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:

deleteSong(): 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).

async deleteSong() {
  const alert = await this.alertController.create({
    message: 'Are you sure you want to delete the song?',
    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.

Firestore Delete 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 :-)

Next Steps

Congratulations, that was a long one, but I’m confident you now understand more about Firestore and how to use it with Firebase.

I created a free e-book that covers how to build an entire app using Firestore, Authentication, Cloud Storage and more, you can get it here.