Building a real-time Trello board with Supabase and Angular (2023)

Building a real-time Trello board with Supabase and Angular (1)

With a platform like , anyone can quickly code a small hello world exampleSuperbase- but how about onereal worldbigger project?

That's exactly what you'll discover in this article:

We are building a Trello board using Supabase, Angular and Tailwind!

Building a real-time Trello board with Supabase and Angular (2)

During our trip:

  • write somethingadvanced sqlto create our tables
  • implementMagic Link Loginand user authentication with Angular
  • To usereal-time skills!

Watch the video version of the tutorial.

Since we need some code snippets, I've gathered them togetherFull source code on GithubThen you can easily do the project yourself!

If you are not familiarTrello, it's a way to manage projects with different boards, lists and cards!

Ready for a wild adventure? So let's start with our Supabase account!

Build the Supabase project

First we need a new Supabase project. If you don't have a Supabase account yet, you canstart for free!

Click "New Project" on your dashboard and leave the default settings, but be sure to save a copy of your database password!

The only thing we will manually change for now is to disable the email confirmation step. This allows users to sign up directly when using the magic link, so go for it.authenticationSelect your project tabIdeasand scroll down to yourauthentication providerwhere it can be disabled.

Building a real-time Trello board with Supabase and Angular (3)

Everything else around authentication[] is handled by Supabase and we don't need to worry about that for now.

Define your tables with SQL

Since Supabase uses Postgres under the hood, we need to write some SQL to define our tables.

Let's start with something simple, namely the general definition of our tables:

  • planks: Follow boards created by users
  • lisa: Lists within a frame
  • cards: Cards with tasks in a list
  • sser: A table to keep track of all registered users
  • user_boards- A many-to-many table to keep track of the boards a user belongs to

We won't go into the SQL details, but you can paste the following snippets intoEditor-SQLyour project

Building a real-time Trello board with Supabase and Angular (4)

Just navigate to the menu item and click on it+ New query, cole or SQL and pressureRUNwhich I hope works without any problems:

drops mesaEexistuser_boards;drops mesaEexistCards;drops mesaEexistlisa;drops mesaEexistboards;drops mesaEexistFrom the user;-- Create board tableto create mesaPlanks (I WENTstartinggeneratedvon Standard with identity primaryI like,uuid creatorreferencesauthentication userNO Null Standardauthentication.uid(),title textStandard 'Nameless Council',created intime stamp swindler TempoZoneStandardtime zone('COORDINATED UNIVERSAL TIME'::text, now())NO Null);-- Create list tableto create mesaTo hear (I WENTstartinggeneratedvon Standard with identity primaryI like,board_idstarting referencesplanksEM EXTINGUISHWATERFALLNO Null,title textStandard '',Positione T NO Null Standard 0,created intime stamp swindler TempoZoneStandardtime zone('COORDINATED UNIVERSAL TIME'::text, now())NO Null);- Create map tableto create mesaCards (I WENTstartinggeneratedvon Standard with identity primaryI like,list_idstarting referenceslisaEM EXTINGUISHWATERFALLNO Null,board_idstarting referencesplanksEM EXTINGUISHWATERFALLNO Null,Positione T NO Null Standard 0,title textStandard '',descriptive textcheck(character length(Description)> 0),assigned_uuidreferencesauthentication user,doneboolesch Standard INCORRECT,created intime stamp swindler TempoZoneStandardtime zone('COORDINATED UNIVERSAL TIME'::text, now())NO Null);-- Many-to-many table for list of users <-> framesto create mesauser_boards (I WENTstartinggeneratedvon Standard with identity primaryI like,user_id uuidreferencesauthentication userEM EXTINGUISHWATERFALLNO Null Standardauthentication.uid(),board_idstarting referencesplanksEM EXTINGUISHWATERFALL);-- User ID lookup tableto create mesaof the user (UUID-IDNO Null primaryI like,Email-Text);-- Make sure deleted records are included in real timeto change mesamap replicasidentity complete;to change mesaThe replica listidentity complete;-- Function to retrieve all user cardsto create Ösubstitutefunctionget_boards_for_authenticated_user()Returnssentence ofstartingLanguage sqlsecurity definitionlowersearch path=publicstablewith$$ to chooseboard_id vonuser_boards WoBenutzer ID=authentication.uid()$$;

In addition to table creation, we also change the replica identity, which helps to change the records retrieved when a row is deleted.

Finally, we define a very importantfunctionwith which we protect the tableRow level security.

This function retrieves all frames of a user from theuser_boardsTable and is now used in our policies.

Now let's enable row-level security for the various tables and define someguidelinesSo only users with the correct access can read/update/delete rows.

Go ahead and now run another SQL query in notepad:

- Board line-level securityto change mesaallow signsfilaSecurity level;-- Guidelinesto create"Users can create folders" policyEmplanksfor insertion Aauthenticatedswindler CHECK(TRUE);to createPolicy "Users can see their dashboards"Emplanksfor to choose to use(I WENTEm( to chooseget_boards_for_authenticated_user()) );to createPolicy "Users can update their dashboards"EmplanksforTo updateto use(I WENTEm( to chooseget_boards_for_authenticated_user()) );to createPolicy "Users can delete their created dashboards"Emplanksfor extinguish to use(auth.uid()=The creator);-- user_boards row-level securityto change mesaEnable user boardsfilaSecurity level;to createPolicy "Users can add their boards"Emuser_boardsfor insertion Aauthenticatedswindler check(TRUE);to create"Users can view dashboards" policyEmuser_boardsfor to choose to use(auth.uid()=Benutzer ID);to createPolicy "Users can delete their dashboards"Emuser_boardsfor extinguish to use(auth.uid()=Benutzer ID);-- Enumerate row-level securityto change mesaenable listsfilaSecurity level;-- Guidelinesto createPolicy "Users can edit lists if they are part of the board"Emlisafor no to use(board_idEm( to chooseget_boards_for_authenticated_user()) );- Security at the card line levelto change mesaactivated cardsfilaSecurity level;-- Guidelinesto createPolicy "Users can edit cards if they are part of the board"Emcardsfor no to use(board_idEm( to chooseget_boards_for_authenticated_user()) );

Finally we need aDeductionwhich responds to changes in our database.

In our case, we want to listen for the creation of new frames, which will automatically create the frame user connection < - > in theuser_boardsMesa.

In addition, we will also add each new authenticated user to ourfrom the usertable because then you won't have access to Supabase's internal authentication table.

So run one final query:

-- add a line in user_boardsto create functionpúblico.handle_board_added()Returns DeductionLanguageplpgsqlsecurity definitionwith$$To start insertion Empublic.user_boards (board_id, user_id) values(, auth.uid()); give back neu;fin;$$;-- enable the feature every time a frame is createdto create Deductionon_board_createdafterinsertion Emplanks for all fila carry out Procedurespublic.handle_board_added();to create Ösubstitutefunctionpublic.handle_new_user()Returns Deduction with$$To start insertion Empublic.users (ID, E-Mail) values(,; give back neu;fin;$$Languageplpgsql security definition;to create Deductionon_auth_user_createdafterinsertion Emauthentication user for all fila carry out Procedurespublic.handle_new_user();

At this point, our Supabase project is set up correctly and we can move on to the real application!

Creating the Angular project

We are not tied to any framework, but in this article we are going to use itSquareto build a robust web application.

start usingCLI angularto create a new project and then add some components and services we need.

Finally we can install itSupabase JS packageand two additional helper packages for some nice features, so go ahead and run:

(Video) Building a Realtime Trello Board with Supabase and Angular

novo trelloBoard --routing --style=scssCD./trelloTablero# Generate components and servicesng Generate components from components/Login Generate components from components/within/Workspaceng Generate components from components/within/Placang Generate services from services/Authentication Generate services from services/Data# Install Supabase and additional packagesnpm install @supabase/supabase-jsnpm install ngx-spinner ngx-gravatar

To import installed packages, we can quickly change oursrc/app/app.module.tsA:

Object{ module of }von '@eckig/núcleo'Object{ Browsermodul }von '@angular/browser platform'Object{ application routing module }von './app-routing.modulo'Object{ application component }von './aplicativo.componente'Object{ login component }von './components/login/login.component'Object{ dashboard component }von './componentes/interior/placa/placa.componente'Object{ Workspace Components }von './components/interior/workspace/workspace.component'Object{ Browser-Animationsmodul }von '@angular/browser platform/animations'Object{NgxSpinnerModule}von 'ngx-spinner'Object{ Forms-Modul }von '@angular/shapes'Object{ Gravatar-Modul }von 'ngx-gravatar'@NgModule({Declarations: [AppComponent, LoginComponent, BoardComponent, WorkspaceComponent], Imports: [FormsModule, BrowserModule, AppRoutingModule, BrowserAnimationsModule, NgxSpinnerModule, GravatarModule, ], Approved: [], Bootstrap: [AppComponent],})Export Classroom application module{}

On top of that hengx-spinnerneed another entry inangle.jsonto copy the resources so we can easily show a load meter later, so open it up and change thatstylematrix for:

"style": [ "src/estilos.scss", "node_modules/ngx-spinner/animations/ball-scale-multiple.css"],

As we have already generated some components, we can also change the routing of our application to accommodate the new pages in thesrc/app/app-routing.módulo.tsNow:

importing { BoardComponent } from './components/inside/board/board.component'import { WorkspaceComponent } de './components/inside/workspace/workspace.component'import { LoginComponent } de './components/login/login.component'import { NgModule } de '@angular/core'import { RouterModule, Routes } from '@angular/router'const routes: Routes = [ { route: '', component: LoginComponent, }, { route: 'workspace', component: WorkspaceComponent, }, { route: 'workspace/:id', component : BoardComponent, }, { route: '**', redirectTo: '/', },]@NgModule({ imports: [RouterModule.forRoot(routes, {})], exports: [RouterModule],}) exportação de classe Anwendungs-Routing-Modul {}

Our app starts with the login screen, then we can move to the workspace with our boards, and finally we can dive into a specific board to see all of its lists and cards.

To properly use the Angular router, we can now update thesrc/app/app.component.htmlSo you only have one line:

<output router></exit-router>

Finally, the most important configuration step: add our Supabase credentialssrc/environments/environment.tslike this:

ExportConstantlyenvironment = {Production:INCORRECT,superbaseUrl:'TU-URL',superbaseKey:'TU-ANON-CLAVE',}

You can find these values ​​in your Supabase project by clicking on the buttonIdeasand then navigate toAPIwhere does your show come from?Project API key.

Building a real-time Trello board with Supabase and Angular (5)

ANDThenThe key is safe to use in a front-end project as we have RLS enabled in our database anyway.

Tailwind to style

We could build an ugly project or just make it awesome by installing it.CSS from the tail- We chose the latter in this article!

No doubt there are other style libraries you can use, so this step is completely optional but necessary for the code in this tutorial to work.

That's why we follow youangle guideand install Tailwind as follows:

npm install -D tailwindcss postcss autoprefixer @tailwindcss/formulariosnpx tailwindcss inicio

Now we need to update ours tootailwind.config.jsfurthermore:

/**@Typ{import('tailwindcss').config} */Module.export = { Content: ['./src/**/*.{html,ts}'], he: { renovation: {},}, added: [demand('@tailwindcss/formularios')],}

Finally, we include styling in oursrc/estilos.scss:

@tailwindBase;@tailwindcomponents;@tailwindPublic utility services;

And with that, all project setup is done and we can focus 100% on the functionality of our Trello clone!

Create Magic Link Login

Now we could add all types of authentication using authentication providers provided by Supabase but we only use magic link login where users just need to enter their email.

To start, let's implement a simple authentication service that tracks our current user with abehavior problemThat way we can easily generate new values ​​later when the user session changes.

We also load the session "by hand" oncegetUser()doonAuthStateChangeThe event is usually not broadcast when the page is loaded and we want to load a saved session in this case as well.

To send an email to the user, simply callrecord()and just send an email - Supabase will do the rest for us!

So start changing thosesrc/app/services/auth.service.tsabout it now:

Object{Injectable}von '@eckig/núcleo'Object{Router}von '@angular/router'Object{createCustomer, CustomerSupabase, User}von '@supabase/supabase-js'Object{behavior problem}von 'rxjs'Object{ Surroundings }von 'source/environments/environment'@ Injectable({ Supplied in:'Source',})Export Classroom authentication service { Privatesuprabase: SuperbaseClient Private_currentUser: SubjectBehavior<boolesch| User |any> =neuSubjectBehaviour(Null) constructor(PrivateRouter: Router){ That's it.supabase = createClient(ambient.supabaseUrl, ambient.supabaseKey) // Manually load the user session once on page load // Note: This will become a promise in the next release with getUser()! Constantlyuser =That's it.supabase.auth.user() . E(from user) { That's .}the rest{ That's} That's it.supabase.auth.onAuthStateChange((event, session) =>{ E(Event =='REGISTERED') { That's!.user)}the rest{ That's That's it.router.navigateByUrl('/', {replace url:TRUE})} }) } Login with email(E-mail:Chain){ // Note: This will become signInWithOTP() in the next release! give back That's it.supabase.auth.signIn({E-mail, }) } cancel registration(){ That's it.supabase.auth.signOut()} to receive current user() { give back That's it._currentUser.asObservable()}}
(Video) Realtime Trello Board with Angular by Simon Grimm

This is a solid starting point for our authentication logic, and now we just need to use these features on our login page.

In addition, we will also be looking out for user changes here, as this is the page that the user loads when they click on the magic link. We can use thosecurrent userfrom our service, so we don't need any additional logic for this.

After starting the login, we can also use our cool spinner package to show a small prompt and change the value oflink successso that we can present a small text in our user interface.

We keep it real simple so let's change it upsrc/app/components/login/login.component.tsA:

Object{Router}von '@angular/router'Object{ authentication service }von './../../services/auth.servicio'Object{Component, OnInit}von '@eckig/núcleo'Object{NgxSpinner Service}von 'ngx-spinner'@Components({ voters:'App-Login', template url:'./login.component.html', style url: ['./login.component.scss'],})Export Classroom login component implemented OnInit {Email =''linkSucesso =INCORRECT constructor( PrivateAuthentication: authentication service, Privatehilandero: NgxSpinnerService, PrivateRouter: Router ){ That's it.auth.currentuser.subscribe((from the user) =>{ E(from user) { That's it.router.navigateByUrl('/Workspace', {replace url:TRUE})} }) }conOnInit():file{} asynchronous record(){ That's Constantlyresult =hope That's it.auth.signInWithEmail(That's it.E-mail) That's it.spinner.hide() E(!Result.Error) { That's it.linkSucesso =TRUE}the rest{alert(result.error.message) } }}

The final piece is now our UI, and since we're using Tailwind, the HTML snippets don't look very pretty.

However, it's just a bit of CSS and connecting our fields and buttons with the correct functionality, so go ahead and change them.src/app/components/login/login.component.htmlA:

<ngx-Spinnertyp="ball-scale multiple"></ngx-spinner><divClassroom="to bend Minimum-H-complete to bend-To divide justify-center py-12 SM:pixel-6 LG:pixel-8"><division Classroom="SM:mx-Auto SM:c-complete SM:maximum-c-Maryland"><h2 Classroom="monte-6 Text-center Text-3xl source-extra fat Text-grau-900">Superbase Trello</h2></division><division Classroom="monte-8 SM:mx-Auto SM:c-complete SM:maximum-c-Maryland"><division Classroom="bg-branco py-8 pixel-4 Sombra SM:rounded-lg SM:pixel-10"><division Classroom="Space-j-6"*GIF="!link success;the rest check_mail"><division Classroom="Space-j-6"><label for="E-mail"Classroom="Block Text-SM source-half Text-grau-700">E-mail ADDRESS</label><division Classroom="monte-1"><Prohibited Type="E-mail"[(Model of)]="E-mail" autocompletar="E-mail" placeholder="" Classroom="Block c-complete rounded-Maryland he must he must-grau-300 pixel-3 py-2 placeholder-grau-400 Sombra-SM Focus:he must-Esmeralda-500 Focus:to describe-none Focus:Ring-Esmeralda-500 SM:Text-SM"/></division></division><division><I like(clique)="record()" Classroom="to bend c-complete justify-center rounded-Maryland he must he must-transparent bg-Esmeralda-600 py-2 pixel-4 Text-SM source-half Text-branco Sombra-SM to float:bg-Esmeralda-700 Focus:to describe-none Focus:Ring-2 Focus:Ring-Esmeralda-500 Focus:Ring-compensate-2"> From you Magic shortcut</I like></division></division><von-Presentation#check_mail>Please check They are E-mails! </von-Presentation></division></division></division>

Once you're done, you should have a neat login page!

Building a real-time Trello board with Supabase and Angular (6)

If you enter your email address and click the button, you should automatically receive an email with a link that will reopen your app in your browser and this time take you straight to your desktop.

Building a real-time Trello board with Supabase and Angular (7)

Well, at this point we could also insert thistraineeSo let's add a mechanism to prevent the page from manually changing the URL without authorization.

Protect your pages with a protector

At Angular we protect pages with guard and since we already track the user in our authentication service it will be very easy to protect pages that only authorized users should see.

Start generating a new guard:

ng generate guards/author --implement CanActivate

This gatekeeper now checks our service's observable, filters the initial state and then sees whether a user can access a page or not.

bring the newsrc/app/guards/auth.guard.tsand change like this:

Object{ authentication service }von './../servicios/autorización.servicio'Object{Injectable}von '@eckig/núcleo'Object{ CanActivate, Router, UrlTree }von '@angular/router'Object{observable}von 'rxjs'Object{filter, map, take}von 'rxjs/operators'@ Injectable({ Supplied in:'Source',})Export Classroom AuthGuardName implemented canActivate { constructor(PrivateAuthentication: authentication service,PrivateRouter: Router){}canActivate(): Observable<boolesch| URL-Baum > { give back That's it.auth.currentuser.pipe(Filter((bravura) =>Bravura !==Null),// Filter the initial behavior subject valueto load(1),// Otherwise, the observable will not complete!Map((is authenticated) =>{ E(is authenticated) { give back TRUE}the rest{ give back That's it.router.createUrlTree(['/'])} }) ) }}

Now we can apply this protection to all the routes we want to protect, so open oursrc/app/app-routing.módulo.tsand add it to the two interiors we want to protect:

import {AuthGuard} from './guards/auth.guard'import { BoardComponent } de './components/inside/board/board.component'import { WorkspaceComponent } de './components/inside/workspace/workspace.component'import { LoginComponent } de './components/login/login. component' import { NgModule } from '@angular/core' import { RouterModule, Routes } from '@angular/router' const routes: Routes = [ { route: '', component: LoginComponent, }, { route: 'space work ' , Componente: WorkspaceComponent, canActivate: [AuthGuard], }, { Pfad: 'workspace/:id', Componente: BoardComponent, canActivate: [AuthGuard], }, { Pfad: '**', forwardTo: '/' , } ,]@NgModule({ imports: [RouterModule.forRoot(routes, {})], exports: [RouterModule],}) export class AppRoutingModule {}

Now only registered users can access these pages and we can take a step closer to forum logic.

Create the workspace

Once a user lands on the workspace page, we want to list all of a user's dashboards and implement the ability to add dashboards.

For this, we start again with a service that handles all the interaction between our code and Supabase, allowing the view to focus on the presentation of data.

Our first function simply inserts an empty object into theplankstable, which we define as a constant so we can't add typos to our code.

Since we set a default value for newlines in our SQL at the start, we don't need to pass any more data here.

To load all tables, a user can simply query theuser_boardstable, but we may want more information about the related table so we can do that as wellquery foreign tablesto load the panel information!

Go ahead and start thesrc/app/services/data.service.tstherefore:

Object{Injectable}von '@eckig/núcleo'Object{SupabaseClient, createClient}von '@supabase/supabase-js'Object{ Surroundings }von 'source/environments/environment'Export ConstantlyBOARDS_TABLE ='boards'Export ConstantlyUSER_BOARDS_TABLE ='boards_user'Export ConstantlyTABLE_LISTS ='liza'Export ConstantlyCARDS_TABLE ='Cards'Export ConstantlyUSERS_TABLE ='From user'@ Injectable({ Supplied in:'Source',})Export Classroom data service { Privatesuprabase: SuperbaseClient constructor(){ That's it.supabase = createClient(ambient.supabaseUrl, ambient.supabaseKey)} asynchronous starting table(){ // The Min Return will be the default value in the next version and can be removed here! give back hope That's it.supabase.from(BOARDS_TABLE).insert({}, {To go back:'Minimum'})} asynchronous getBoards(){ Constantlyplates =hope That's it.supabase.from(USER_BOARDS_TABLE).select(`Boards:board_id(title, ID)`) give backPlates.Data || []}}

This is actually enough for our first interaction with our Supabase tables so that we can go back to our view and load the user dashboards when the page loads.

Also we want to add a panel and here we find one of themreal world problems:

because we have adatabase triggerwhich adds an entry when adding a new table, the user is not immediately authorized to access the new table row! Only after the trigger ends can the RLS verify user boards confirm that this user is part of the board.

(Video) Angular Supabase Realtime Database Connection Tutorial

So let's add another line to reload the frames and call the last added item so we can automatically navigate to its details page.

Now open thesrc/app/components/inside/workspace/workspace.component.tsand change to:

Object{ authentication service }von './../../../services/auth.service'Object{Router}von '@angular/router'Object{ data service }von './../../../services/data.service'Object{Component, OnInit}von '@eckig/núcleo'@Components({ voters:'application workspace', template url:'./workspace.component.html', style url: ['./workspace.component.scss'],})Export Classroom Workspace Components implemented OnInit { planks:any[] = []user =That's it.auth.currentuser constructor( Privatedata service: data service, PrivateRouter: Router, PrivateAuthentication: authentication service ){} asynchronous com OnInit(){ That's it.boards =hope That's it.dataservice.getBoards()} asynchronous starting table(){ Constantlydata =hope That's it.dataservice.startBoard() // Loads all panels as we only get minimal data // Trigger must be executed first // Otherwise RLS would fail That's it.boards =hope That's it.dataservice.getBoards() E(That's it.length table >0) { Constantlynew board =That's it.boards.pop() E(newBoard.boards) { That's it.router.navigateByUrl(`/desktop/${}`)} } } separate(){ That's it.auth.logout()}}

To display all of this, we create another visualization using Tailwind and also use the Gravatar package to display a small image of the current user based on email.

Also, we just looped through all the boards, added the router link to a board based on the ID, and added a button to create new boards, then open thosesrc/app/components/inside/workspace/workspace.component.htmland change to:

< headerClassroom="bg-Esmeralda-600"><Navigation Classroom="mx-Auto maximum-c-7xl pixel-4 SM:pixel-6 LG:pixel-8"><division Classroom="to bend c-complete elements-center justify-in between he must-B he must-Esmeralda-500 py-6 LG:he must-none"><division Classroom="to bend elements-center"><A routerLink="/work space"><Photo Classroom="H-6 c-Auto"Origin=""Alternative="" /></A></division><division Classroom="ml-10 to bend elements-center Space-x-4"><teams Classroom="Text-branco">{{ (user | async)?.email }}</span><img ngxGravatar [E-Mail]="(user | async)?.email"/><button(clique)="separate()" Classroom="on-line-Block rounded-Maryland he must he must-transparent bg-branco py-1 pixel-4 Text-Base source-half Text-Esmeralda-600 to float:bg-Esmeralda-50"> cancel registration</I like></division></division></Navigation></Header><director Classroom="mx-Auto maximum-c-7xl pixel-4 py-8 SM:pixel-6 LG:pixel-8"><ul role="List" Classroom="Rot Rot-columns-2 gap-x-4 gap-j-8 SM:Rot-columns-3 SM:gap-x-6 LG:Rot-columns-4 SG:gap-x-8"><li*Abdomen="to leave Junta von planks"[routerLink]="Junta.planks.I WENT" Classroom="relative H-52 rounded bg-Esmeralda-200 py-4 pixel-4 Text-lg source-half denied to float:cursor-pointer to float:bg-Esmeralda-300"> {{ board.boards.title }} </li> <li(clique)="starting plate()" Classroom="relative H-52 rounded bg-Esmeralda-500 py-4 pixel-4 Text-lg source-half denied to float:cursor-pointer">+Neu Junta</li></ul></director>

At this point, we have the board logic working and, in fact, we are already routed to the next page of details.

Building a real-time Trello board with Supabase and Angular (8)

The logout function also ends our session and takes us back to login, so we've already covered that flow at the same time.

It's time for more interaction with our Supabase tables!

Adding CRUD functionality to the database

On the details page of our dashboard, we now need to interact with all the tables and mainly perform CRUD functions: create, read, update or delete records from our database.

Since there's no real value in discussing every line, let's quickly add the following group of functions to oursrc/app/services/data.service.ts:

// CRUD board asynchronous getBoardInfo(tableroId:Chain){ give back hope That's it.supabase.from(BOARDS_TABLE).to choose('*').Phosphor({I WENT: tableroId }).einzel(); } asynchronous UpdateBoard(Junta:any){ give back hope That's it.supabase.from(BOARDS_TABLE) .update(tablero).Phosphor({I WENT: });} asynchronous drunk tray(Junta:any){ give back hope That's it.supabase.from(BOARDS_TABLE) .delete().Phosphor({I WENT: });} // CRUD-Listen asynchronous obtenerBoardLists(tableroId:Chain){ Constantlyhear =hope That's choose('*').eq('board_id', Board-ID).Command('Position'); give || [];} asynchronous addBoardList(tableroId:Chain, place =0){ give back hope That's{board_id: BoardId, Position,title:'new list'}).to choose('*').einzel(); } asynchronous Updated Advice List(List:any){ give back hope That's it.supabase.from(LISTS_TABLE) .update(lista).Phosphor({I WENT: });} asynchronous eliminarBoardList(List:any){ give back hope That's it.supabase.from(LISTS_TABLE) .delete().Phosphor({I WENT: });} // CRUD cards asynchronous agregarListCard(List ID:Chain, Board-ID:Chain, place =0){ give back hope That's it.supabase.from(CARDS_TABLE) .insert({board_id: tableroId,list_id: listen ID, position }).to choose('*').einzel(); } asynchronous getListCards(List ID:Chain){ Constantlyhear =hope That's it.supabase.from(CARDS_TABLE).to choose('*').eq('list_id', listen ID).Command('Position'); give || [];} asynchronous Upgrade-Karte(Map:any){ give back hope That's it.supabase.from(CARDS_TABLE) .update(tarjeta).Phosphor({I WENT: I WENT });} asynchronous delete card(Map:any){ give back hope That's it.supabase.from(CARDS_TABLE) .delete().Phosphor({I WENT: I WENT });}

Most, if not all, is basic SQL as described inSupabase documents for the database

One additional feature is missing, which is simple invite logic. However, let's skip the "Okay, I want to join this board" step and just add guest users to a new board. Sometimes it is necessary to force users to do what is good for them.

So let's try to find a user's userid based on the entered email address and if it exists we will create a new entry inuser_boardsTable for this user:

 // Invite others asynchronous addUserToBoard(tableroId:Chain, E-mail:Chain){ Constantlyuser =hope That's it.supabase.von(USERS_TABLE).to choose('I WENT').match({E-Mail}) .single(); E( { ConstantlyuserID =; ConstantlyIU =hope That's it.supabase.von(USER_BOARDS_TABLE).insertar({user_id: user_id, board_id: board_id, }); give backuser board;}the rest{ give back Null;} }

With these resources I think we are more than ready to create a powerful dashboard page.

Creating the dashboard view

This page is the most important and challenging part of our app, as it's where the real work happens and users collaborate on dashboards.

However, let's start with setting up the basics and introduce them toReal-time functionality and presencein a separate step after that.

As it would be tedious to split the page into several code snippets, let's limit ourselves to one big snippet and explain what happens:

  • First, we need to load the frame's general information, such as the titlegetBoardInfo()and identification approved by the council
  • So we have to load everythinglisafrom a plate withgetBoardLists()
  • So we have to load each oneMapfor each list withgetListCards()

To keep track of data and changes, we save all maps onlistCartasObject that stores all cards in the associated list ID key.

Regarding additional logic, we might want to update or remove the card, which we can easily do with the service functions created above.

The same applies to lists and cards that can be added, updated or deleted.

However, this does not (yet) update our local data as we want to implement this later with real-time updates.

For now, go ahead and change thosesrc/app/components/inside/board/board.component.tsA:

Object{ data service }von './../../../services/data.service'Object{ Component, HostListener, OnInit }von '@eckig/núcleo'Object{ route enabled, router }von '@angular/router'@Components({ voters:'Application Dashboard', template url:'./placa.componente.html', style url: ['./placa.componente.scss'],})Export Classroom BoardComponent implemented OnInit { lisa:any[] = [] tableroId:Chain|Null=Null edit title:any= {} editCard:any= {} Panel Information:any=Nullchanged title =INCORRECT listCartas:any= {}addUserEmail ='' constructor( PrivateRoute: activated route, Privatedata service: data service, PrivateRouter: Router ){} asynchronous com OnInit(){ That's it.boardId =That's it.route.snapshot.paramMap.get( .'I WENT') E(That's it.boardId) { // Load general panel information ConstantlyBrett =hope That's service. getBoardInfo(That's it.boardId) That's it.boardInfo = // Get all lists That's it.listen =hope That's service. getBoardLists(That's it.boardId) // Paste cards for each list for(to leaveListvon That's it.To hear) { That's it.listCards[] =hope That's it.service data.getListCards(} // For later... That's it.handleRealtimeUpdates()} } // // BOARD-Logik // asynchronous salvarBoardTitle(){ hope That's it.Datenienst. Upgrade board (That's it.boardInfo) That's it.titleAltered =INCORRECT} asynchronous drunk tray(){ hope That's it.dataservice.deleteBoard(That's it.boardInfo) That's it.router.navigateByUrl('/Workspace')} // // LIST-Logik // asynchronous add list(){ ConstantlyneueListe =hope That's it.dataservice.addBoardList(That's it.boardId!,That's it.List.Long)} edition title(List:any, edit =INCORRECT){ That's it.editTitle[] = edit} asynchronous updateListTitle(List:any){ hope That's it.dataService.updateBoardList(Liste) That's it.editingTitle(Lista,INCORRECT)} asynchronous eliminarBoardList(List:any){ hope That's it.dataService.deleteBoardList(Liste)} // // CARDS Logic // asynchronous add card(List:any){ hope That's it.dataService.addListCard(,That's it.boardId!,That's it.listCards[List.ID].Long)} editingCard(Map:any, edit =INCORRECT){ That's it.editCard[] = editar} asynchronous Upgrade-Karte(Map:any){ hope That's it.dataService.updateCard(tarjeta) That's it.editingCard(Karte,INCORRECT)} asynchronous delete card(Map:any){ hope That's it.dataService.deleteCard(tarjeta)} // to invite asynchronous add user(){ hope That's it.dataService.addUserToBoard(That's it.boardId!,That's it.addUserEmail) That's it.addUserEmail =''} handle real-time updates(){ // NO}}

It was a huge file - take the time to go through it at least once or twice to better understand the various features we've added.

Now we have to deal with the view of this page, and since it's a tailwind, the snippets don't get any shorter.

We can start with the simplest part, which is the header area, which shows a back button, dashboard information that can be updated when clicked, and a delete button, well, you know what.

bringsrc/app/components/inside/board/board.component.htmland add this first:

< headerClassroom="bg-Esmeralda-600"><Navigation Classroom="mx-Auto maximum-c-7xl pixel-4 SM:pixel-6 LG:pixel-8"><division Classroom="to bend c-complete elements-center justify-in between he must-B he must-Esmeralda-500 py-6 LG:he must-none"><division Classroom="to bend elements-center"><A routerLink="/work space"Classroom="source-half denied Text-Esmeralda-900"> <Turn back</A></division><division Classroom="to bend gap-4"><Prohibited*GIF="Panel Information"(ngModelChange)="TitleAltered=TRUE" Classroom="ml-10 Space-x-4 bg-Esmeralda-600 source-clearly Text-branco"[(Model of)]="Panel Information.title"/><I like Classroom="source-half"*GIF="TitleAltered" (clique)="salvarBoardTitle()">save not computer</I like></division><division Classroom="to bend"><I like Classroom="Text-some source-half Text-red-700" (clique)="drunk tray()"> extinguish Junta</I like></division></division></Navigation></Header>

Since we'll have more of these update entry fields later, let's quickly add a columnHostListenerto our application so that we can at least capture the ESC key event and then close all edit input boxes in oursrc/app/components/inside/board/board.component.ts

(Video) 🛑 Angular Real World App with Supabase & Tailwind

 @HostListener('Document:Keyboard', ['$event'])onKeydownHandler(event: keyboard event) { E(event.keyCode ===27) { // close what needs to be closed! That's it.titleAltered =INCORRECT;object.keys(That's it.editCard).map((element) => { That's it.editCard[elemento] =INCORRECT; give backArticle;});object.keys(That's it.editTitle).map((element) => { That's it.editTitle[Item] =INCORRECT; give backArticle;}); } }

Finally, we need to go through all the lists and show all the cards from each list.

Simple enough task actually, but since we need more buttons to control elements so we can remove, add and update them throughout the code, it gets a bit bloated.

However, we can continue with the previous code in oursrc/app/components/inside/board/board.component.htmland add this:

<DirectorClassroom="mx-Auto maximum-c-7xl pixel-4 py-8 SM:pixel-6 LG:pixel-8"><division Classroom="Rot Rot-columns-2 gap-x-4 gap-j-8 SM:Rot-columns-3 SM:gap-x-6 LG:Rot-columns-4 SG:gap-x-8"><!--is repeated NO LIZA--><division*Abdomen="to leave List von lisa" Classroom="Minimum-H-52 relative H-Auto rounded bg-Esmeralda-200 py-4 pixel-4 Text-SM source-half denied"><division Classroom="to bend gap-2 pb-4"><Page(clique)="edition title(List,TRUE)" Classroom="to float:cursor-pointer"*GIF="!edit title[List.I WENT]"> {{ List.Title }} </p> <Input[(ngModel)]="List.Title"*ngSe="editTitle[]" Classroom="Block c-complete rounded-Maryland he must he must-grau-300 pixel-3 py-2 Sombra-SM Focus:he must-Esmeralda-500 Focus:to describe-none Focus:Ring-Esmeralda-500 SM:Text-SM"/><I like Classroom="source-half"*GIF="edit title[List.I WENT]" (clique)="updateListTitle(List)"> save not computer</I like></division><!--is repeated LIST CARDS--><division Classroom="to bend to bend-To divide elements-center gap-2"><division Classroom="to bend H-Auto c-complete to bend-To divide gap-2 to float:cursor-pointer"*Abdomen="to leave Map von listCartas[List.I WENT]"(clique)="editingCard(Map,TRUE)"><Page Classroom="H-10 bg-branco py-2 pixel-2"*GIF="!editCard[Map.I WENT]">{{ Map title }}</p><Enter[(ngModel)]="card.title"*ngSe="editCard[]" Classroom="Block rounded-Maryland he must he must-grau-300 pixel-3 py-2 Sombra-SM Focus:he must-Esmeralda-500 Focus:to describe-none Focus:Ring-Esmeralda-500 SM:Text-SM"/><division Classroom="line up-elements-center to bend justify-in between"><I like Classroom="source-half"*GIF="editCard[Map.I WENT]" (clique)="Upgrade-Karte(Map)"> To update</I like><I like Classroom="source-half Text-red-600"*GIF="editCard[Map.I WENT]"(clique)="delete card(Map)"> extinguish</I like></division></division><division(clique)="add card(List)"Classroom="To point-8 Text-grau-500 to float:cursor-pointer">+Add to A Map</division><I like Classroom="Text-some source-half Text-red-700" (clique)="eliminarBoardList(List)"> extinguish List</I like></division></division><division(clique)="add list()" Classroom="relative H-sixteen rounded bg-Esmeralda-500 py-4 pixel-4 Text-lg source-half denied to float:cursor-pointer">+Neu List</division></division></director>

At this point we can add a list, add a new card to this list and finally update or delete everything.

Building a real-time Trello board with Supabase and Angular (9)

Most of this won't update the display, as we'll cover in a minute with real-time updates. So you should refresh your page now that you've added a card or list.

But actually, we can already add our invite logic, which just needs one more input field so we can invite another email to work with us on the board.

Add the following in<Director>label oursrc/app/components/inside/board/board.component.htmlat the bottom:

<divClassroom="to bend elements-center gap-4 py-12"><teams Classroom="Block Text-3xl source-extra fat Text-grau-900">To invite</teams><Prohibited[(Model of)]="Add User Email" placeholder="" Classroom="Block rounded-Maryland he must he must-grau-300 pixel-3 py-2 Sombra-SM Focus:he must-Esmeralda-500 Focus:to describe-none Focus:Ring-Esmeralda-500 SM:Text-SM"/><I like(clique)="add user()" Classroom="on-line-to bend elements-center rounded he must he must-transparent bg-Esmeralda-600 pixel-2.5 py-1.5 Text-xs source-half Text-branco Sombra-SM to float:bg-Esmeralda-700 Focus:to describe-none Focus:Ring-2 Focus:Ring-Esmeralda-500 Focus:Ring-compensate-2"> To invite</I like></division>

The necessary functionality in our class and service already exists, so now you can invite other users (who are already registered!) and see the same dashboard you have in your account.

Handling table changes in real time

The good thing is that it's now easy for us to implement the functionality on the fly: it just needs to be enabled.

We can do this in the Supabase table editor, so go to your tables, click on the little arrow next to edi so you can edit the table and enable realtime for the bot.cardsjlisa!

Building a real-time Trello board with Supabase and Angular (10)

We can now fetch those updates if we hear them, and while the API for them might change a bit with the next Supabase JS update, the general idea still applies:

We create a newHeand return it asobservableand then listen for changes to our sheetsEm().

Whenever we receive an update, we issue that change to the subject, so we have a stream of updates to feed our view.

To continue, open thesrc/app/services/data.service.tsand add this additional function:

getTableChanges() {constant changes = new topic();this.supabase.von(CARD TABLE).Em('*', (Useful load:any) => { load);}).subscribe to();this.supabase.von(TABLE_LISTS).Em('*', (Useful load:any) => { load);}).subscribe to();Return changes.asObservable();}

Now that we can easily get any updates to our relevant tables in real time, we just need to handle them accordingly.

It's just a matter of figuring out whatCairoccurred (INSERT, UPDATE, DELETE) and then apply the changes to our local data to add, change or delete data.

Also, we have finally implemented our feature onsrc/app/components/inside/board/board.component.tswe leave open until now:

handleRealtimeUpdates() {this.dataService.getTableChanges().subscribe((update: any) => {Constantlyrecord= update.neu?.id ? update.neu: update.alt;ConstantlyCair= update.event-Typ;if (!record) returns;E (update table=='Cards') {E (Cair==='INSERTION') {this.listCards[registro.list_id].push(pegar);} else if (Cair==='TO UPDATE') {ConstantlynuevoArr= [];for (leave card this.listCards[registro.list_id]) {E (I WENT== record id) { Map= record;}newArr.push(Card);}this.listCards[registro.list_id]= nuevaArr;} else if (Cair==='EXTINGUISH') {this.listCards[registro.list_id]= this.listCards[record.list_id].filter((card: any) => !==;}} else if (update table=='liza') {// REPEAT }});}

This handles events if our event table iscards, but the second part is something similar.

I just have the code for itthe restCase in a second block to not make the first treatment seem so big, but it's pretty much the same logic to handle the different cases and now update everything related to itlisa:

more if (update table=='liza') {E (Cair==='INSERTION') {this.lists.push(Registrador);this.listCards[]=[];} else if (Cair==='TO UPDATE') {this.lists.filter((Lista: beliebig) =>[0] = record;ConstantlynuevoArr= [];for (Liste von this.lists lassen) {E ( record id) { List= record;}newArr.push(lista);} this.lists= nuevaArr;} else if (Cair==='EXTINGUISH') { this.lists= this.lists.filter((list: any) => !==;}}

With this last code snippet, we are completely done with our Supabase Angular Trello clone and you can enjoy the fruits of your hard work!


Building projects with Supabase is great, and we hope this real-world cloning example has given you an idea of ​​the different areas to think about.

He canYou can find the full code for this tutorial on Githubwhere you just need to insert your own Supabase instance and create the tables with the included SQL file.

If you liked the tutorial, you can do itYou can find many other tutorials on my YouTube channel.where I help web developers build amazing mobile apps.

Until next time, happy programming with Supabase!


  • Quickstart: Angular
  • Authentication in Ionic Angular with Supabase


1. 🛑 Angular Real World App with Supabase & Tailwind
(Simon Grimm)
2. 🛑 Angular Real World App with Supabase & Tailwind
(Simon Grimm)
3. Supabase Realtime Subscriptions
(Jason Creviston)
4. Trello Clone using Angular
(Sumit Vekariya)
5. Full Stack Web Developer Trello Clone with Socket IO
(Monsterlessons Academy)
6. SupaCharge Your Supabase With Nx
(Nx - Smart, Fast, Extensible)
Top Articles
Latest Posts
Article information

Author: Melvina Ondricka

Last Updated: 02/02/2023

Views: 6163

Rating: 4.8 / 5 (68 voted)

Reviews: 83% of readers found this page helpful

Author information

Name: Melvina Ondricka

Birthday: 2000-12-23

Address: Suite 382 139 Shaniqua Locks, Paulaborough, UT 90498

Phone: +636383657021

Job: Dynamic Government Specialist

Hobby: Kite flying, Watching movies, Knitting, Model building, Reading, Wood carving, Paintball

Introduction: My name is Melvina Ondricka, I am a helpful, fancy, friendly, innocent, outstanding, courageous, thoughtful person who loves writing and wants to share my knowledge and understanding with you.