Real-Time Data Management: Performing CRUD Operations in Real-Time with Supabase PostgreSQL Database (Part 20) 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 explored a wide range of topics, including authentication, data management, task organization, calendar integration, event management, media management, UI customization, and enhanced user authentication. Today, in Part 20, we're diving into real-time data manipulation by performing CRUD (Create, Read, Update, Delete) operations using the Supabase PostgreSQL database.

Real-time data manipulation is a crucial aspect of many applications, enabling users to interact with and modify data in real time. By leveraging the capabilities of Supabase, a powerful PostgreSQL database service, we can seamlessly integrate real-time CRUD operations into our Angular-15 Ionic-7 app, offering users a responsive and dynamic data manipulation experience.


In this installment, we'll guide you through the process of performing real-time CRUD operations using the Supabase PostgreSQL database. We'll start by setting up the necessary dependencies and configuring our app to establish a connection with the Supabase database.

Next, we'll explore the fundamental CRUD operations - Create, Read, Update, and Delete - and demonstrate how to implement them in our app using Supabase's real-time functionality. We'll cover the necessary code modifications, event handling, and data synchronization to ensure that changes made by one user are instantly reflected in real time for all connected users.

Throughout this tutorial, we'll emphasize best practices for real-time data management, error handling, and synchronization. By the end of this article, you'll have a solid understanding of how to leverage the power of Supabase and Angular-15 Ionic-7 to perform real-time CRUD operations, allowing users to seamlessly create, read, update, and delete data within our app.

So, join us in Part 20 of our series as we embark on a journey into the realm of real-time data manipulation. Together, let's harness the power of Supabase and Angular-15 Ionic-7 to create a responsive and dynamic data management experience, enhancing the interactivity and usability of our application.

Tutorial

Here I will use the previously written service for the achievements login page to build the achievements list that the student will see after the login. To begin, I have created a database using the following SQL in the Supabase console:

--
-- For use with:
-- <https://github.com/supabase/supabase/tree/master/examples/todo-list/sveltejs-todo-list> or
-- <https://github.com/supabase/examples-archive/tree/main/supabase-js-v1/todo-list>
--

create table todos (
  id bigint generated by default as identity primary key,
  user_id uuid references auth.users not null,
  task text check (char_length(task) > 3),
  is_complete boolean default false,
  inserted_at timestamp with time zone default timezone('utc'::text, now()) not null
);
alter table todos enable row level security;
create policy "Individuals can create todos." on todos for
    insert with check (auth.uid() = user_id);
create policy "Individuals can view their own todos. " on todos for
    select using (auth.uid() = user_id);
create policy "Individuals can update their own todos." on todos for
    update using (auth.uid() = user_id);
create policy "Individuals can delete their own todos." on todos for
    delete using (auth.uid() = user_id);

This is a boilerplate code I have used the SQL snippets provided by supabase. Since the API keys are already in the application’s environment file. I will use the Supabase documentation to write the service which communicates with the above-created database in supabase.service.ts like this:

import { Injectable } from '@angular/core';
import { createClient, SupabaseClient, User } from '@supabase/supabase-js';
import { BehaviorSubject } from 'rxjs';
import { environment } from 'src/environments/environment';
import { Router } from '@angular/router';
import { Observable } from 'rxjs';

const TODO_DB = 'todos'; // define the name of the table in the database

export interface Todo {
  id: number;
  inserted_at: string;
  is_complete: boolean;
  task: string;
  user_id: string;
}

@Injectable({
  providedIn: 'root',
})
export class SupabaseService {
  supabase: SupabaseClient; // define supabase as a SupabaseClient type variable (from @supabase/supabase-js)

  private _todos: BehaviorSubject<any> = new BehaviorSubject([]); // this is a BehaviorSubject from rxjs that is used to store the todos and is initialized with an empty array as the default value

  // Try to recover our user session
  async ngOnInit() {
    await this.loadUser();
  }

  constructor(private router: Router) {
    // initialize supabase with the environment variables
    this.supabase = createClient(
      // createClient is a function from @supabase/supabase-js that is used to initialize supabase
      environment.supabaseUrl, // environment variables from src/environments/environment.ts
      environment.supabaseKey,
      {
        auth: {
          // auth is an object from @supabase/supabase-js that is used to store the logged-in session
          autoRefreshToken: true, // autoRefreshToken is a boolean from @supabase/supabase-js that is used to refresh the token for logged-in users
          persistSession: true, // persistSession is a boolean from @supabase/supabase-js that is used to store the logged-in session
        },
      }
    );

    this.supabase.auth.onAuthStateChange((event, session) => {
      this.loadNotifications();
      //notifications are made accessible without login here
      if (event == 'SIGNED_IN') {
        this._currentUser.next(session?.user);
        this.loadTodos();
        this.handleTodosChanged();

      } else {
        this._currentUser.next(false);
      }
    });
  }

  
  get todos(): Observable<Todo[]> {
    // this is a getter function that is used to return the todos BehaviorSubject as an Observable to access the private todos BehaviorSubject from outside the service
    return this._todos.asObservable(); // return the todos BehaviorSubject as an Observable
  }

  async loadTodos() {
    const query = await this.supabase.from(TODO_DB).select('*'); // select all the todos from the database table and store them in a variable called query
    // this is async because we are using await to wait for the query to finish before continuing
    // console.log('query: ', query);
    this._todos.next(query.data); // pass the todos to the todos BehaviorSubject
  }

  async addTodo(task: string) {
    const newTodo = {
      user_id: this._currentUser.value.id,
      task,
    };

    const result = await this.supabase.from(TODO_DB).insert(newTodo); // insert the new todo into the database table
  }

  async removeTodo(id: number) {
    await this.supabase.from(TODO_DB).delete().match({ id }); // delete the todo from the database table
  }

  async updateTodo(id: number, is_complete: boolean) {
    await this.supabase.from(TODO_DB).update({ is_complete }).match({ id }); // update the todo in the database table
  }

  handleTodosChanged() {
    // this function is used to handle changes to the todos in the database table and update the todos BehaviorSubject in real-time when the database table is updated
    this.supabase
      .channel(TODO_DB) // channel is a function from @supabase/supabase-js that is used to listen for changes to the database table
      .on('postgres_changes', { event: '*', schema: '*' }, (payload: any) => {
        // on method takes in 3 arguments: the event type, the schema, and a callback function that takes in the payload
        //!Remember to make the above changes which are different from old version of supabase and what is on many videos. The above is the new way to do it. It took me a lot of time to read through the documentation to figure this out.
        console.log('payload: ', payload); // log the payload to the console

        // if the eventType is DELETE, UPDATE, or INSERT, then update the todos BehaviorSubject with the new todos from the database table
        if (payload.eventType === 'DELETE') {
          //take the old todos and filter out the todo that was deleted
          const oldItem: Todo = payload.old;
          const newValue = this._todos.value.filter(
            // filter out the todo that was deleted
            (item: any) => oldItem['id'] !== item.id
          );
          this._todos.next(newValue); // pass the new todos to the todos BehaviorSubject to update the todos
        } else if (payload.eventType === 'UPDATE') {
          const updatedItem: Todo = payload.new;
          const newValue = this._todos.value.map((item: any) => {
            // map over the todos and update the todo that was updated
            if (updatedItem['id'] === item.id) {
              // return updatedItem;
              item = updatedItem;
            }
            return item;
          });
          this._todos.next(newValue); // pass the new todos to the todos BehaviorSubject to update the todos
        } else if (payload.eventType === 'INSERT') {
          const newItem: Todo = payload.new;
          this._todos.next([...this._todos.value, newItem]); // pass the new todos to the todos BehaviorSubject to update the todos using the spread operator to add the new todo to the todos
        }
      })
      .subscribe();
  }
}

The above code defines the class SupabaseService that uses the Supabase client library (@supabase/supabase-js) to interact with a database table called TODO_DB.

The class has the following properties and methods:

  • _todos: A BehaviorSubject from the RxJS library that is used to store the todos. It is initialized with an empty array as the default value.
  • constructor(): Initializes supabase and sets up a listener for changes to the TODO_DB database table using the channel() method from Supabase.
  • get todos(): A getter function that returns the todos BehaviorSubject as an Observable to access the private todos BehaviorSubject from outside the service.
  • loadTodos(): An async method that selects all the todos from the database table and stores them in a variable called query. It passes the todos to the todos BehaviorSubject using the next() method.
  • addTodo(): An async method that inserts a new todo into the database table.
  • removeTodo(): An async method that deletes a todo from the database table.
  • updateTodo(): An async method that updates a todo in the database table.
  • handleTodosChanged(): A function that listens for changes to the database table and updates the todos BehaviorSubject in real-time when the database table is updated. It uses the on() method from Supabase to listen for changes to the TODO_DB database table. When a change is detected, it updates the todos BehaviorSubject accordingly.

Next, I will use the service to build the logic for the achievement list page in list.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, ToastController } from '@ionic/angular';
import { RouterLink } from '@angular/router';
import { Router } from '@angular/router';
import { LoadingController } from '@ionic/angular';
import { SupabaseService, Todo } from 'src/app/services/supabase.service';
import { AlertController } from '@ionic/angular';

@Component({
  selector: 'app-list',
  templateUrl: './list.page.html',
  styleUrls: ['./list.page.scss'],
  standalone: true,
  imports: [IonicModule, CommonModule, FormsModule, RouterLink],
})
export class ListPage implements OnInit {
  items = this.supabaseService.todos; // define items as an array of Todo type variables from src/app/services/supabase.service.ts (this is the array of todos that will be displayed in the list)

  constructor(
    private router: Router,
    private supabaseService: SupabaseService,
    private loadingController: LoadingController,
    private toastController: ToastController,
    private alertCtrl: AlertController
  ) {}

  ngOnInit() {}

  async createTodo() {
    // create a new todo item
    const alert = await this.alertCtrl.create({
      // create an alert controller
      header: 'New Achievement',
      inputs: [
        {
          name: 'task',
          placeholder: 'Mastered Ionic',
        },
      ],
      buttons: [
        // create the buttons for the alert controller
        {
          text: 'Cancel',
          role: 'cancel',
        },
        {
          text: 'Add',
          handler: (data: any) => {
            // handle the data from the alert controller
            this.supabaseService.addTodo(data.task); // add the new todo item to the database using the addTodo function from src/app/services/supabase.service.ts
            // present a toast message to show todo item added
            this.toastController
              .create({
                message: 'Achievement added',
                duration: 2000,
              })

              .then((toast) => {
                toast.present();
              });
          },
        },
      ],
    });
    await alert.present(); // present the alert controller
  }

  delete(item: Todo) {
    // delete the todo item from the database
    this.supabaseService.removeTodo(item.id); // remove the todo item from the database using the removeTodo function from src/app/services/supabase.service.ts

    // present a toast message to show todo item deleted
    this.toastController
      .create({
        message: 'Achievement deleted',
        duration: 2000,
      })
      .then((toast) => {
        toast.present();
      });
  }

  toggleDone(item: Todo) {
    // toggle the is_complete value of the todo item
    this.supabaseService.updateTodo(item.id, !item.is_complete); // update the todo item in the database using the updateTodo function from src/app/services/supabase.service.ts
  }

  async supabaseLogout() {
    //logout the user from supabase by creating a loading controller
    const loading = await this.loadingController.create({
      message: 'Logging out...',
      spinner: 'crescent',
      duration: 2000,
      showBackdrop: true,
    });
    await loading.present();

    //logout the user from supabase
    this.supabaseService.signOut();

    //dismiss the loading controller
    await loading.dismiss();
    this.router.navigateByUrl('/tabs/list-login', { replaceUrl: true });
    //present a toast message to show logout success
    const toast = await this.toastController.create({
      message: 'You have been logged out',
      duration: 2000,
      position: 'bottom',
    });
    toast.present();
  }
}

The above code defines a ListPage class that displays a list of to-do items and allows the user to create, delete, and toggle the completion status of to-do items. In the constructor, several services are injected, including the SupabaseService, which provides functions to interact with the Supabase database to add, remove, and update to-do items.

The createTodo function creates a new to-do item by presenting an alert controller that allows the user to enter a task name. If the user selects the "Add" button, the task is added to the database using the addTodo function from SupabaseService. A toast message is displayed to confirm that the to-do item has been added.

The delete the function removes a to-do item from the database using the removeTodo function from SupabaseService and displays a toast message to confirm that the to-do item has been deleted.

The toggleDone function toggles the completion status of a to-do item in the database using the updateTodo function from SupabaseService.

The supabaseLogout function logs the user out of the Supabase database by presenting a loading controller and then calling the signOut function from SupabaseService. After the user is logged out, a toast message is displayed to confirm that the user has been logged out. The user is then redirected to the login page.

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

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>Achievements</ion-title>
    <!-- <ion-buttons slot="start">
      <ion-back-button defaultHref="/tabs/tab1"></ion-back-button>
    </ion-buttons> -->
    <ion-buttons slot="start">
      <ion-button
        (click)="supabaseLogout()"
        expand="block"
        fill="clear"
        shape="round"
        color="primary"
      >
        Logout
      </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 Achievements</ion-title>
    </ion-toolbar>
  </ion-header>

  <ion-list lines="full">
    <ion-item-sliding *ngFor="let item of items | async">
      <!-- We used async pipe above because it was an observable -->
      <ion-item>
        <ion-label
          >{{item.task}}
          <p>{{ item.inserted_at | date:'short' }}</p>
        </ion-label>
        <ion-icon
          name="checkbox-outline"
          slot="end"
          color="success"
          *ngIf="item.is_complete"
        ></ion-icon>
      </ion-item>

      <ion-item-options side="end">
        <ion-item-option (click)="delete(item)" color="danger">
          <ion-icon name="trash" slot="icon-only"></ion-icon>
        </ion-item-option>
      </ion-item-options>

      <ion-item-options side="start">
        <ion-item-option
          (click)="toggleDone(item)"
          [color]="item.is_complete ? 'warning' : 'success'"
        >
          <ion-icon
            [name]="item.is_complete ? 'close' : 'checkmark'"
            slot="icon-only"
          ></ion-icon>
        </ion-item-option>
      </ion-item-options>
    </ion-item-sliding>
  </ion-list>

  <ion-fab vertical="bottom" horizontal="end" slot="fixed">
    <ion-fab-button (click)="createTodo()">
      <ion-icon name="add"></ion-icon>
    </ion-fab-button>
  </ion-fab>
</ion-content>

The above code defines an ion-header component at the top of the page with a title "Achievements", a Logout button on the left and a menu button on the right.

The ion-content component fills the remaining space and contains an ion-list component that uses *ngFor directive to iterate over an array of items (using async pipe because it's an observable) and display each item in an ion-item-sliding component that contains an ion-item component and two ion-item-options components on the left and right.

The ion-item-options on the right contains a button with a trash icon that deletes the corresponding item from the list. The ion-item-options on the left contains a button with a checkmark or close icon that toggles the is_complete property of the corresponding item.

Finally, an ion-fab button is displayed at the bottom right corner of the screen that, when clicked, creates a new item. This feature is demonstrated in figures 81, 82 & 83 below:

Figure 81: Add List Item


Figure 82: List Item Added


Figure 83: Slide and Delete


Conclusion

In this twentieth installment of our series on building a multiplatform application with Angular-15 and Ionic-7, we explored the implementation of real-time CRUD operations using the Supabase PostgreSQL database. By leveraging the power of Supabase and Angular-15 Ionic-7, we created a dynamic and responsive data manipulation experience that allows users to seamlessly create, read, update, and delete data within our app.

Real-time data manipulation is a critical aspect of modern applications, enabling users to interact with data in real time and see immediate changes. By incorporating Supabase into our Angular-15 Ionic-7 app, we harnessed its capabilities to handle real-time synchronization and data manipulation seamlessly.

Throughout this tutorial, we guided you through the process of setting up the necessary dependencies and configuring your app to connect with the Supabase PostgreSQL database. We explored the fundamental CRUD operations and demonstrated how to implement them in real-time using Supabase's powerful functionality.

By following best practices for real-time data management, error handling, and synchronization, we created a dynamic and responsive data manipulation experience for our users. Changes made by one user are instantly reflected in real-time for all connected users, enhancing collaboration and interactivity within our app.

We hope this tutorial has provided you with valuable insights and practical knowledge on performing real-time CRUD operations using the Supabase PostgreSQL database in your Angular-15 Ionic-7 app. By embracing the power of real-time data manipulation, you can create a seamless and interactive user experience, ensuring that your app remains responsive and up-to-date.

Thank you for joining us on this journey as we explored the fascinating world of app development with Angular-15, Ionic-7, and Supabase. Stay tuned for future installments where we will continue to explore new features and functionalities to make our app even more dynamic, powerful, 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