中安拓也のブログ

プログラミングについて書くブログ

Angular Materialでモーダルとトースト使えるの知ってた???

はじめに

ずっとないと思い込んでいたけど.....モーダルとトースト普通にあるんすね、Angular Material

ということでCSSフレームワークのAngular Materialを使ってモーダルとトーストを実装していきます

環境

本記事における実行環境と使用ライブラリについて

$ ng version

Angular CLI: 8.3.20
Node: 12.13.1
OS: darwin x64
Angular: 8.2.14
... animations, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.803.20
@angular-devkit/build-angular     0.803.20
@angular-devkit/build-optimizer   0.803.20
@angular-devkit/build-webpack     0.803.20
@angular-devkit/core              8.3.20
@angular-devkit/schematics        8.3.20
@angular/cdk                      7.3.7
@angular/cli                      8.3.20
@angular/fire                     5.2.3
@angular/material                 7.3.7
@ngtools/webpack                  8.3.20
@schematics/angular               8.3.20
@schematics/update                0.803.20
rxjs                              6.5.3
typescript                        3.5.3
webpack                           4.39.2

実装

作成中のメモアプリにAngular Materialを使用して、下記2点の機能を実装します。

  • モーダル
  • トースト

モーダル

Angular MaterialのDialogを使って、 リンクをクリックするとモーダルでフォルダの新規作成のフォームを表示する機能を作成します。

f:id:l08084:20191212204225p:plain
新規フォルダ リンクをクリックすると...

f:id:l08084:20191212204423p:plain
フォルダ作成モーダルが表示される

AppModuleの設定1

モーダルを使用する事前準備として、AppModuleのimports:[]MatDialogModuleを追加します。

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

import { MatDialogModule } from '@angular/material/dialog';

@NgModule({
  //... 省略
  imports: [
    MatDialogModule,
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}
モーダルのコンポーネントを作成

モーダルのコンポーネントを作成していきます。

まずは、コンポーネントクラスのファイルを作成します。

  • src/app/component/folder-create-modal/folder-create-modal.component.ts
import { Component, OnInit } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';

/**
 * フォルダ新規作成モーダルのコンポーネントクラス
 *
 * @export
 * @class FolderCreateModalComponent
 * @implements {OnInit}
 */
@Component({
  selector: 'app-folder-create-modal',
  templateUrl: './folder-create-modal.component.html',
  styleUrls: ['./folder-create-modal.component.scss']
})
export class FolderCreateModalComponent implements OnInit {
  // モーダルへの参照をDI
  constructor(public dialogRef: MatDialogRef<FolderCreateModalComponent>) {}

  ngOnInit() {}

  /**
   * モーダルの作成ボタン押下時に呼び出し
   *
   * @memberof FolderCreateModalComponent
   */
  public onOkClick(): void {
    // モーダルを閉じる
    this.dialogRef.close();
    // TODO: フォルダ作成処理追加
  }

  /**
   * モーダルのキャンセルボタン押下時に呼び出し
   *
   * @memberof FolderCreateModalComponent
   */
  public onNoClick(): void {
    // モーダルを閉じる
    this.dialogRef.close();
  }
}

constructor(public dialogRef: MatDialogRef<FolderCreateModalComponent>) {}でモーダルへの参照変数をDIしています。このモーダルへの参照変数を使用して、ボタン押下時にモーダルを閉じる処理を実装しています。

続いて、モーダルのテンプレートファイル(HTML)です。

mat-dialog-actions align="end"でモーダルに表示するボタンを右寄せにしています。

  • src/app/component/folder-create-modal/folder-create-modal.component.html
<h1 mat-dialog-title>新規フォルダを作成</h1>
<div mat-dialog-content>
  <p>フォルダは、同じテーマや目的をもつメモをまとめて整理するのに役立ちます。</p>
  <mat-form-field class="folder">
    <input matInput class="title" placeholder="名前" />
  </mat-form-field>
</div>
<div mat-dialog-actions align="end">
  <button mat-button (click)="onNoClick()">キャンセル</button>
  <button mat-button cdkFocusInitial (click)="onOkClick()">作成</button>
</div>

モーダルのレイアウトを設定しているSCSSファイルです。特に特記事項がないため、このファイルについては説明をスキップします。

  • src/app/component/folder-create-modal/folder-create-modal.component.scss
.folder {
  width: 100%;
}
モーダルを表示する側の実装

モーダルを表示するリンクを実装するコンポーネントを作成します。

  • src/app/component/forder-list-header/forder-list-header.component.ts
import { MatDialog } from '@angular/material/dialog';
// ...省略

export class ForderListHeaderComponent implements OnInit {
  // MatDialogをDI
  constructor(public dialog: MatDialog) {}

  /**
   * フォルダの新規作成モーダルを表示する
   *
   * @memberof ForderListHeaderComponent
   */
  public create() {
    console.log('create');
    const dialogRef = this.dialog.open(FolderCreateModalComponent, {
      width: '360px'
    });

    dialogRef.afterClosed().subscribe(() => {
      console.log('The dialog was closed');
    });
  }
}

フォルダ新規作成リンクをクリックした時にcreate()メソッドを呼び出して、Dialogのopenでモーダルを表示しています。

なお、モーダルを表示する機能を実装しているコンポーネントであるため、constructorMatDialogをDIする必要があります。

モーダルを表示する側のコンポーネントのテンプレートファイルです。新規フォルダリンクを実装しています。

  • src/app/component/forder-list-header/forder-list-header.component.html
<div class="header">
  <div class="title">フォルダ</div>
  <a (click)="create()" class="add-link">
    <mat-icon class="icon">create_new_folder</mat-icon>
    <span>新規フォルダ</span>
  </a>
</div>
AppModuleの設定2

最後に、AppModuleのentryComponents:[]に今回作成したモーダルのコンポーネントであるFolderCreateModalComponentを追加してモーダルの実装は完了です。

  • src/app/app.module.ts
// ...省略
import { FolderCreateModalComponent } from './component/folder-create-modal/folder-create-modal.component';

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

トースト

Angular MaterialのSnackbarを使って、 ログアウトが成功した時にログアウトが完了した旨をトーストでユーザーに知らせる機能を作成します。

f:id:l08084:20191213185820p:plain
ログアウトに成功した旨をトーストで知らせる

AppModuleの設定

モーダルを使用する事前準備として、AppModuleのimports:[]MatSnackBarModuleを追加します。

  • src/app/app.module.ts
// ...省略
import { MatSnackBarModule } from '@angular/material/snack-bar';

@NgModule({
  // ...省略
  imports: [
    MatSnackBarModule
  ],
})
export class AppModule {}
トースト用のサービスクラスを作成

トーストを表示するためのサービスクラスを作成します。もちろんサービスを作成せずにトーストを表示したいコンポーネントに直接処理を描いても問題ありません。

  • src/app/services/toast.service.ts
import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';

/**
 * トーストの表示を制御するサービス
 *
 * @export
 * @class ToastService
 */
@Injectable({
  providedIn: 'root'
})
export class ToastService {
  // tslint:disable-next-line:variable-name
  private _actionMessage = 'Close';

  // tslint:disable-next-line:variable-name
  constructor(private _snackBar: MatSnackBar) {}

  /**
   * トーストを表示する
   *
   * @param {string} message トーストに表示するメッセージ
   * @memberof ToastService
   */
  public open(message: string): void {
    // 2000ミリ秒間トーストを表示する
    this._snackBar.open(message, this._actionMessage, {
      duration: 2000
    });
  }
}

openメソッドでトーストを表示しています。なお、MatSnackBarをDIする必要があります。

サービスクラスを経由してトーストを表示

最後に作成したToastServiceをトーストを表示したいコンポーネントで呼び出す処理を書いてあげれば完了です。

  • src/app/component/header/header.component.ts
import { ToastService } from '../../services/toast.service';

// ...省略
export class HeaderComponent implements OnInit {
  constructor(
    public afAuth: AngularFireAuth,
    private router: Router,
    private _toastService: ToastService,
    private spinnerService: SpinnerService
  ) {}

  /**
   * ログアウト処理
   *
   * @memberof HeaderComponent
   */
  public signOut(): void {
    this.spinnerService.show();
    this.afAuth.auth
      .signOut()
      .then(() => {
        this.router.navigate(['/login']);
        // トーストを表示する
        this._toastService.open('ログアウトしました。');
      })
      .catch(error => {
        // トーストを表示する
        this._toastService.open('ログアウトに失敗しました。');
        console.log(error);
      })
      // 一連の処理が完了したらスピナーを消す
      .finally(() => this.spinnerService.hide());
  }
}