L08084のブログ

技術記事の執筆は祈りに似ている

【Angular】 送信後にフォームをリセットする

やりたいこと

フォームのSubmitに成功したら、フォームに入力した内容もバリデーションの状態もリセットしたい

環境

  • Angular@7.2.0
  • Angular Material@7.3.7

コーディング

NgForm.resetForm()を使う。

FormGroup.reset()でもフォームのリセットができそうにみえるが、FormGroup.reset()だとフォームの値のみをリセットするため、必須バリデーションなどに引っかかってしまう。

f:id:l08084:20191110190334p:plain
FormGroup.reset()だと必須バリデーションに引っかかる

それでは、NgForm.resetForm()で送信後にバリデーションと値をリセットする処理を書いていく。

まず、テンプレート(HTML)から...

<form class="example-form" (ngSubmit)="onSubmit(createNgForm)" [formGroup]="createFormGroup" #createNgForm="ngForm">
  <!-- タイトル欄 -->
  <mat-form-field class="example-full-width">
    <input matInput class="title" placeholder="Title" formControlName="title" />
  </mat-form-field>

  <!-- 詳細欄 -->
  <mat-form-field class="example-full-width">
    <textarea matInput cdkTextareaAutosize cdkAutosizeMinRows="10" cdkAutosizeMaxRows="30" placeholder="Discription"
      formControlName="description"></textarea>
  </mat-form-field>
  <button type="submit" class="login-button" mat-raised-button [disabled]="false" color="primary">Save</button>
</form>

重要なのは、(ngSubmit)="onSubmit(createNgForm)" [formGroup]="createFormGroup" #createNgForm="ngForm"の部分となる。

#createNgForm="ngForm"でngFormの参照変数を定義した後、その参照変数を(ngSubmit)="onSubmit(createNgForm)"でsubmitイベントの処理に渡しています。

続いて、Submitイベントで呼び出されるメソッドについて

  public onSubmit(form: NgForm) {
    // スピナーを表示する
    this.spinnerService.show();

    // ログインしているユーザ情報の取得
    const user = this.afAuth.auth.currentUser;

    // メモを新規作成する
    this.memo = {
      id: '',
      title: this.titleControl.value,
      description: this.descriptionControl.value,
      createdUser: user.uid,
      createdDate: firebase.firestore.FieldValue.serverTimestamp(),
      updatedDate: firebase.firestore.FieldValue.serverTimestamp()
    };
    this.afStore
      .collection('memos')
      .add(this.memo)
      .then(docRef => {
        this.memoCollection.doc(docRef.id).update({
          id: docRef.id
        });
        // フォームをリセットする
        form.resetForm();
      })
      .finally(() => {
        // スピナーを非表示にする
        this.spinnerService.hide();
      });
  }

メソッドの引数として受け取っているform: NgFormを使って、フォームの保存処理が成功したタイミングでform.resetForm();を呼び出すことによって、フォームの内容とバリデーションのリセットを行なっています。

動作確認

実装が終わったので実際に動かしてみます。

フォームにテキストを入力してsubmitボタンを押下する。

f:id:l08084:20191110192323p:plain
submitボタンを押下すると...

保存処理が成功したタイミングでバリデーションもフォームへの入力内容もリセットされる。

f:id:l08084:20191110192536p:plain
フォームがリセットされている

参考サイト

angular - Cleanest way to reset forms - Stack Overflow

Angularで入れ子(ネスト)のルーティング

やりたいこと

<router-outlet>をふたつ設置することで、ホーム画面にサイドメニューを作成します。

一つ目の<router-outlet>では、URLに応じて、ログイン画面・アカウント登録画面などを表示し、二つ目の<router-outlet>をホーム画面のサイドメニューに設置することで新規メモ画面とメモ一覧画面の表示を行います。

一つ目のルーティング基点

f:id:l08084:20191016194956p:plain
ログイン画面

f:id:l08084:20191016201259p:plain
アカウント登録画面
まず、一つ目の<router-outlet>を設置します。このルーティング基点は、ログイン画面とアカウント登録画面で使用します。

  • src/app/app.component.html
<!-- 全画面で使用するスピナー -->
<app-spinner></app-spinner>
<!-- 一つ目のルーティング基点 -->
<router-outlet></router-outlet>

続いて、ルーティングの構成ファイルとなります。URLが/loginだとログイン画面、URLが/sign-upだとアカウント登録画面を表示します。

  • src/app/app-routing.module.ts
const routes: Routes = [
  { path: '', redirectTo: '/login', pathMatch: 'full' },
  {
    path: 'login',
    component: LoginComponent,
    canActivate: [AuthenticatedGuard]
  },
  {
    path: 'sign-up',
    component: SignUpComponent,
    canActivate: [AuthenticatedGuard]
  },
// ...省略

二つ目のルーティング基点

二つ目の<router-outlet>はホーム画面のサイドメニューに設置します。

サイドメニューで新規メモリンクをクリックすると、ホーム画面のメインコンテンツ部分に新規メモ画面を表示します。

f:id:l08084:20191016200636p:plain
新規メモ画面

サイドメニューでメモ一覧リンクをクリックすると、ホーム画面のメインコンテンツ部分にメモ一覧画面を表示します。

f:id:l08084:20191016200928p:plain
メモ一覧画面

ルーティングの構成ファイル。path: 'home',のルーティング構成を追加しています。children: []に子のルーティングを設定することで、ネストしているルーティングを実現することができます

  • src/app/app-routing.module.ts
// ... 省略

const routes: Routes = [
  { path: '', redirectTo: '/login', pathMatch: 'full' },
  {
    path: 'login',
    component: LoginComponent,
    canActivate: [AuthenticatedGuard]
  },
  {
    path: 'sign-up',
    component: SignUpComponent,
    canActivate: [AuthenticatedGuard]
  },
  {
    path: 'home',
    component: HomeComponent,
    canActivate: [AuthenticationGuard],
    children: [
      { path: '', redirectTo: 'create', pathMatch: 'full' },
      {
        path: 'create',
        component: CreateComponent,
        canActivate: [AuthenticationGuard]
      },
      {
        path: 'list',
        component: ListComponent,
        canActivate: [AuthenticationGuard]
      }
    ]
  }
];
// ...省略

ネストしているルーティングを使用しているホーム画面のテンプレートファイルです。サイドメニューで選択したリンクをメインコンテンツに表示します。

  • src/app/home/home.component.html
<!-- ヘッダー -->
<app-header></app-header>

<mat-sidenav-container class="container">
  <!-- サイドメニュー -->
  <mat-sidenav mode="side" class="side-menu" opened>
    <mat-nav-list>
      <a mat-list-item [routerLink]="'./create'"> 新規メモ </a>
      <a mat-list-item [routerLink]="'./list'"> メモ一覧 </a>
    </mat-nav-list>
  </mat-sidenav>
  <!-- メイン コンテンツ -->
  <mat-sidenav-content class="main-contents">
    <router-outlet></router-outlet>
  </mat-sidenav-content>
</mat-sidenav-container>

参考サイト

Nested Routes • Angular

Angular Rooter (サブモジュールの使い方) - Qiita

【Angular】オブジェクトの中身を表示する JSON パイプ

「TypeScript(Angular)でオブジェクトの中身を画面に表示したいのに[object Object]って表示される。どうしたらいい?」みたいな質問を仕事中に受けたのでメモ。

オブジェクトをJSON文字列に変換する

質問を受けた時は、JavaScriptのメソッドであるJSON.stringifyを使ってオブジェクトをJSON文字列に変換すると表示できますよーみたいに回答したんですが、後から調べたら、オブジェクトをJSON文字列にして表示するJsonPipeというのがAngularにあるとのこと。

試してみます

  • JsonPipeを使わない場合
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
  // 中身を表示したいオブジェクト
  public member = {
    id: 1,
    name: '大久保綾乃',
    age: 35,
    job: '小学校の教師'
  };

  constructor() {}

  ngOnInit() {
  }
}
<div>{{ member}}</div>

上記の実装で画面を表示すると、オブジェクトの中身は表示されず[object Object]と表示されてしまいます。

f:id:l08084:20191012153835p:plain
オブジェクトの中身が表示されない

  • JsonPipeを使った場合

特に事前準備は必要なく、テンプレート上で[中身を表示したい object ] | pipeと記載してあげるだけでOKです。

<div>{{ member | json }}</div>

このようにJSONパイプを使ってあげると、オブジェクトの中身が表示されます。

f:id:l08084:20191012154422p:plain
オブジェクトの中身が表示される

バージョン情報

  • Angular v7.2.0

【iPhone】Touch IDで表示されるダイアログのスクショをとる

f:id:l08084:20191009205757j:plain

iPhoneのTouch IDで表示されるダイアログって、ホームボタンに触ると消えてしまうので、いつものやり方だとスクショが取れない....

こんな時のために(?)、ホームボタンと電源ボタンの同時押し以外でスクリーンショットを撮る方法について整理します

バージョン情報

  • iPhone 8 plus
  • iOS 13.1.2

ホームボタンの代わりにAssistiveTouchを使う

AssistiveTouchを使うことで、ホームボタンを押下せずにスクリーンショットを撮ることができます。

AssistiveTouchを使ったスクショの撮影方法について説明します。

  1. 「設定」>「アクセシビリティ」>「タッチ」の順に選択し、「AssistiveTouch」を選択してオンにします

  2. 「最上位メニューをカスタマイズ」を選択し、アイコンを「スクリーンショット」だけにします

  3. 画面に表示されているAssistiveTouchをタップ

この手順でAssistiveTouchを使ってスクリーンショットを撮ることができます

参考サイト

iPhone、iPad、iPod touch で AssistiveTouch を使う - Apple サポート

Angular Materialでサイドメニューを作る

f:id:l08084:20191005175516p:plain
初期状態

上記画像の画面に、Angular Materialを使ってサイドメニューを追加します。

前提

下記の対応についてはすでに完了しているものとします。

  • Angular CLIによるAngularプロジェクトの作成
  • Angular Material を使用できるようにするための事前準備
  • ヘッダーの作成

バージョン情報

  • Angular v7.2.0
  • firebase: v6.3.4
  • Angular Material v7.3.7

開発

AppModuleの設定をした後、サイドメニューとメインコンテンツのスペースを作成し、最後にサイドメニューらしくなるようにリンクを追加します。

AppModuleの設定

まず事前準備として、Angular Materialのサイドメニューの部品であるMatSidenavModuleをAppModuleのimports:[]に設定します。

  • app.module.ts
// add this!
import { MatSidenavModule } from '@angular/material/sidenav';
// ...省略

@NgModule({
  imports: [
    // add this!
    MatSidenavModule,
    // ...省略
  ],
  // ...省略
})
export class AppModule {}

サイドメニューの雛形を作成

Sidenavの使い方ですが、まずサイドコンテンツとメインコンテンツの両方を<<mat-sidenav-container>タグ内に配置する必要があります。(ヘッダーなどのサイドメニューと関係ない項目は外側に配置してください)

そしてサイドコンテンツとして表示したい項目を<mat-sidenav>タグの内側に、メインコンテンツとして表示したい項目については<mat-sidenav-content>の内側に配置する必要があります。

  • home.component.html
<!-- ヘッダー -->
<app-header></app-header>

<!-- add this! -->
<mat-sidenav-container class="container">
  <!-- サイドメニュー -->
  <mat-sidenav mode="side" class="side-menu" opened></mat-sidenav>
  <!-- メイン コンテンツ -->
  <mat-sidenav-content></mat-sidenav-content>
</mat-sidenav-container>
  • home.component.scss
.container {
  position: absolute;
  top: 64px;
  bottom: 0;
  left: 0;
  right: 0;

  .side-menu {
    width: 128px;
  }
}

上記のコードを書いた時点で画面がこのようになります。

背景色のせいでわかりづらいですが...左側の白いスペースがサイドメニューになる部分で、右側の灰色のスペースがメインコンテンツを表示する部分になります。

f:id:l08084:20191006215549p:plain
サイドメニューのフレームだけできた状態

ナビゲーション(メニュー)の表示

最後に、サイドメニューにリンクを追加します。(サンプルなので、リンク先は設定していません)

まずメニューの作成にAngular MaterialのMatListModuleを使うので、AppModuleのimports:[]に設定します。

import { MatListModule } from '@angular/material/list';

@NgModule({
  // ...省略
  imports: [
    MatListModule,
  ],
  //... 省略
})
export class AppModule {}

ナビゲーションリンクはサイドコンテンツとして表示したいので、<mat-sidenav>の内側に配置します。

  • home.component.html
<!-- ヘッダー -->
<app-header></app-header>

<mat-sidenav-container class="container">
  <!-- サイドメニュー -->
  <mat-sidenav mode="side" class="side-menu" opened>
    <!-- add this! -->
    <mat-nav-list>
      <a mat-list-item [routerLink]="''"> Accounts </a>
      <a mat-list-item [routerLink]="''"> Create Account </a>
      <a mat-list-item [routerLink]="''"> Contacts </a>
      <a mat-list-item [routerLink]="''"> Create Contact </a>
      <a mat-list-item [routerLink]="''"> Activities </a>
    </mat-nav-list>
  </mat-sidenav>
  <!-- メイン コンテンツ -->
  <mat-sidenav-content></mat-sidenav-content>
</mat-sidenav-container>

上記のコードを書いた時点で、下記の画像のように、サイドメニューにリンクが表示されるようになります。

f:id:l08084:20191008205428p:plain
サイドメニュー完成

参考サイト

[AngularMaterial] Sidenavの使い方を理解しよう! - まんくつ

【Angular】Guardで認証されていないアカウントをブロックする

やりたいこと

  • ログイン(認証)を突破していないアカウントが、ログイン・アカウント登録画面以外にアクセスできないようにしたい
    • 現状だとURL(/home)を入力すると認証されていないユーザーでもホーム画面にアクセスできてしまう
  • ログインしている状態のアカウントがログイン・アカウント登録画面にアクセスできないようにしたい

前提

下記の対応についてはすでに完了しているものとします(対応内容の詳細については過去記事をご参照ください)

  • Angular CLIによるAngularプロジェクトの作成
  • メールアドレスとパスワードを用いたFirebase認証
  • ログアウト機能の実装
  • ログイン・ホーム画面などの作成

Guardを作成する

Angular CLIでプロジェクトを作成している場合は、下記のコマンドでauthentication.guard.tsファイルが作成されます。

$ ng g guard authentication

CLIで作成された初期状態のauthentication.guard.tsです。

  • authentication.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class AuthenticationGuard implements CanActivate {
  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    return true;
  }
}

canActivateメソッドでは、Guardを設定したURLに対してのアクセスを許可する場合はtrueを、アクセスを許可しない場合はfalseを返すようにします。

ログインしてないアカウントをホーム画面に入れない

空になっているcanActivateメソッドに、ログインしていないアカウントをホーム画面に遷移させない処理を書いていきます。

  • authentication.guard.ts
import { Injectable } from '@angular/core';
import {
  CanActivate,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  UrlTree,
  Router
} from '@angular/router';
import { Observable } from 'rxjs';
import { AngularFireAuth } from '@angular/fire/auth';
import { take, map } from 'rxjs/operators';

/**
 * ログインしていないアカウントをログイン画面に遷移させる
 *
 * @export
 * @class AuthenticationGuard
 * @implements {CanActivate}
 */
@Injectable({
  providedIn: 'root'
})
export class AuthenticationGuard implements CanActivate {
  constructor(private afAuth: AngularFireAuth, private router: Router) {}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ):
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree>
    | boolean
    | UrlTree {
    return this.afAuth.user.pipe(
      take(1),
      map(user => {
        if (user != null) {
          // ログインしていた場合userにユーザーの情報が入る
          return true;
        } else {
          // ログインしていない場合はログイン画面に遷移する
          this.router.navigate(['/login']);
          return false;
        }
      })
    );
  }
}

AngularFireAuth.userObservable<User|null>を返すため、usernullならばログインしていない、userが設定されていればログインしている、といった判定をすることができます。

作成したAuthenticationGuardAppRoutingModuleに設定するとGuardの実装が完了です。

  • app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { LoginComponent } from './login/login.component';
import { SignUpComponent } from './sign-up/sign-up.component';
import { AuthenticationGuard } from './authentication.guard';

const routes: Routes = [
  { path: '', redirectTo: '/login', pathMatch: 'full' },
  { path: 'login', component: LoginComponent },
  {
    path: 'home',
    component: HomeComponent,
    canActivate: [AuthenticationGuard] // <- add this!
  },
  { path: 'sign-up', component: SignUpComponent }
];
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

ログイン済みのアカウントをログイン画面に入れない

AuthenticationGuardでやったことの逆をやります。まず、Angular CLIでGuardファイルを作成します。

ng g guard authenticated

作成されたauthenticated.guard.tsファイルを使って、ログイン済みのアカウントがログイン・アカウント登録画面に遷移するのを禁止する処理を実装します。

  • authenticated.guard.ts
import { Injectable } from '@angular/core';
import {
  CanActivate,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  UrlTree,
  Router
} from '@angular/router';
import { Observable } from 'rxjs';
import { AngularFireAuth } from '@angular/fire/auth';
import { take, map } from 'rxjs/operators';

/**
 * ログイン済のアカウントをログイン・アカウント登録画面に、
 * 遷移させない
 *
 * @export
 * @class AuthenticatedGuard
 * @implements {CanActivate}
 */
@Injectable({
  providedIn: 'root'
})
export class AuthenticatedGuard implements CanActivate {
  constructor(private afAuth: AngularFireAuth, private router: Router) {}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ):
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree>
    | boolean
    | UrlTree {
    return this.afAuth.user.pipe(
      take(1),
      map(user => {
        if (user != null) {
          // ログインしていた場合はホーム画面に遷移する
          this.router.navigate(['/home']);
          return false;
        } else {
          // ログインしていない場合は遷移を許可する
          return true;
        }
      })
    );
  }
}

今度はusernullの場合に、Observable型のtrueを返して画面への遷移を許可しています。反面、usernullでないログイン済のアカウントの遷移は許可せずにホーム画面に遷移させています。

作成したAuthenticatedGuardloginsign-upのルーティングに設定すると、認証済のアカウントによるログイン・アカウント登録画面への遷移を禁止することができます。

  • app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { LoginComponent } from './login/login.component';
import { SignUpComponent } from './sign-up/sign-up.component';
import { AuthenticationGuard } from './authentication.guard';
import { AuthenticatedGuard } from './authenticated.guard';

const routes: Routes = [
  { path: '', redirectTo: '/login', pathMatch: 'full' },
  {
    path: 'login',
    component: LoginComponent,
    canActivate: [AuthenticatedGuard] // <- add this!
  },
  {
    path: 'home',
    component: HomeComponent,
    canActivate: [AuthenticationGuard]
  },
  {
    path: 'sign-up',
    component: SignUpComponent,
    canActivate: [AuthenticatedGuard] // <- add this!
  }
];
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

動作確認

当初の仕様通りにアクセスをブロックできているか確認します。

未ログインのアカウントをホーム画面に遷移させない

ログインしていない状態から、不正にURLを書き換えて、ホーム画面に遷移しようとします。

f:id:l08084:20191002191401p:plain
URLを`login`から`home`に変える

ホーム画面に遷移せずに、ログイン画面に戻されました。成功です。

f:id:l08084:20191002191609p:plain
ホーム画面に遷移せずにログイン画面に戻る

ログイン済のアカウントをログイン画面に遷移させない

今度はログイン済のアカウントを使って、ホーム画面からログイン画面への遷移を試みます。

f:id:l08084:20191002192137p:plain
URLを`home`から`login`に変える

URLをloginに書き換えて不正アクセスを試みますが、失敗します。こちらも成功です。

f:id:l08084:20191002192432p:plain
ガードされてホーム画面に戻る

バージョン情報

  • Angular v7.2.0
  • firebase: v6.3.4
  • Angular Material v7.3.7

参考サイト

angularfire2/getting-started.md at master · angular/angularfire2 · GitHub

【Angular】Firebase認証からログアウトする

AngularとFirebaseを使ったログイン処理については、下記の記事ですでに実装済みのため、今回はログアウト処理を実装していきます。

関連記事

AngularでFirebase認証(その1) Firebaseのセットアップ - L08084のブログ

AngularでFirebase認証(その2) Angular Materialを使ったログイン画面の作成 - L08084のブログ

AngularでFirebase認証(その3) Firebase Authentication の呼び出し - L08084のブログ

バージョン情報

CSSフレームワークとして、Angular Materialを使用しています。

  • Angular v7.2.0
  • firebase: v6.3.4
  • Angular Material v7.3.7

実装

すでに作成済みのヘッダーにログアウトボタンをつけた後に、そのボタンから呼び出されるサインアウト処理を実装します。

ヘッダーにログアウトボタンをつける

f:id:l08084:20190929194130p:plain
ヘッダーにログアウトボタン(右端)が表示されている
Material Iconexit_to_appアイコンを使ってログアウトボタンを作成します。

  • header.component.html
<mat-toolbar color="primary">
  <div class="main">
    <mat-icon class="header-icon">
      note
    </mat-icon>
    <span class="title">CONCEPT</span>
    <!-- ログアウトボタン -->
    <mat-icon (click)="signOut()" *ngIf="isSigningIn" class="right-icon">exit_to_app</mat-icon>
  </div>
</mat-toolbar>

*ngIf="isSigningIn"でログアウトボタンの表示・非表示の制御を、(click)="signOut()"でクリックイベント時にサインアウト処理を呼び出すように設定しています。

    <!-- ログアウトボタン -->
    <mat-icon (click)="signOut()" *ngIf="isSigningIn" class="right-icon">exit_to_app</mat-icon>

ヘッダーのSCSSファイル、.right-iconでログアウトボタンをヘッダーの右端に表示するように設定しています。

  • header.component.scss
mat-toolbar {
  .main {
    display: flex;
    align-items: center;
    width: 100%;

    .title {
      margin-left: 12px;
      font-size: 21px;
    }

    .header-icon {
      font-size: 24px;
    }

    // ログアウトボタンを右寄せにする
    .right-icon {
      margin: 0 0 0 auto;
    }
  }
}

ログアウト処理を実装する

ログアウトボタンのレイアウトについては実装できたので、肝心のロジックの部分を実装していきます。

ヘッダーのコンポーネントクラスです。

  • header.component.ts
import { Component, OnInit, Input } from '@angular/core';
import { SpinnerService } from 'src/app/services/spinner.service';
import { Router } from '@angular/router';
import { AngularFireAuth } from '@angular/fire/auth';

/**
 * ヘッダーのコンポーネントクラス
 *
 * @export
 * @class HeaderComponent
 * @implements {OnInit}
 */
@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.scss']
})
export class HeaderComponent implements OnInit {
  @Input() isSigningIn: boolean;

  constructor(
    private afAuth: AngularFireAuth,
    private router: Router,
    private spinnerService: SpinnerService
  ) {}

  ngOnInit() {}

  /**
   * ログアウト処理
   *
   * @memberof HeaderComponent
   */
  public signOut(): void {
    // スピナー表示
    this.spinnerService.show();
    // ログアウトAPIを呼び出す
    this.afAuth.auth
      .signOut()
      .then(() => {
        // ログアウトが成功したら、ログイン画面に遷移
        this.router.navigate(['/login']);
      })
      .catch(error => console.log(error))
      // 一連の処理が完了したらスピナーを消す
      .finally(() => this.spinnerService.hide());
  }
}

ログアウト処理の実装部分の抜粋です。

まず最初にスピナーの表示を実行し、AngularFireAuthライブラリを使って、Firebase AuthenticationのログアウトAPIを呼び出しています。
ログアウトAPIの処理が成功したら、Angular Routerによるルーティング処理でログイン画面に遷移します。その後、finally内でスピナーの非表示を実行します。

  public signOut(): void {
    // スピナー表示
    this.spinnerService.show();
    // ログアウトAPIを呼び出す
    this.afAuth.auth
      .signOut()
      .then(() => {
        // ログアウトが成功したら、ログイン画面に遷移
        this.router.navigate(['/login']);
      })
      .catch(error => console.log(error))
      // 一連の処理が完了したらスピナーを消す
      .finally(() => this.spinnerService.hide());
  }

これで実装は完了です。

動作確認

作成したログアウトボタンの動作を確認します。

f:id:l08084:20190929201033p:plain
ログアウトボタンを押下する

ログインした後に、ヘッダーのログアウトボタンをクリックすると....

f:id:l08084:20190929201214p:plain
ログアウトするとログイン画面に戻る

ログアウトした後、ログイン画面に戻ります。