diff --git a/web/docs/guides/with-angular.mdx b/web/docs/guides/with-angular.mdx new file mode 100644 index 00000000000..c1fc84ef5bc --- /dev/null +++ b/web/docs/guides/with-angular.mdx @@ -0,0 +1,582 @@ +--- +id: with-angular +title: "Quickstart: Angular" +description: Learn how to use Supabase in your Angular App. +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Intro + +This example provides the steps to build a simple user management app (from scratch!) using Supabase and Angular. It includes: + +- Supabase [Database](/docs/guides/database): a Postgres database for storing your user data. +- Supabase [Auth](/docs/guides/auth): users can sign in with magic links (no passwords, only email). +- Supabase [Storage](/docs/guides/storage): users can upload a photo. +- [Row Level Security](/docs/guides/auth#row-level-security): data is protected so that individuals can only access their own data. +- Instant [APIs](/docs/guides/api): APIs will be automatically generated when you create your database tables. + +By the end of this guide you'll have an app which allows users to login and update some basic profile details: + +![Supabase User Management example](/img/user-management-demo.png) + +Clicking this button the application will: + +- Launch and prepare the Postgres database in Supabase. +- Launch the app in Vercel. +- Fork the example into your own GitHub account. +- Prepare the deployed application with all the necessary environment variables. + +If you want to do it yourself, let's get started! + +### Github + +Whenever you get stuck at any point, take a look at [this repo](https://github.com/angular-supa/supabase-angular-user-management). + +## Project set up + +Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase +and then creating a "schema" inside the database. + +### Create a project + +1. Go to [app.supabase.io](https://app.supabase.io). +1. Click on "New Project". +1. Enter your project details. +1. Wait for the new database to launch. + + +### Set up the database schema + +Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor, +or you can just copy/paste the SQL from below and run it yourself. + + + + + ```sh + 1. Go to the "SQL" section. + 2. Click "User Management Starter". + 4. Click "Run". + ``` + + + + + + + ```sql + -- Create a table for public "profiles" + create table profiles ( + id uuid references auth.users not null, + updated_at timestamp with time zone, + username text unique, + avatar_url text, + website text, + + primary key (id), + unique(username), + constraint username_length check (char_length(username) >= 3) + ); + + alter table profiles enable row level security; + + create policy "Public profiles are viewable by everyone." + on profiles for select + using ( true ); + + create policy "Users can insert their own profile." + on profiles for insert + with check ( auth.uid() = id ); + + create policy "Users can update own profile." + on profiles for update + using ( auth.uid() = id ); + + -- Set up Realtime! + begin; + drop publication if exists supabase_realtime; + create publication supabase_realtime; + commit; + alter publication supabase_realtime add table profiles; + + -- Set up Storage! + insert into storage.buckets (id, name) + values ('avatars', 'avatars'); + + create policy "Avatar images are publicly accessible." + on storage.objects for select + using ( bucket_id = 'avatars' ); + + create policy "Anyone can upload an avatar." + on storage.objects for insert + with check ( bucket_id = 'avatars' ); + + ``` + + + + + +### Get the API Keys + +Now that you've created some database tables, you are ready to insert data using the auto-generated API. +We just need to get the URL and `anon` key from the API settings. + + + + + ```sh + 1. Go to the "Settings" section. + 2. Click "API" in the sidebar. + 3. Find your API URL in this page. + 4. Find your "anon" and "service_role" keys on this page. + ``` + + + + + + +## Building the App + +Let's start building the Angular app from scratch. + +### Initialize a Angular app + +We can use the [Angular CLI](https://angular.io/cli) to initialize +an app called `supabase-angular`: + +```bash +npx ng new supabase-angular --routing false --style css +cd supabase-angular +``` + +Then let's install the only additional dependency: [supabase-js](https://github.com/supabase/supabase-js) + +```bash +npm install @supabase/supabase-js +``` + +And finally we want to save the environment variables in the `environment.ts` file. +All we need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys). +These variables will be exposed on the browser, and that's completely fine since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database. + + +```ts title="environment.ts" +export const environment = { + production: false, + supabaseUrl: "YOUR_SUPABASE_URL", + supbaseKey: "YOUR_SUPABASE_KEY" +}; +``` + +Now that we have the API credentials in place, let's create a **SupabaseService** with `ng g s supabase` to initialize the Supabase client and implement functions to communicate with the Supabase API. + +```ts title="src/app/supabase.service.ts" +import { Injectable } from '@angular/core'; +import {AuthChangeEvent, createClient, Session, SupabaseClient} from '@supabase/supabase-js'; +import {environment} from "../environments/environment"; + +export interface Profile { + username: string; + website: string; + avatar_url: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class SupabaseService { + private supabase: SupabaseClient; + + constructor() { + this.supabase = createClient(environment.supabaseUrl, environment.supbaseKey); + } + + get user() { + return this.supabase.auth.user(); + } + + get session() { + return this.supabase.auth.session(); + } + + get profile() { + return this.supabase + .from('profiles') + .select(`username, website, avatar_url`) + .eq('id', this.user?.id) + .single(); + } + + authChanges(callback: (event: AuthChangeEvent, session: Session | null) => void) { + return this.supabase.auth.onAuthStateChange(callback); + } + + signIn(email: string) { + return this.supabase.auth.signIn({email}); + } + + signOut() { + return this.supabase.auth.signOut(); + } + + updateProfile(profile: Profile) { + const update = { + ...profile, + id: this.user?.id, + updated_at: new Date() + } + + return this.supabase.from('profiles').upsert(update, { + returning: 'minimal', // Don't return the value after inserting + }); + } + + 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); + } +} + +``` + +And one optional step is to update the CSS file `src/index.css` to make the app look nice. +You can find the full contents of this file [here](https://raw.githubusercontent.com/angular-supa/supabase-angular-user-management/main/src/styles.css). + +### Set up a Login component + +Let's set up a Angular component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords. +Create an **AuthComponent** with `ng g c auth` Angular CLI command. + +```ts title="src/app/auth.component.ts" +import { Component } from '@angular/core'; +import {SupabaseService} from "./supabase.service"; + +@Component({ + selector: 'app-auth', + template: ` +
+
+

Supabase + Angular

+

Sign in via magic link with your email below

+
+ +
+
+ +
+
+
+ `, +}) +export class AuthComponent { + loading = false; + + constructor(private readonly supabase: SupabaseService) { } + + async handleLogin(input: string) { + try { + this.loading = true; + await this.supabase.signIn(input); + alert('Check your email for the login link!'); + } catch (error) { + alert(error.error_description || error.message) + } finally { + this.loading = false; + } + } +} +``` + +### Account page + +After a user is signed in we can allow them to edit their profile details and manage their account. +Create an **AccountComponent** with `ng g c account` Angular CLI command. + +```ts title="src/app/account.component.ts" +import {Component, Input, OnInit} from '@angular/core'; +import {Profile, SupabaseService} from "./supabase.service"; +import {Session} from "@supabase/supabase-js"; + +@Component({ + selector: 'app-account', + template: ` +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+ +
+ +
+
+ ` +}) +export class AccountComponent implements OnInit { + loading = false; + profile: Profile | undefined; + + @Input() session: Session | undefined; + + constructor(private readonly supabase: SupabaseService) { } + + ngOnInit() { + this.getProfile(); + } + + async getProfile() { + try { + this.loading = true; + let {data: profile, error, status} = await this.supabase.profile; + + if (error && status !== 406) { + throw error; + } + + if (profile) { + this.profile = profile; + } + } catch (error) { + alert(error.message) + } finally { + this.loading = false; + } + } + + async updateProfile(username: string, website: string, avatar_url: string = '') { + try { + this.loading = true; + await this.supabase.updateProfile({username, website, avatar_url}); + } catch (error) { + alert(error.message); + } finally { + this.loading = false; + } + } + + async signOut() { + await this.supabase.signOut(); + } +} +``` + + + +### Launch! + +Now that we have all the components in place, let's update **AppComponent**: + +```ts title="src/app/app.component.ts" +import {Component, OnInit} from '@angular/core'; +import {SupabaseService} from "./supabase.service"; + +@Component({ + selector: 'app-root', + template: ` +
+ + + + +
+ ` +}) +export class AppComponent implements OnInit { + session = this.supabase.session; + + constructor(private readonly supabase: SupabaseService) { } + + ngOnInit() { + this.supabase.authChanges((_, session) => this.session = session); + } +} +``` + +Once that's done, run this in a terminal window: + +```bash +npm run start +``` + +And then open the browser to [localhost:4200](http://localhost:4200) and you should see the completed app. + + +![Supabase Angular](/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 + +Let's 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. + +```ts title="src/app/avatar.component.ts" +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {SupabaseService} from "./supabase.service"; +import {DomSanitizer, SafeResourceUrl} from "@angular/platform-browser"; + +@Component({ + selector: 'app-avatar', + template: ` +
+ Avatar
+
+
+ + +
+ `, +}) +export class AvatarComponent { + _avatarUrl: SafeResourceUrl | undefined; + uploading = false; + + @Input() + set avatarUrl(url: string | undefined) { + if (url) { + this.downloadImage(url); + } + }; + + @Output() upload = new EventEmitter(); + + constructor( + private readonly supabase: SupabaseService, + private readonly dom: DomSanitizer + ) { } + + async downloadImage(path: string) { + try { + const {data} = await this.supabase.downLoadImage(path); + this._avatarUrl = this.dom.bypassSecurityTrustResourceUrl(URL.createObjectURL(data)); + } catch (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 fileName = `${Math.random()}.${fileExt}`; + const filePath = `${fileName}`; + + await this.supabase.uploadAvatar(filePath, file); + this.upload.emit(filePath); + } catch (error) { + alert(error.message); + } finally { + this.uploading = false; + } + } +} +``` + + +### Add the new widget + +And then we can add the widget on top of the **AccountComponent** html template: + +```ts title="src/app/account.component.ts" +template: ` + + + + +` +``` + +## Next steps + +At this stage you have a fully functional application! + +- Got a question? [Ask here](https://github.com/supabase/supabase/discussions). +- Sign in: [app.supabase.io](https://app.supabase.io) diff --git a/web/sidebars.js b/web/sidebars.js index d34518e5098..3e4c4533af6 100755 --- a/web/sidebars.js +++ b/web/sidebars.js @@ -88,6 +88,7 @@ module.exports = { 'guides/with-react', 'guides/with-svelte', 'guides/with-vue-3', + 'guides/with-angular', 'guides/self-hosting', ], }, diff --git a/web/static/img/supabase-angular-demo.png b/web/static/img/supabase-angular-demo.png new file mode 100644 index 00000000000..7fbeaa64199 Binary files /dev/null and b/web/static/img/supabase-angular-demo.png differ