Elevating Data Management: Implementing Firebase Database and Angular Service for CRUD Operations on Notes (Part 13) in Your Angular-15 Ionic-7 App

Welcome back to our ongoing series on building a multiplatform application with Angular-15 and Ionic-7! In our previous articles, we've explored various aspects of app development, from authentication to integrating external APIs. Today, in Part 13, we will delve into the world of data management by utilizing Firebase Database and an Angular service to perform CRUD (Create, Read, Update, Delete) operations on notes within our app.

Efficiently managing data is a critical aspect of app development, and Firebase Database provides a real-time NoSQL cloud database that seamlessly synchronizes data across different platforms and devices. Combined with Angular-15's powerful framework and Ionic-7's versatility, we can create a robust and interactive app that allows users to create, view, update, and delete notes effortlessly.


In this installment, we'll guide you through the process of integrating Firebase Database into our Angular-15 Ionic-7 app to enable seamless data management. We'll explore how to set up the Firebase project, initialize the Firebase Database, and configure the necessary authentication rules to secure our data.

We'll also dive into the implementation details, demonstrating how to create an Angular service to interact with the Firebase Database. This service will provide the functionality to perform CRUD operations on notes, including creating new notes, fetching existing notes, updating note content, and deleting unwanted notes.

Throughout this tutorial, we'll emphasize best practices for data organization, error handling, and optimizing data retrieval and updates. By the end of this article, you'll have a solid understanding of how to leverage Firebase Database and an Angular service to efficiently manage notes within your Angular-15 Ionic-7 app.

So, join us in Part 13 of our series as we embark on a journey into the world of data management. Together, let's empower our app with the ability to perform seamless CRUD operations on notes, enabling users to organize and interact with their data effectively. Get ready to take your app to the next level of functionality and user experience!

Till now, I have only used the authentication service provided by Firebase but now I will use Firebase for the primary purpose for which it was built and that is, a NoSQL cloud database service that stores data in JSON format without a traditional structure like SQL databases.

To begin, I will be using the previously completed steps to install angular fire and enter the firebase API keys in the environment.ts file. Since the basic setup is already done, I will simply go to my Firebase console and start using the Firestore database from the side menu. In the rules of the database, I will make sure there are no unnecessary rules that prevent the user from testing during the development stages. The rules should look something like this:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if
          request.time < timestamp.date(2023, 5, 19);
    }
  }
}

That’s it. I won’t be creating any data collections manually in the Firebase database. I will use the Firestore package within my application to create a new collection and new documents within the collection.

To demonstrate CRUD operations using Firebase, I have created a notes feature where students can write short notes and save them within the application and on the Firebase database. Here the students can create new notes, view all the notes, update an existing note, or delete them.

To begin, I will create a service that will communicate with the Firebase database with the following command:

ionic generate service services/diary-data

This will create a service file which I use to communicate with the database in firebase and the data manipulation on the ‘Diary’ page in the bottom navigation that I created earlier. In the diary-data.service.ts file, I will write the following logic:

import { Injectable } from '@angular/core';
import {
 Firestore, collectionData, collection, docData, doc, addDoc, deleteDoc, updateDoc, DocumentData, getDocs, } from '@angular/fire/firestore';
import { Observable, map } from 'rxjs';

// create an interface for the note
export interface Note {
  id?: string;
  title: string;
  text: string;
  timestamp?: string;
}

@Injectable({
  providedIn: 'root',
})
export class DiaryDataService {
  constructor(private firestore: Firestore) {} // inject the firestore service

  // get the notes from the firestore
  getNotes(): Observable<Note[]> {
    // return an observable of type Note[]
    // create a reference to the notes collection
    const notesRef = collection(this.firestore, 'notes');
    // added the idField option to the collectionData operator
    // to get the id of the document
    return collectionData(notesRef, { idField: 'id' }) as Observable<Note[]>; // return the notes
  }

  // get Note by id from the firestore
  getNoteById(id: string): Observable<Note> {
    // return an observable of type Note
    const noteDocRef = doc(this.firestore, `notes/${id}`); // create a reference to the note document
    return docData(noteDocRef, { idField: 'id' }) as Observable<Note>; // return the note by id
  }

  // add Note to the firestore
  addNote(note: Note) {
    const notesRef = collection(this.firestore, 'notes'); // create a reference to the notes collection
    return addDoc(notesRef, note); // add the note to the firestore
  }

  // delete note from the firestore
  deleteNoteById(note: Note) {
    const noteDocRef = doc(this.firestore, `notes/${note.id}`); // create a reference to the note document
    return deleteDoc(noteDocRef); // delete the note from the firestore
  }

  // update note in the firestore
  updateNoteById(note: Note) {
    const noteDocRef = doc(this.firestore, `notes/${note.id}`); // create a reference to the note document
    return updateDoc(noteDocRef, { title: note.title, text: note.text }); // update the note in the firestore with the new title and text passed in the note object
  }
}

The above code defines the Angular service I created before, called DiaryDataService that interacts with a Firestore database to perform CRUD (Create, Read, Update, and Delete) operations on a collection of notes. The service imports necessary modules from @angular/fire/firestore, including Firestore, collectionData, collection, docData, doc, addDoc, deleteDoc, updateDoc, and getDocs. It also imports Observable and map from rxjs.

First, I’ve created the method, getNotes() that returns an observable that represents a collection of Note objects. Note interface is already created after imports which creates a structure for the Note object that will be used further. The method creates a reference to a firestore collection called notes using the collection() method, then applies the collectionData() operator with the idField option to retrieve the documents with their IDs. The Observable<Note[]> is returned. This method will be used to get all the notes and display them on separate cards on the main Notes tab of the Diary page.

Secondly, I have created the method getNoteById(id: string) that returns an observable that represents a single Note object based on its ID. It creates a reference to a firestore document with a specific ID by using the doc() method, then applies the docData() operator with the idField option to retrieve the document with its ID. The Observable<Note> is returned. This will be used to access a single note to be displayed in a separate modal so that it can updated or deleted.

Third, I have created the method, addNote(note: Note) which adds a new Note object to the notes collection in firestore using the addDoc() method. The new Note object is passed in as a parameter, and the method returns the promise of the new document's ID as by default the return type is addNote(note:Note): Promis<void>{}. This method will be used to add a new note to the notes data collection.

The fourth method, deleteNoteById(note: Note) deletes a Note object from the notes collection in firestore based on its ID. It creates a reference to the document by using the doc() method and passes it to the deleteDoc() method. The method returns a promise indicating if the deletion was successful. This method will be used to delete a single note from its respective modal page.

Lastly, the fifth method, updateNoteById(note: Note) updates a Note object in the notes collection in firestore based on its ID. It creates a reference to the document by using the doc() method and passes it to the updateDoc() method along with the new title and text fields. The method returns a promise indicating if the update was successful.

I have used the following documentation to build the above service using the latest version of firestore:

Get started with Cloud Firestore  |  Firebase

Now, once the service is created, I will use it to build the Notes feature on the Diary page. First I will write the logic to add a new note and open an existing note which I have created using the ionic generate page page/diary-modal and imported it into app.component.ts also, and written the code to open it in a separate modal box in the diary.page.ts file in the following way:

import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import {
  AlertController, IonicModule, ModalController, ToastController,
} from '@ionic/angular';
import { DiaryDataService, Event } from 'src/app/services/diary-data.service';
import { DiaryModalPage } from '../diary-modal/diary-modal.page';
import { RouterLink } from '@angular/router';

@Component({
  selector: 'app-diary',
  templateUrl: './diary.page.html',
  styleUrls: ['./diary.page.scss'],
  standalone: true,
  imports: [ IonicModule, CommonModule, FormsModule, DiaryModalPage, RouterLink,],})

export class DiaryPage implements OnInit {
  selectTabs: string = 'notes'; // set the default tab
  notes: any = []; // array of notes

  constructor(
    private diaryDataService: DiaryDataService,
    private alertCtrl: AlertController,
    private modalCtrl: ModalController,
    private toastCtrl: ToastController,
  ) {
    this.diaryDataService.getNotes().subscribe((res) => {
      // get the notes from the diary data service
      // console.log(res);
      // subscribe to the notes observable
      this.notes = res; // assign the notes property to the array of notes returned by the observable
    });
  }

  ngOnInit() {}

  //------------------Notes------------------//

  async openNote(note: any) {
    // open the note
    const modal = await this.modalCtrl.create({
      // create a modal
      component: DiaryModalPage, // set the modal component
      componentProps: { id: note.id }, // pass the id of the note to the modal
      breakpoints: [0, 0.75, 1], // set the breakpoints
      initialBreakpoint: 0.75, // set the initial breakpoint
    }); // create a modal
    modal.present(); // present the modal
  }

  async addNote() {
    // add a note
    const alert = await this.alertCtrl.create({
      // create an alert
      header: 'Add Note', // set the header
      inputs: [
        // array of inputs
        {
          name: 'title', // input name
          type: 'text', // input type
          placeholder: 'Enter Your Note Title', // input placeholder
        },
        {
          name: 'text', // input name
          type: 'textarea', // input type
          placeholder: 'I need to learn react tomorrow...', // input placeholder
        },
      ],
      buttons: [
        // array of buttons
        {
          text: 'Cancel', // button text
          role: 'cancel', // set to cancel role
          cssClass: 'secondary', // set a css class for buttons
          handler: () => {
            // button handler
            console.log('Confirm Cancel');
          },
        },
        {
          text: 'Save', // button text
          handler: (note) => {
            // button handler
            console.log(note);
            // const timestamp = new Date().getTime(); // get current timestamp
            //get the current date and time
            //create a standard date time format
            const standardDateTimeFormat = new Intl.DateTimeFormat('en-US', {
              year: 'numeric',
              month: '2-digit',
              day: '2-digit',
              hour: '2-digit',
              minute: '2-digit',
              second: '2-digit',
            });

            const currentDate = new Date(); // get the current date and time

            const timestamp = standardDateTimeFormat.format(currentDate); // format the date and time to a standard format and assign it to the timestamp variable

            this.diaryDataService.addNote({
              title: note.title,
              text: note.text,
              timestamp: timestamp, // add timestamp property to the note object
            }); // add the note to the firestore
            const toast = this.toastCtrl.create({
              // create a toast
              message: 'Note Added', // set the message
              duration: 2000, // set the duration
            }); // create a toast
            toast.then((toast: any) => toast.present()); // present the toast
          },
        },
      ],
    }); // create an alert
    alert.present(); // present the alert
  }
}

The above class has four dependencies injected into its constructor via dependency injection: DiaryDataService, AlertController, ModalController, and ToastController. The DiaryDataService service is used to retrieve and add notes to a database, while the other three services are used for displaying messages and creating user interfaces like alert boxes for adding new note details, toast to indicate that some action has been take, and a modal to convert a new diary-modal page into a modal and make it appear as a bottom sheet.

The class has three properties: selectTabs, notes, and idCounter. selectTabs is a string that represents the currently selected tab of the diary page. notes is an array that will hold the notes. idCounter is an integer that is used to keep track of the next unique ID for a note.

In the constructor, the DiaryDataService service is used to retrieve the notes from the database and assign them to the notes property. This is done by subscribing to an observable that emits the notes. When the notes are emitted, the callback function assigns the notes to the notes property.

The openNote() method is used to display a modal window with the contents of a selected note. It creates a modal by calling the create() method of the ModalController service and passing in a configuration object that includes the component to display (DiaryModalPage) and the ID of the note to display. Once the modal is created, it is presented to the user.

The addNote() method is used to add a new note to the diary. It first creates an alert window using the AlertController service. The alert window contains two input fields, one for the title of the note and one for the text of the note. The user can fill in these fields and then save the note by clicking the Save button.

When the user clicks the Save button, the handler function of the button is called. This function retrieves the values of the input fields, generates a timestamp, and calls the addNote() method of the DiaryDataService service, passing in an object that contains the title, text, and timestamp of the note. The addNote() method adds the note to the database. Once the note is added, a toast message is displayed using the ToastController service to indicate that the note has been added.

Next, I will use the above logic to display the template in diary.page.html file in the following way:

<ion-header [translucent]="false">
  <ion-toolbar>
    <ion-title>My Diary</ion-title>
    <ion-buttons slot="start">
      <ion-button routerLink="/tabs/notifications">
        <ion-icon slot="icon-only" name="notifications-outline"></ion-icon>
      </ion-button>
    </ion-buttons>
    <ion-buttons slot="end">
      <ion-menu-button menu="main-menu"></ion-menu-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">My Diary</ion-title>
    </ion-toolbar>
  </ion-header>

  <!-- Added a segment to the page to allow the user to switch between different pages -->

  <!-- Added segments from ionic framework to segment the page into three separate pages all accessible via a segment nav on top -->

  <ion-segment [(ngModel)]="selectTabs">
    <!-- ngModel is used to bind the value of the segment to the selectTabs variable -->
    <ion-segment-button value="schedule">
      <ion-label>Schedule</ion-label>
      <ion-icon name="calendar-outline"></ion-icon>
    </ion-segment-button>

    <ion-segment-button value="tasks">
      <ion-label>Tasks</ion-label>
      <ion-icon name="checkbox-outline"></ion-icon>
    </ion-segment-button>

    <ion-segment-button value="notes">
      <ion-label>Notes</ion-label>
      <ion-icon name="document-text-outline"></ion-icon>
    </ion-segment-button>
  </ion-segment>

  <!-- Added ngIf below to display data specific to the selected segment -->
  <ion-content class="ion-padding-top" *ngIf="selectTabs === 'schedule'">
  </ion-content>

  <div *ngIf="selectTabs === 'tasks'">
  </div>

  <div *ngIf="selectTabs === 'notes'">
    <ion-card class="ion-margin-top" *ngFor="let note of notes">
      <ion-card-header (click)="openNote(note)">
        <ion-card-subtitle>{{note.timestamp}}</ion-card-subtitle>
        <ion-card-title>{{note.title}}</ion-card-title>
      </ion-card-header>
      <ion-card-content (click)="openNote(note)">
        {{note.text.slice(0, 100) + '...'}}
      </ion-card-content>
    </ion-card>
  </div>

  <ion-fab slot="fixed" vertical="bottom" horizontal="end">
    <ion-fab-button>
      <ion-icon name="add"></ion-icon>
    </ion-fab-button>
    <ion-fab-list side="top">
      <ion-fab-button (click)="addNewEvent()" color="tertiary" size="large">
        <ion-icon name="calendar-outline"></ion-icon>
      </ion-fab-button>
      <ion-fab-button (click)="addTask()" color="tertiary" size="large">
        <ion-icon name="checkbox-outline"></ion-icon>
      </ion-fab-button>
      <ion-fab-button color="tertiary" size="large" (click)="addNote()">
        <ion-icon name="document-text-outline"></ion-icon>
      </ion-fab-button>
    </ion-fab-list>
  </ion-fab>
</ion-content>

In the above code, an <ion-segment> is added to the page to allow the user to switch between different pages. The segment UI component is referenced from ion-segment: API Documentation for Segmented Controls (ionicframework.com) from the Ionic documentation. The [(ngModel)] directive binds the value of the segment to the selectTabs variable in the code. There are three buttons within the segment, each with a value of "schedule", "tasks", and "notes", respectively.

Below the segment, there are three <div> elements with *ngIf directives. The *ngIf directives are used to display data specific to the selected segment, based on the selectTabs variable.

The second <div> contains an <ion-card> element that displays a list of notes using the *ngFor directive. Each note is displayed in an <ion-card-header> with the date, title, and text content. The openNote() function is called when the user clicks on a note header or content, allowing the user to view and edit the note. This <div> is displayed if selectTabs is equal to "notes".

Finally, there is an <ion-fab> element that provides a floating action button for adding new events, tasks, and notes. When the button is clicked, the corresponding function (addNewEvent(), addTask(), or addNote()) is called. This element is fixed to the bottom-right corner of the screen.

Next, I will write the logic for the modal page that appears when a user clicks on Add New Note button in the diary-modal.page.ts file in the following way:

import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule, ModalController, ToastController } from '@ionic/angular';
import { DiaryDataService, Note } from 'src/app/services/diary-data.service';
import { Input } from '@angular/core';

@Component({
  selector: 'app-diary-modal',
  templateUrl: './diary-modal.page.html',
  styleUrls: ['./diary-modal.page.scss'],
  standalone: true,
  imports: [IonicModule, CommonModule, FormsModule],
})
export class DiaryModalPage implements OnInit {
  @Input() id!: string; // id of the note taken from the modal component props
  note!: Note; // note object

  constructor(
    private diaryDataService: DiaryDataService,
    private modalCtrl: ModalController,
    private toastCtrl: ToastController
  ) {}

  ngOnInit() {
    this.diaryDataService.getNoteById(this.id).subscribe((res) => {
      this.note = res; 
// assign the note property to the note returned by the observable
    });
  }

  // update the note
  async updateNote() {
    this.diaryDataService.updateNoteById(this.note); // update the note
    const toast = await this.toastCtrl.create({
      // create a toast
      message: 'Note Updated', // set the message
      duration: 2000, // set the duration
    }); // create a toast
    toast.present(); // present the toast
    this.modalCtrl.dismiss(); // dismiss the modal
  }

  // delete the note
  async deleteNote() {
    await this.diaryDataService.deleteNoteById(this.note);
    const toast = await this.toastCtrl.create({
      // create a toast
      message: 'Note Deleted', // set the message
      duration: 2000, // set the duration
    }); // create a toast
    toast.present(); // present the toast
    this.modalCtrl.dismiss(); // dismiss the modal
  }

  dismissModal() {
    //dismiss the modal
    this.modalCtrl.dismiss();
  }
}

The above code defines the class named DiaryModalPage that implements the OnInit interface.

The @Input() decorator is used to define an input property named id which is of type string and is taken from the modal component props. The note property is an object of type Note that represents the note that is being edited or deleted. The constructor function of the class takes in three parameters, diaryDataService, modalCtrl, and toastCtrl, which are injected by Angular's dependency injection system.

The ngOnInit() method is called after the component is initialized and is used to retrieve the note data from the DiaryDataService service using the getNoteById() method. Once the note data is retrieved, it is assigned to the note property.

The updateNote() method is used to update the note. It calls the updateNoteById() method of the DiaryDataService service to update the note, then creates a Toast to inform the user that the note was updated, and finally dismisses the modal. The deleteNote() method is used to delete the note. It calls the deleteNoteById() method of the DiaryDataService service to delete the note, then creates a Toast to inform the user that the note was deleted, and finally dismisses the modal.

The dismissModal() method is used to dismiss the modal. It simply calls the dismiss() method of the ModalController service to dismiss the modal.

Next, the template for this modal is written in diary-modal.page.html file in the following way:

<ion-header [translucent]="true"> </ion-header>

<ion-content [fullscreen]="true">
  <ion-list lines="full" *ngIf="note">
    <ion-item>
      <ion-label>{{note.timestamp}}</ion-label>
      <ion-icon slot="end" name="close" (click)="dismissModal()"></ion-icon>
    </ion-item>
    <ion-item>
      <ion-label position="stacked">Title</ion-label>
      <ion-input aria-label="title" [(ngModel)]="note.title"></ion-input>
    </ion-item>
    <ion-item>
      <ion-label position="stacked">Note</ion-label>
      <ion-textarea
        aria-label="note"
        [(ngModel)]="note.text"
        rows="8"
      ></ion-textarea>
    </ion-item>

    <ion-button
      color="danger"
      class="ion-margin"
      shape="round"
      (click)="deleteNote()"
      expand="block"
    >
      <ion-icon slot="start" name="trash-outline"></ion-icon>
      Delete
    </ion-button>
    <ion-button
      shape="round"
      class="ion-margin"
      color="success"
      (click)="updateNote()"
      expand="block"
    >
      <ion-icon slot="start" name="save"></ion-icon> Update
    </ion-button>
  </ion-list>
</ion-content>

The above code creates an <ion-list> element that creates a list of input fields. The *ngIf="note" directive ensures that the list is only displayed if a note object exists. Each <ion-item> element contains a label and an input field. The [(ngModel)] directive is used to bind the value of the input field to the corresponding property of the note object. The ion-button elements are used to create two buttons, one for deleting the note and one for updating it. The (click) event is used to specify what happens when the button is clicked.

And that’s it. The basic CRUD operations are implemented in the Notes segment of the diary as demonstrated in Figure 55, 56, 57, and 58:

Figure 55: Notes Segment in Diary



Figure 56: Add New Note Alert Box



Figure 57: Note added to notes



Figure 58: Update Note Modal


Conclusion:

In this thirteenth installment of our series on building a multiplatform application with Angular-15 and Ionic-7, we explored the integration of Firebase Database and an Angular service to perform CRUD operations on notes within our app. By leveraging the power of these tools, we created a robust data management system that allows users to efficiently organize and interact with their notes.

Data management is a fundamental aspect of app development, and Firebase Database provides a reliable and real-time NoSQL cloud database that seamlessly synchronizes data across different platforms and devices. By integrating Firebase Database with our Angular-15 Ionic-7 app, we were able to create a seamless data management experience for our users.

Throughout this tutorial, we walked you through the process of setting up a Firebase project, initializing Firebase Database, and configuring authentication rules to secure our data. We then implemented an Angular service that encapsulates the logic for performing CRUD operations on notes. This service allowed users to create new notes, retrieve existing notes, update note content, and delete unwanted notes.

In addition to the core functionality, we emphasized best practices for data organization, error handling, and optimizing data retrieval and updates. These practices ensure a smooth and efficient user experience, regardless of the size and complexity of the note database.

By leveraging Firebase Database and an Angular service, we have empowered our app with the ability to seamlessly manage notes. Users can effortlessly create, view, update, and delete their notes, enhancing productivity and organization within the app.

As you move forward, consider exploring additional features and functionalities that Firebase Database offers, such as real-time data synchronization and data querying capabilities. Stay updated with the latest updates from Firebase and Angular to leverage new enhancements and improvements that can further enhance your app's data management capabilities.

We hope this tutorial has provided you with valuable insights and practical knowledge on integrating Firebase Database and an Angular service to perform CRUD operations on notes in your Angular-15 Ionic-7 app. By embracing the power of these technologies, you can create an app that offers a seamless and efficient data management experience for your users.

Thank you for joining us on this journey as we explored the fascinating world of app development with Angular-15, Ionic-7, Firebase Database, and an Angular service. Stay tuned for future installments where we will continue to explore new features and functionalities to make our app even more robust and user-friendly.

Happy coding!

Popular Posts

Perform CRUD (Create, Read, Update, Delete) Operations using PHP, MySQL and MAMP : Part 4. My First Angular-15 Ionic-7 App

Visualize Your Data: Showcasing Interactive Charts for Numerical Data using Charts JS Library (Part 23) in Your Angular-15 Ionic-7 App

How to Build a Unit Converter for Your Baking Needs with HTML, CSS & Vanilla JavaScript