Published:

Angular 10 - Facebook Login Tutorial & Example

Tutorial built with Angular 10.1.2

Other versions available:

In this tutorial we'll cover how to implement Facebook Login in Angular 10 with an example app that allows you to login with Facebook and view/update/delete accounts registered in the Angular app.

The first time you login with Facebook an account is registered in the Angular app with your Facebook id so it can identify you when you login again with Facebook. The account is created with the name from your Facebook account and an extraInfo field with some default text, both the name and extra info can be updated in the Angular app, and updating account details only changes them in the app it doesn't affect anything on Facebook.

The example project is available on GitHub at https://github.com/cornflourblue/angular-10-facebook-login-example.

Here it is in action: (See on StackBlitz at https://stackblitz.com/edit/angular-10-facebook-login-example)


Angular Facebook Login App Details

The example app contains the following three routes (pages) to demonstrate logging in with Facebook, viewing accounts and updating account details:

  • Login (/login) - contains a Facebook login button that triggers authentication with Facebook and registration/authentication with the Angular app.
  • Home (/) - displays a list of all accounts in the Angular app with buttons to edit or delete any of them.
  • Edit Account (/edit/:id) - contains a form to update the specified account details.

Facebook App is required for Facebook Login

To integrate Facebook Login into a website or application you need to create a Facebook App at https://developers.facebook.com/apps/ and set up the Facebook Login product under the App. Creating a Facebook App will provide you with a Facebook App ID which is required when initializing the Facebook JavaScript SDK (FB.init(...)). For more info see the Facebook Login docs at https://developers.facebook.com/docs/facebook-login/web.

The example Angular app uses a Facebook App named JasonWatmore.com Login Example that I created for this tutorial (App ID: 314930319788683). The Facebook App ID is located in environment.ts and environment.prod.ts in the example.

Fake backend API

The example Angular app runs with a fake backend api by default to enable it to run completely in the browser without a real api (backend-less), the fake api contains routes for authentication and account CRUD operations and it uses browser local storage to save data. To disable the fake backend you just have to remove a couple of lines of code from the app module, you can refer to the fake-backend to see what's required to build a real api for the example.

Updates only affect data in the Angular app

Updating or deleting account information in the Angular app will only change the data saved in the app, it won't (and can't) change anything in the associated Facebook account.

Authentication flow with Facebook access tokens and JWT tokens

Authentication is implemented with Facebook access tokens and JWT tokens. On successful login to Facebook an access token is returned to the Angular app, which is then used to authenticate with the api (or fake backend) which returns a short lived JWT token that expires after 15 minutes. The JWT is used for accessing secure routes on the api and the Facebook access token is used for re-authenticating with the api to get a new JWT token when (or just before) it expires, the Angular app starts a timer to re-authenticate for a new JWT token 1 minute before it expires to keep the account logged in.


Run the Angular Facebook Login App Locally

  1. Install Node.js and npm from https://nodejs.org
  2. Download or clone the project source code from https://github.com/cornflourblue/angular-10-facebook-login-example
  3. Install all required npm packages by running npm install from the command line in the project root folder (where the package.json is located).
  4. Start the application in SSL (https) mode by running npm run start:ssl from the command line in the project root folder, SSL is required for the Facebook SDK to run properly, this will launch a browser with the URL https://localhost:4200/.
  5. You should see the message Your connection is not private (or something similar in non Chrome browsers), this is nothing to worry about it's just because the Angular development server runs with a self signed SSL certificate. To open the app click the "Advanced" button and the link "Proceed to localhost".


Angular Project Structure

The Angular CLI was used to generate the base project structure with the ng new <project name> command, the CLI is also used to build and serve the application. For more info about the Angular CLI see https://angular.io/cli.

The app and code structure of the tutorial mostly follow the best practice recommendations in the official Angular Style Guide, with a few of my own tweaks here and there.

Each feature has it's own folder (home & login), other shared/common code such as services, models, helpers etc are placed in folders prefixed with an underscore _ to easily differentiate them from features and group them together at the top of the folder structure.

The index.ts files in each folder are barrel files that group the exported modules from that folder together so they can be imported using only the folder path instead of the full module path, and to enable importing multiple modules in a single import (e.g. import { HomeComponent, EditAccountComponent } from './home').

Path aliases @app and @environments have been configured in tsconfig.base.json that map to the /src/app and /src/environments directories. This allows imports to be relative to the app and environments folders by prefixing import paths with aliases instead of having to use long relative paths (e.g. import MyComponent from '../../../MyComponent').

Here are the main project files that contain the application logic, I left out some files that were generated by Angular CLI ng new command that I didn't change.

 

App Initializer

Path: /src/app/_helpers/app.initializer.ts

The app initializer runs before the Angular app starts up to load and initialize the Facebook SDK, and get the user's login status from Facebook. If the user is already logged in with Facebook they are automatically logged into the Angular app using the Facebook access token and taken to the home page, otherwise the app starts normally and displays the login page.

The app initializer is added to angular app in the providers section of the app module using the APP_INITIALIZER injection token. For more info see https://angular.io/api/core/APP_INITIALIZER.

import { AccountService } from '@app/_services';
import { environment } from '@environments/environment';

export function appInitializer(accountService: AccountService) {
    return () => new Promise(resolve => {
        // wait for facebook sdk to initialize before starting the angular app
        window['fbAsyncInit'] = function () {
            FB.init({
                appId: environment.facebookAppId,
                cookie: true,
                xfbml: true,
                version: 'v8.0'
            });

            // auto authenticate with the api if already logged in with facebook
            FB.getLoginStatus(({authResponse}) => {
                if (authResponse) {
                    accountService.apiAuthenticate(authResponse.accessToken)
                        .subscribe()
                        .add(resolve);
                } else {
                    resolve();
                }
            });
        };

        // load facebook sdk script
        (function (d, s, id) {
            var js, fjs = d.getElementsByTagName(s)[0];
            if (d.getElementById(id)) { return; }
            js = d.createElement(s); js.id = id;
            js.src = "https://connect.facebook.net/en_US/sdk.js";
            fjs.parentNode.insertBefore(js, fjs);
        }(document, 'script', 'facebook-jssdk'));    
    });
}
 

Auth Guard

Path: /src/app/_helpers/auth.guard.ts

The auth guard is an angular route guard that's used to prevent unauthorized users from accessing restricted routes, it does this by implementing the CanActivate interface which allows the guard to decide if a route can be activated with the canActivate() method. If the method returns true the route is activated (allowed to proceed), otherwise if the method returns false the route is blocked.

The auth guard uses the account service to check if the user is logged in, if the user isn't logged in they're redirected to the /login page with the returnUrl in the query parameters.

Angular route guards are attached to routes in the router config, this auth guard is used in app-routing.module.ts to protect the home and edit account routes.

import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

import { AccountService } from '@app/_services';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
    constructor(
        private router: Router,
        private accountService: AccountService
    ) { }

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        const account = this.accountService.accountValue;
        if (account) {
            // logged in so return true
            return true;
        }

        // not logged in so redirect to login page with the return url
        this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
        return false;
    }
}
 

Error Interceptor

Path: /src/app/_helpers/error.interceptor.ts

The Error Interceptor intercepts http responses from the api to check if there were any errors. If there is a 401 Unauthorized or 403 Forbidden response the account is automatically logged out of the application, all other errors are re-thrown up to the calling service so an alert with the error can be displayed on the screen.

It's implemented using the Angular HttpInterceptor interface included in the HttpClientModule, by implementing the HttpInterceptor interface you can create a custom interceptor to catch all error responses from the api in a single location.

Http interceptors are added to the request pipeline in the providers section of the app.module.ts file.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

import { AccountService } from '@app/_services';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
    constructor(private accountService: AccountService) { }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(request).pipe(catchError(err => {
            if ([401, 403].includes(err.status)) {
                // auto logout if 401 or 403 response returned from api
                this.accountService.logout();
            }

            const error = err.error?.message || err.statusText;
            console.error(err);
            return throwError(error);
        }))
    }
}
 

Fake Backend Provider

Path: /src/app/_helpers/fake-backend.ts

In order to run and test the Angular app without a real backend API, the example uses a fake backend that intercepts the HTTP requests from the Angular app and sends back "fake" responses. This is done by a class that implements the Angular HttpInterceptor interface, for more information on Angular HTTP Interceptors see https://angular.io/api/common/http/HttpInterceptor or this article.

The fake backend is organised into a top level handleRoute() function that checks the request url and method to determine how the request should be handled. For intercepted routes one of the below // route functions is called, for all other routes the request is passed through to the real backend by calling next.handle(request). Below the route functions there are // helper functions for returning different response types and performing small tasks.

For more info see Angular 10 - Fake Backend Example for Backendless Development.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS } from '@angular/common/http';
import { Observable, of, throwError, from } from 'rxjs';
import { delay, materialize, dematerialize, concatMap } from 'rxjs/operators';

// array in local storage for accounts
const accountsKey = 'angular-10-facebook-login-accounts';
let accounts = JSON.parse(localStorage.getItem(accountsKey)) || [];

@Injectable()
export class FakeBackendInterceptor implements HttpInterceptor {
    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const { url, method, headers, body } = request;

        // wrap in delayed observable to simulate server api call
        return handleRoute();

        function handleRoute() {
            switch (true) {
                case url.endsWith('/accounts/authenticate') && method === 'POST':
                    return authenticate();
                case url.endsWith('/accounts') && method === 'GET':
                    return getAccounts();
                case url.match(/\/accounts\/\d+$/) && method === 'GET':
                    return getAccountById();
                case url.match(/\/accounts\/\d+$/) && method === 'PUT':
                    return updateAccount();
                case url.match(/\/accounts\/\d+$/) && method === 'DELETE':
                    return deleteAccount();
                default:
                    // pass through any requests not handled above
                    return next.handle(request);
            }
        }

        // route functions

        function authenticate() {
            const { accessToken } = body;

            return from(new Promise(resolve => {
                fetch(`https://graph.facebook.com/v8.0/me?access_token=${accessToken}`)
                    .then(response => resolve(response.json()));
            })).pipe(concatMap((data: any) => {
                if (data.error) return unauthorized(data.error.message);

                let account = accounts.find(x => x.facebookId === data.id);
                if (!account) {
                    // create new account if first time logging in
                    account = {
                        id: newAccountId(),
                        facebookId: data.id,
                        name: data.name,
                        extraInfo: `This is some extra info about ${data.name} that is saved in the API`
                    }
                    accounts.push(account);
                    localStorage.setItem(accountsKey, JSON.stringify(accounts));
                }

                return ok({
                    ...account,
                    token: generateJwtToken(account)
                });
            }));
        }

        function getAccounts() {
            if (!isLoggedIn()) return unauthorized();
            return ok(accounts);
        }

        function getAccountById() {
            if (!isLoggedIn()) return unauthorized();

            let account = accounts.find(x => x.id === idFromUrl());
            return ok(account);
        }

        function updateAccount() {
            if (!isLoggedIn()) return unauthorized();

            let params = body;
            let account = accounts.find(x => x.id === idFromUrl());

            // update and save account
            Object.assign(account, params);
            localStorage.setItem(accountsKey, JSON.stringify(accounts));

            return ok(account);
        }

        function deleteAccount() {
            if (!isLoggedIn()) return unauthorized();

            // delete account then save
            accounts = accounts.filter(x => x.id !== idFromUrl());
            localStorage.setItem(accountsKey, JSON.stringify(accounts));
            return ok();
        }
        
        // helper functions

        function ok(body?) {
            return of(new HttpResponse({ status: 200, body }))
                .pipe(delay(500));
        }

        function unauthorized(message = 'Unauthorized') {
            return throwError({ status: 401, error: { message } })
                .pipe(materialize(), delay(500), dematerialize());
        }

        function isLoggedIn() {
            return headers.get('Authorization')?.startsWith('Bearer fake-jwt-token');
        }

        function idFromUrl() {
            const urlParts = url.split('/');
            return parseInt(urlParts[urlParts.length - 1]);
        }

        function newAccountId() {
            return accounts.length ? Math.max(...accounts.map(x => x.id)) + 1 : 1;
        }

        function generateJwtToken(account) {
            // create token that expires in 15 minutes
            const tokenPayload = { 
                exp: Math.round(new Date(Date.now() + 15*60*1000).getTime() / 1000),
                id: account.id
            }
            return `fake-jwt-token.${btoa(JSON.stringify(tokenPayload))}`;
        }
    }
}

export let fakeBackendProvider = {
    // use fake backend in place of Http service for backend-less development
    provide: HTTP_INTERCEPTORS,
    useClass: FakeBackendInterceptor,
    multi: true
};
 

JWT Interceptor

Path: /src/app/_helpers/jwt.interceptor.ts

The JWT Interceptor intercepts http requests from the application to add a JWT auth token to the Authorization header if the user is logged in and the request is to the Angular app's api url (environment.apiUrl).

It's implemented using the HttpInterceptor interface included in the HttpClientModule, by implementing the HttpInterceptor interface you can create a custom interceptor to modify http requests before they get sent to the server.

Http interceptors are added to the request pipeline in the providers section of the app.module.ts file.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs';

import { environment } from '@environments/environment';
import { AccountService } from '@app/_services';

@Injectable()
export class JwtInterceptor implements HttpInterceptor {
    constructor(private accountService: AccountService) { }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // add auth header with jwt if account is logged in and request is to the api url
        const account = this.accountService.accountValue;
        const isLoggedIn = account?.token;
        const isApiUrl = request.url.startsWith(environment.apiUrl);
        if (isLoggedIn && isApiUrl) {
            request = request.clone({
                setHeaders: { Authorization: `Bearer ${account.token}` }
            });
        }

        return next.handle(request);
    }
}
 

Account Model

Path: /src/app/_models/account.ts

The account model is a small class that defines the properties of an account.

export class Account {
    id: string;
    facebookId: string;
    name: string;
    extraInfo: string;
    token?: string;
}
 

Account Service

Path: /src/app/_services/account.service.ts

The account service handles communication between the Angular app and the backend api for everything related to accounts. It contains methods for logging in and out, as well as standard CRUD methods for retrieving and modifying account data.

The login() method first calls the facebookLogin() method to login to Facebook, then passes the Facebook access token to the apiAuthenticate() method to login to the api (or fake backend).

On successful login the api returns the account details and a JWT token which are published to all subscriber components with the call to this.accountSubject.next(account) in the apiAuthenticate() method. The method then starts a countdown timer by calling this.startAuthenticateTimer() to auto refresh the JWT token in the background (silent refresh) one minute before it expires in order to keep the account logged in.

The logout() method revokes the Facebook App's permissions with FB.api('/me/permissions', 'delete') then logs out of Facebook by calling FB.logout(), revoking permissions is required to completely logout because FB.logout() doesn't remove the FB auth cookie so the user is logged back in on page refresh. The logout() method then cancels the silent refresh running in the background by calling this.stopAuthenticateTimer(), logs the user out of the Angular app by publishing a null value to all subscriber components (this.accountSubject.next(null)) and redirects to the login page.

The account property exposes an RxJS observable (Observable<Account>) so any component can subscribe to be notified when a user logs in, logs out, has their token refreshed or updates their account. The notification is triggered by the call to this.accountSubject.next() from each of the corresponding methods in the service. For more info on component communication with RxJS see Angular 10 - Communicating Between Components with Observable & Subject.

import { Injectable } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, from, of, EMPTY } from 'rxjs';
import { map, concatMap, finalize } from 'rxjs/operators';

import { environment } from '@environments/environment';
import { Account } from '@app/_models';

const baseUrl = `${environment.apiUrl}/accounts`;

@Injectable({ providedIn: 'root' })
export class AccountService {
    private accountSubject: BehaviorSubject<Account>;
    public account: Observable<Account>;

    constructor(
        private router: Router,
        private route: ActivatedRoute,
        private http: HttpClient
    ) {
        this.accountSubject = new BehaviorSubject<Account>(null);
        this.account = this.accountSubject.asObservable();
    }

    public get accountValue(): Account {
        return this.accountSubject.value;
    }

    login() {
        // login with facebook then authenticate with the API to get a JWT auth token
        this.facebookLogin()
            .pipe(concatMap(accessToken => this.apiAuthenticate(accessToken)))
            .subscribe(() => {
                // get return url from query parameters or default to home page
                const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
                this.router.navigateByUrl(returnUrl);
            });
    }

    facebookLogin() {
        // login with facebook and return observable with fb access token on success
        return from(new Promise<fb.StatusResponse>(resolve => FB.login(resolve)))
            .pipe(concatMap(({ authResponse }) => {
                if (!authResponse) return EMPTY;
                return of(authResponse.accessToken);
            }));
    }

    apiAuthenticate(accessToken: string) {
        // authenticate with the api using a facebook access token,
        // on success the api returns an account object with a JWT auth token
        return this.http.post<any>(`${baseUrl}/authenticate`, { accessToken })
            .pipe(map(account => {
                this.accountSubject.next(account);
                this.startAuthenticateTimer();
                return account;
            }));
    }

    logout() {
        // revoke app permissions to logout completely because FB.logout() doesn't remove FB cookie
        FB.api('/me/permissions', 'delete', null, () => FB.logout());
        this.stopAuthenticateTimer();
        this.accountSubject.next(null);
        this.router.navigate(['/login']);
    }

    getAll() {
        return this.http.get<Account[]>(baseUrl);
    }

    getById(id) {
        return this.http.get<Account>(`${baseUrl}/${id}`);
    }
    
    update(id, params) {
        return this.http.put(`${baseUrl}/${id}`, params)
            .pipe(map((account: any) => {
                // update the current account if it was updated
                if (account.id === this.accountValue.id) {
                    // publish updated account to subscribers
                    account = { ...this.accountValue, ...account };
                    this.accountSubject.next(account);
                }
                return account;
            }));
    }

    delete(id: string) {
        return this.http.delete(`${baseUrl}/${id}`)
            .pipe(finalize(() => {
                // auto logout if the logged in account was deleted
                if (id === this.accountValue.id)
                    this.logout();
            }));
    }

    // helper methods

    private authenticateTimeout;

    private startAuthenticateTimer() {
        // parse json object from base64 encoded jwt token
        const jwtToken = JSON.parse(atob(this.accountValue.token.split('.')[1]));

        // set a timeout to re-authenticate with the api one minute before the token expires
        const expires = new Date(jwtToken.exp * 1000);
        const timeout = expires.getTime() - Date.now() - (60 * 1000);
        const { accessToken } = FB.getAuthResponse();
        this.authenticateTimeout = setTimeout(() => {
            this.apiAuthenticate(accessToken).subscribe();
        }, timeout);
    }

    private stopAuthenticateTimer() {
        // cancel timer for re-authenticating with the api
        clearTimeout(this.authenticateTimeout);
    }
}
 

Edit Account Component Template

Path: /src/app/home/edit-account.component.html

The edit account component template contains a form for updating the details of an account.

<h2>Edit Account</h2>
<p>Updating the information here will only change it inside this application, it won't (and can't) change anything in the associated Facebook account.</p>
<form *ngIf="account" [formGroup]="form" (ngSubmit)="onSubmit()">
    <div class="form-group">
        <label>Facebook Id</label>
        <div>{{account.facebookId}}</div>
    </div>
    <div class="form-group">
        <label>Name</label>
        <input type="text" formControlName="name" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.name.errors }" />
        <div *ngIf="submitted && f.name.errors" class="invalid-feedback">
            <div *ngIf="f.name.errors.required">Name is required</div>
        </div>
    </div>
    <div class="form-group">
        <label>Extra Info</label>
        <input type="text" formControlName="extraInfo" class="form-control" />
    </div>
    <div class="form-group">
        <button type="submit" [disabled]="loading" class="btn btn-primary">
            <span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span>
            Save
        </button>
        <a routerLink="../../" class="btn btn-link">Cancel</a>
        <div *ngIf="error" class="alert alert-danger mt-3 mb-0">{{error}}</div>
    </div>
</form>
<div *ngIf="!account" class="text-center p-3">
    <span class="spinner-border spinner-border-lg align-center"></span>
</div>
 

Edit Account Component

Path: /src/app/home/edit-account.component.ts

The edit account component enables updating the details of a selected account, the account service is called when the component initializes to get the account details (this.accountService.getById(id)) to pre-populate the field values.

On successful submit the account is updated by with the account service and the user is redirected back to the home page with the accounts list.

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';

import { AccountService } from '@app/_services';
import { Account } from '@app/_models';

@Component({ templateUrl: 'edit-account.component.html' })
export class EditAccountComponent implements OnInit {
    form: FormGroup;
    account: Account;
    loading = false;
    submitted = false;
    error = '';

    constructor(
        private formBuilder: FormBuilder,
        private route: ActivatedRoute,
        private router: Router,
        private accountService: AccountService
    ) { }

    ngOnInit() {
        this.form = this.formBuilder.group({
            name: ['', Validators.required],
            extraInfo: ['']
        });

        // get account and populate form
        const id = this.route.snapshot.params['id'];
        this.accountService.getById(id)
            .pipe(first())
            .subscribe(x => {
                this.account = x;
                this.form.patchValue(x);
            });
    }

    // convenience getter for easy access to form fields
    get f() { return this.form.controls; }

    onSubmit() {
        this.submitted = true;

        // stop here if form is invalid
        if (this.form.invalid) {
            return;
        }

        this.loading = true;
        this.error = '';
        this.accountService.update(this.account.id, this.form.value)
            .pipe(first())
            .subscribe({
                next: () => {
                    this.router.navigate(['../'], { relativeTo: this.route });
                },
                error: error => {
                    this.error = error;
                    this.loading = false;
                }
            });
    }
}
 

Home Component Template

Path: /src/app/home/home.component.html

The home component template contains a simple welcome message and a list of all accounts with buttons for editing or deleting.

<h2>You're logged in with Angular 10 & Facebook!!</h2>
<p>All accounts from secure api end point:</p>
<table class="table table-striped">
    <thead>
        <tr>
            <th>Id</th>
            <th>Facebook Id</th>
            <th>Name</th>
            <th>Extra Info</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        <tr *ngFor="let account of accounts">
            <td>{{account.id}}</td>
            <td>{{account.facebookId}}</td>
            <td>{{account.name}}</td>
            <td>{{account.extraInfo}}</td>
            <td class="text-right" style="white-space: nowrap">
                <a routerLink="edit/{{account.id}}" class="btn btn-sm btn-primary mr-1">Edit</a>
                <button (click)="deleteAccount(account.id)" class="btn btn-sm btn-danger btn-delete-account" [disabled]="account.isDeleting">
                    <span *ngIf="account.isDeleting" class="spinner-border spinner-border-sm"></span>
                    <span *ngIf="!account.isDeleting">Delete</span>
                </button>
            </td>
        </tr>
        <tr *ngIf="!accounts">
            <td colspan="5" class="text-center">
                <span class="spinner-border spinner-border-lg align-center"></span>
            </td>
        </tr>
    </tbody>
</table>
 

Home Component

Path: /src/app/home/home.component.ts

The home component gets all accounts from the account service in the ngOnInit() method and makes them available to the home template via the accounts property.

The deleteAccount() method sets the property account.isDeleting = true so the template displays a spinner on the delete button, then calls this.accountService.delete(id) to delete the account and removes the deleted account from component accounts array so it is removed from the UI.

import { Component } from '@angular/core';
import { first } from 'rxjs/operators';

import { AccountService } from '@app/_services';

@Component({ templateUrl: 'home.component.html' })
export class HomeComponent {
    accounts: any[];

    constructor(private accountService: AccountService) { }

    ngOnInit() {
        this.accountService.getAll()
            .pipe(first())
            .subscribe(accounts => this.accounts = accounts);
    }

    deleteAccount(id: string) {
        const account = this.accounts.find(x => x.id === id);
        account.isDeleting = true;
        this.accountService.delete(id)
            .pipe(first())
            .subscribe(() => this.accounts = this.accounts.filter(x => x.id !== id));
    }
}
 

Login Component Template

Path: /src/app/login/login.component.html

The login component template contains a single Facebook login button that is bound to the login() method of the login component on click.

<div class="col-md-6 offset-md-3 mt-5 text-center">
    <div class="card">
        <h4 class="card-header">Angular 10 Facebook Login Example</h4>
        <div class="card-body">
            <button class="btn btn-facebook" (click)="login()">
                <i class="fa fa-facebook mr-1"></i>
                Login with Facebook
            </button>
        </div>
    </div>
</div>
 

Login Component

Path: /src/app/login/login.component.ts

The login component uses the account service to login to the application using Facebook. If the user is already logged in they are automatically redirected to the home page.

import { Component } from '@angular/core';
import { Router } from '@angular/router';

import { AccountService } from '@app/_services';

@Component({ templateUrl: 'login.component.html' })
export class LoginComponent {
    constructor(
        private router: Router,
        private accountService: AccountService
    ) {
        // redirect to home if already logged in
        if (this.accountService.accountValue) {
            this.router.navigate(['/']);
        }
    }

    login() {
        this.accountService.login();
    }
}
 

App Routing Module

Path: /src/app/app-routing.module.ts

The app routing module defines the routes for the angular application and generates a root routing module by passing the array of routes to the RouterModule.forRoot() method. The module is imported into the main app module below.

The home route maps the root path (/) of the app to the home component, the edit account route /edit/:id maps to the edit account component, and the /login route maps to the login component.

The home and edit account routes are secured by passing the auth guard to the canActivate property of each route.

For more information on angular routing see https://angular.io/guide/router.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { HomeComponent, EditAccountComponent } from './home';
import { LoginComponent } from './login';
import { AuthGuard } from './_helpers';

const routes: Routes = [
    { path: '', component: HomeComponent, canActivate: [AuthGuard] },
    { path: 'edit/:id', component: EditAccountComponent, canActivate: [AuthGuard] },
    { path: 'login', component: LoginComponent },

    // otherwise redirect to home
    { path: '**', redirectTo: '' }
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule { }
 

App Component Template

Path: /src/app/app.component.html

The app component template is the root component template of the angular application, it contains the main nav bar which is only displayed for authenticated accounts, and a router-outlet directive displaying the contents of each view based on the current route / path.

<!-- nav -->
<nav class="navbar navbar-expand navbar-dark bg-dark" *ngIf="account">
    <div class="navbar-nav">
        <a class="nav-item nav-link" routerLink="/" routerLinkActive="active">Home</a>
        <a class="nav-item nav-link" (click)="logout()">Logout</a>
    </div>
</nav>

<!-- main app container -->
<div class="container pt-4">
    <router-outlet></router-outlet>
</div>
 

App Component

Path: /src/app/app.component.ts

The app component is the root component of the angular app, it defines the root tag of the app as <app></app> with the selector property of the @Component() decorator.

It subscribes to the account observable of the account service so it can reactively show/hide the main nav bar in the app component template when the user logs in/out of the application. I didn't worry about unsubscribing from the observable here because it's the root component of the application and will only be destroyed when the angular app is closed.

The logout() method is called from the logout link in the main nav bar to log the account out and redirect to the login page.

import { Component } from '@angular/core';

import { AccountService } from './_services';
import { Account } from './_models';

@Component({ selector: 'app', templateUrl: 'app.component.html' })
export class AppComponent {
    account: Account;

    constructor(private accountService: AccountService) {
        this.accountService.account.subscribe(x => this.account = x);
    }

    logout() {
        this.accountService.logout();
    }
}
 

App Module

Path: /src/app/app.module.ts

The app module defines the root module of the angular application along with metadata about the module. For more info about angular 10 modules see https://angular.io/docs/ts/latest/guide/ngmodule.html.

This is where the fake backend provider is added to the application, to switch to a real backend simply remove the fakeBackendProvider located below the comment // provider used to create fake backend.

import { NgModule, APP_INITIALIZER } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';

// used to create fake backend
import { fakeBackendProvider } from './_helpers';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { JwtInterceptor, ErrorInterceptor, appInitializer } from './_helpers';
import { AccountService } from './_services';
import { HomeComponent, EditAccountComponent } from './home';
import { LoginComponent } from './login';

@NgModule({
    imports: [
        BrowserModule,
        ReactiveFormsModule,
        HttpClientModule,
        AppRoutingModule
    ],
    declarations: [
        AppComponent,
        HomeComponent,
        EditAccountComponent,
        LoginComponent
    ],
    providers: [
        { provide: APP_INITIALIZER, useFactory: appInitializer, multi: true, deps: [AccountService] },
        { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
        { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },

        // provider used to create fake backend
        fakeBackendProvider
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }
 

Production Environment Config

Path: /src/environments/environment.prod.ts

The production environment config contains variables required to run the application in production. This enables you to build the application with a different configuration for each different environment (e.g. production & development) without updating the app code.

When you build the application for production with the command ng build --prod, the output environment.ts is replaced with environment.prod.ts.

export const environment = {
    production: true,
    apiUrl: 'http://localhost:4000',
    facebookAppId: '314930319788683'
};
 

Development Environment Config

Path: /src/environments/environment.ts

The development environment config contains variables required to run the app in development.

Environment config is accessed by importing the environment object into any Angular service of component with the line import { environment } from '@environments/environment' and accessing properties on the environment object, see the account service for an example.

export const environment = {
    production: false,
    apiUrl: 'http://localhost:4000',
    facebookAppId: '314930319788683'
};
 

Main Index Html File

Path: /src/index.html

The main index.html file is the initial page loaded by the browser that kicks everything off. The Angular CLI (with Webpack under the hood) bundles all of the compiled javascript files together and injects them into the body of the index.html page so the scripts can be loaded and executed by the browser.

<!DOCTYPE html>
<html>
<head>
    <base href="/" />
    <title>Angular 10 - Facebook Login Tutorial & Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- bootstrap & font-awesome css -->
    <link href="//netdna.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" />
    <link href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
</head>
<body>
    <app>Loading...</app>
</body>
</html>
 

Main (Bootstrap) File

Path: /src/main.ts

The main file is the entry point used by angular to launch and bootstrap the application.

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
    enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.error(err));
 

Polyfills

Path: /src/polyfills.ts

Some features used by Angular 10 are not yet supported natively by all major browsers, polyfills are used to add support for features where necessary so your Angular 10 application works across all major browsers.

This file is generated by the Angular CLI when creating a new project with the ng new command, I've excluded the comments in the file for brevity.

import 'zone.js/dist/zone';
 

Global LESS/CSS Styles

Path: /src/styles.less

The global styles file contains LESS/CSS styles that are applied globally throughout the angular 10 application.

a { cursor: pointer }

.btn-facebook {
    background: #3B5998;
    color: #fff;

    &:hover {
        color: #fff;
        opacity: 0.8;
    }
}

.btn-delete-account {
    width: 40px;
    text-align: center;
    box-sizing: content-box;
}
 

Package.json

Path: /package.json

The package.json file contains project configuration information including package dependencies that get installed when you run npm install and scripts that are executed when you run npm start or npm run build etc. Full documentation is available at https://docs.npmjs.com/files/package.json.

{
    "name": "angular-facebook-login-example",
    "version": "0.0.0",
    "scripts": {
        "ng": "ng",
        "start": "ng serve --open",
        "start:ssl": "ng serve --ssl --open",
        "build": "ng build",
        "test": "ng test",
        "lint": "ng lint",
        "e2e": "ng e2e"
    },
    "private": true,
    "dependencies": {
        "@angular/animations": "^10.1.0",
        "@angular/common": "^10.1.0",
        "@angular/compiler": "^10.1.0",
        "@angular/core": "^10.1.0",
        "@angular/forms": "^10.1.0",
        "@angular/platform-browser": "^10.1.0",
        "@angular/platform-browser-dynamic": "^10.1.0",
        "@angular/router": "^10.1.0",
        "@types/facebook-js-sdk": "^3.3.0",
        "rxjs": "^6.6.0",
        "tslib": "^2.0.0",
        "zone.js": "^0.10.2"
    },
    "devDependencies": {
        "@angular-devkit/build-angular": "^0.1001.0",
        "@angular/cli": "^10.1.0",
        "@angular/compiler-cli": "^10.1.0",
        "@types/node": "^12.11.1",
        "@types/jasmine": "^3.5.0",
        "@types/jasminewd2": "^2.0.3",
        "codelyzer": "^6.0.0",
        "jasmine-core": "^3.6.0",
        "jasmine-spec-reporter": "^5.0.0",
        "karma": "^5.0.0",
        "karma-chrome-launcher": "^3.1.0",
        "karma-coverage-istanbul-reporter": "^3.0.2",
        "karma-jasmine": "^4.0.0",
        "karma-jasmine-html-reporter": "^1.5.0",
        "protractor": "^7.0.0",
        "ts-node": "^8.3.0",
        "tslint": "^6.1.0",
        "typescript": "^4.0.2"
    }
}
 

TypeScript tsconfig.base.json

Path: /tsconfig.base.json

The tsconfig.base.json file contains the base TypeScript compiler configuration for all projects in the Angular workspace, it configures how the TypeScript code will be compiled / transpiled into JavaScript that is understood by the browser. For more info see https://angular.io/config/tsconfig.

Most of the file is unchanged from when it was generated by the Angular CLI, only the paths property has been added to map the @app and @environments aliases to the /src/app and /src/environments directories. This allows imports to be relative to the app and environments folders by prefixing import paths with aliases instead of having to use long relative paths (e.g. import MyComponent from '@app/MyComponent' instead of import MyComponent from '../../../MyComponent').

{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "es2015",
    "module": "es2020",
    "lib": [
      "es2018",
      "dom"
    ],
    "paths": {
      "@app/*": ["src/app/*"],
      "@environments/*": ["src/environments/*"]
    }
  }
}

 

Subscribe or Follow Me For Updates

Subscribe to my YouTube channel or follow me on Twitter or GitHub to be notified when I post new content.

 


Supported by