Files
supabase/apps/docs/content/guides/getting-started/tutorials/with-angular.mdx
Chris Chinchilla 0e2bd38457 Update Angular tutorial to use v20 (#37380)
* Add Chris Ward to humans

Signed-off-by: Chris Chinchilla <christopher.ward@supabase.io>

* Update Angular tutorial for Angular 20

* Update Angular doc

* Revert "Add Chris Ward to humans"

This reverts commit c45ea9d213.

* Update tsconfig

* Add note about CLI defaults

* Prettier fix

---------

Signed-off-by: Chris Chinchilla <christopher.ward@supabase.io>
Co-authored-by: Chris Chinchilla <christopher.ward@supabase.io>
2025-07-28 16:27:50 +02:00

585 lines
15 KiB
Plaintext

---
title: 'Build a User Management App with Angular'
description: 'Learn how to use Supabase in your Angular App.'
---
<$Partial path="quickstart_intro.mdx" />
![Supabase User Management example](/docs/img/user-management-demo.png)
<Admonition type="note">
If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/angular-user-management).
</Admonition>
<$Partial path="project_setup.mdx" />
## Building the app
Start with building the Angular app from scratch.
### Initialize an Angular app
You can use the [Angular CLI](https://angular.io/cli) to initialize
an app called `supabase-angular`. The command sets some defaults, that you change to suit your needs:
```bash
npx ng new supabase-angular --routing false --style css --standalone false --zoneless true --ssr false
cd supabase-angular
```
Then, install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js)
```bash
npm install @supabase/supabase-js
```
Finally, save the environment variables in the `src/environments/environment.ts` file.
All you need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys).
The application exposes these variables in the browser, and that's fine as you have [Row Level Security](/docs/guides/auth#row-level-security) enabled on the Database.
<$CodeTabs>
```ts name=src/environments/environment.ts
export const environment = {
production: false,
supabaseUrl: 'YOUR_SUPABASE_URL',
supabaseKey: 'YOUR_SUPABASE_KEY',
}
```
</$CodeTabs>
Now you have the API credentials in place, create a `SupabaseService` with `ng g s supabase` and add the following code to initialize the Supabase client and implement functions to communicate with the Supabase API.
<$CodeTabs>
```ts name=src/app/supabase.service.ts
import { Injectable } from '@angular/core'
import {
AuthChangeEvent,
AuthSession,
createClient,
Session,
SupabaseClient,
User,
} from '@supabase/supabase-js'
import { environment } from '../environments/environment'
export interface Profile {
id?: string
username: string
website: string
avatar_url: string
}
@Injectable({
providedIn: 'root',
})
export class SupabaseService {
private supabase: SupabaseClient
_session: AuthSession | null = null
constructor() {
this.supabase = createClient(environment.supabaseUrl, environment.supabaseKey)
}
get session() {
this.supabase.auth.getSession().then(({ data }) => {
this._session = data.session
})
return this._session
}
profile(user: User) {
return this.supabase
.from('profiles')
.select(`username, website, avatar_url`)
.eq('id', user.id)
.single()
}
authChanges(callback: (event: AuthChangeEvent, session: Session | null) => void) {
return this.supabase.auth.onAuthStateChange(callback)
}
signIn(email: string) {
return this.supabase.auth.signInWithOtp({ email })
}
signOut() {
return this.supabase.auth.signOut()
}
updateProfile(profile: Profile) {
const update = {
...profile,
updated_at: new Date(),
}
return this.supabase.from('profiles').upsert(update)
}
downLoadImage(path: string) {
return this.supabase.storage.from('avatars').download(path)
}
uploadAvatar(filePath: string, file: File) {
return this.supabase.storage.from('avatars').upload(filePath, file)
}
}
```
</$CodeTabs>
Optionally, update `src/styles.css` [with the following styles](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/angular-user-management/src/styles.css) to style the app.
### Set up a login component
Next, set up an Angular component to manage logins and sign ups. The component uses [Magic Links](/docs/guides/auth/auth-email-passwordless#with-magic-link), so users can sign in with their email without using passwords.
Create an `AuthComponent` with the `ng g c auth` Angular CLI command and add the following code.
<$CodeTabs>
```ts name=src/app/auth/auth.ts
import { Component } from '@angular/core'
import { FormBuilder, FormGroup } from '@angular/forms'
import { SupabaseService } from '../supabase.service'
@Component({
selector: 'app-auth',
templateUrl: './auth.html',
styleUrls: ['./auth.css'],
standalone: false,
})
export class AuthComponent {
signInForm!: FormGroup
constructor(
private readonly supabase: SupabaseService,
private readonly formBuilder: FormBuilder
) {}
loading = false
ngOnInit() {
this.signInForm = this.formBuilder.group({
email: '',
})
}
async onSubmit(): Promise<void> {
try {
this.loading = true
const email = this.signInForm.value.email as string
const { error } = await this.supabase.signIn(email)
if (error) throw error
alert('Check your email for the login link!')
} catch (error) {
if (error instanceof Error) {
alert(error.message)
}
} finally {
this.signInForm.reset()
this.loading = false
}
}
}
```
```html name=src/app/auth/auth.html
<div class="row flex-center flex">
<div class="col-6 form-widget" aria-live="polite">
<h1 class="header">Supabase + Angular</h1>
<p class="description">Sign in via magic link with your email below</p>
<form [formGroup]="signInForm" (ngSubmit)="onSubmit()" class="form-widget">
<div>
<label for="email">Email</label>
<input
id="email"
formControlName="email"
class="inputField"
type="email"
placeholder="Your email"
/>
</div>
<div>
<button type="submit" class="button block" [disabled]="loading">
{{ loading ? "Loading" : "Send magic link" }}
</button>
</div>
</form>
</div>
</div>
```
</$CodeTabs>
### Account page
Users also need a way to edit their profile details and manage their accounts after signing in.
Create an `AccountComponent` with the `ng g c account` Angular CLI command and add the following code.
<$CodeTabs>
```ts name=src/app/account/account.ts
import { Component, Input, OnInit } from '@angular/core'
import { FormBuilder, FormGroup } from '@angular/forms'
import { AuthSession } from '@supabase/supabase-js'
import { Profile, SupabaseService } from '../supabase.service'
@Component({
selector: 'app-account',
templateUrl: './account.html',
styleUrls: ['./account.css'],
standalone: false,
})
export class AccountComponent implements OnInit {
loading = false
profile!: Profile
updateProfileForm!: FormGroup
get avatarUrl() {
return this.updateProfileForm.value.avatar_url as string
}
async updateAvatar(event: string): Promise<void> {
this.updateProfileForm.patchValue({
avatar_url: event,
})
await this.updateProfile()
}
@Input()
session!: AuthSession
constructor(
private readonly supabase: SupabaseService,
private formBuilder: FormBuilder
) {
this.updateProfileForm = this.formBuilder.group({
username: '',
website: '',
avatar_url: '',
})
}
async ngOnInit(): Promise<void> {
await this.getProfile()
const { username, website, avatar_url } = this.profile
this.updateProfileForm.patchValue({
username,
website,
avatar_url,
})
}
async getProfile() {
try {
this.loading = true
const { user } = this.session
const { data: profile, error, status } = await this.supabase.profile(user)
if (error && status !== 406) {
throw error
}
if (profile) {
this.profile = profile
}
} catch (error) {
if (error instanceof Error) {
alert(error.message)
}
} finally {
this.loading = false
}
}
async updateProfile(): Promise<void> {
try {
this.loading = true
const { user } = this.session
const username = this.updateProfileForm.value.username as string
const website = this.updateProfileForm.value.website as string
const avatar_url = this.updateProfileForm.value.avatar_url as string
const { error } = await this.supabase.updateProfile({
id: user.id,
username,
website,
avatar_url,
})
if (error) throw error
} catch (error) {
if (error instanceof Error) {
alert(error.message)
}
} finally {
this.loading = false
}
}
async signOut() {
await this.supabase.signOut()
}
}
```
```html name=src/app/account/account.html
<form [formGroup]="updateProfileForm" (ngSubmit)="updateProfile()" class="form-widget">
<app-avatar [avatarUrl]="this.avatarUrl" (upload)="updateAvatar($event)"> </app-avatar>
<div>
<label for="email">Email</label>
<input id="email" type="text" [value]="session.user.email" disabled />
</div>
<div>
<label for="username">Name</label>
<input formControlName="username" id="username" type="text" />
</div>
<div>
<label for="website">Website</label>
<input formControlName="website" id="website" type="url" />
</div>
<div>
<button type="submit" class="button primary block" [disabled]="loading">
{{ loading ? "Loading ..." : "Update" }}
</button>
</div>
<div>
<button class="button block" (click)="signOut()">Sign Out</button>
</div>
</form>
```
</$CodeTabs>
### Launch!
Now you have all the components in place, update `AppComponent`:
<$CodeTabs>
```ts name=src/app/app.ts
import { Component, OnInit } from '@angular/core'
import { SupabaseService } from './supabase.service'
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrls: ['./app.css'],
standalone: false,
})
export class AppComponent implements OnInit {
constructor(private readonly supabase: SupabaseService) {}
title = 'angular-user-management'
session: any
ngOnInit() {
this.session = this.supabase.session
this.supabase.authChanges((_, session) => (this.session = session))
}
}
```
```html name=src/app/app.html
<div class="container" style="padding: 50px 0 100px 0">
<app-account *ngIf="session; else auth" [session]="session"></app-account>
<ng-template #auth>
<app-auth></app-auth>
</ng-template>
</div>
```
</$CodeTabs>
You also need to change `app.module.ts` to include the `ReactiveFormsModule` from the `@angular/forms` package.
<$CodeTabs>
```ts name=src/app/app.module.ts
import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { AppComponent } from './app'
import { AuthComponent } from './auth/auth'
import { AccountComponent } from './account/account'
import { ReactiveFormsModule } from '@angular/forms'
import { AvatarComponent } from './avatar/avatar'
@NgModule({
declarations: [AppComponent, AuthComponent, AccountComponent, AvatarComponent],
imports: [BrowserModule, ReactiveFormsModule],
providers: [],
bootstrap: [AppComponent],
exports: [AppComponent, AuthComponent, AccountComponent, AvatarComponent],
})
export class AppModule {}
```
</$CodeTabs>
Once that's done, run the application in a terminal:
```bash
npm run start
```
Open the browser to [localhost:4200](http://localhost:4200) and you should see the completed app.
![Screenshot of the Supabase Angular application running in a browser](/docs/img/supabase-angular-demo.png)
## Bonus: Profile photos
Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like photos and videos.
### Create an upload widget
Create an avatar for the user so that they can upload a profile photo.
Create an `AvatarComponent` with `ng g c avatar` Angular CLI command and add the following code.
<$CodeTabs>
```ts name=src/app/avatar/avatar.ts
import { Component, EventEmitter, Input, Output } from '@angular/core'
import { SafeResourceUrl, DomSanitizer } from '@angular/platform-browser'
import { SupabaseService } from '../supabase.service'
@Component({
selector: 'app-avatar',
templateUrl: './avatar.html',
styleUrls: ['./avatar.css'],
standalone: false,
})
export class AvatarComponent {
_avatarUrl: SafeResourceUrl | undefined
uploading = false
@Input()
set avatarUrl(url: string | null) {
if (url) {
this.downloadImage(url)
}
}
@Output() upload = new EventEmitter<string>()
constructor(
private readonly supabase: SupabaseService,
private readonly dom: DomSanitizer
) {}
async downloadImage(path: string) {
try {
const { data } = await this.supabase.downLoadImage(path)
if (data instanceof Blob) {
this._avatarUrl = this.dom.bypassSecurityTrustResourceUrl(URL.createObjectURL(data))
}
} catch (error) {
if (error instanceof Error) {
console.error('Error downloading image: ', error.message)
}
}
}
async uploadAvatar(event: any) {
try {
this.uploading = true
if (!event.target.files || event.target.files.length === 0) {
throw new Error('You must select an image to upload.')
}
const file = event.target.files[0]
const fileExt = file.name.split('.').pop()
const filePath = `${Math.random()}.${fileExt}`
await this.supabase.uploadAvatar(filePath, file)
this.upload.emit(filePath)
} catch (error) {
if (error instanceof Error) {
alert(error.message)
}
} finally {
this.uploading = false
}
}
}
```
```html name=src/app/avatar/avatar.html
<div>
<img
*ngIf="_avatarUrl"
[src]="_avatarUrl"
alt="Avatar"
class="avatar image"
style="height: 150px; width: 150px"
/>
</div>
<div *ngIf="!_avatarUrl" class="avatar no-image" style="height: 150px; width: 150px"></div>
<div style="width: 150px">
<label class="button primary block" for="single">
{{ uploading ? "Uploading ..." : "Upload" }}
</label>
<input
style="visibility: hidden; position: absolute"
type="file"
id="single"
accept="image/*"
(change)="uploadAvatar($event)"
[disabled]="uploading"
/>
</div>
```
</$CodeTabs>
### Add the new widget
And then we can add the widget on top of the `AccountComponent` HTML template:
<$CodeTabs>
```html name=src/app/account.html
<form [formGroup]="updateProfileForm" (ngSubmit)="updateProfile()" class="form-widget">
<app-avatar [avatarUrl]="this.avatarUrl" (upload)="updateAvatar($event)"></app-avatar>
<!-- input fields -->
</form>
```
</$CodeTabs>
And add an `updateAvatar` function along with an `avatarUrl` getter to the `AccountComponent` typescript file:
<$CodeTabs>
```ts name=src/app/account.ts
@Component({
selector: 'app-account',
templateUrl: './account.html',
styleUrls: ['./account.css'],
})
export class AccountComponent implements OnInit {
// ...
get avatarUrl() {
return this.updateProfileForm.value.avatar_url as string
}
async updateAvatar(event: string): Promise<void> {
this.updateProfileForm.patchValue({
avatar_url: event,
})
await this.updateProfile()
}
// ...
}
```
</$CodeTabs>
At this stage you have a fully functional application!