1. はじめに
最終更新日: 2020 年 9 月 11 日
作成するアプリの概要
この Codelab では、Angular と Firebase を使用してウェブカンバンを作成します。完成したアプリには、バックログ、進行中、完了済みという 3 つのカテゴリがあります。タスクの作成、削除、ドラッグ&ドロップによるカテゴリ間での移動を行うことができます。
Angular はユーザー インターフェースの開発に使用し、Firestore は永続ストアとして使用します。Codelab の最後では、Angular CLI を使用してアプリを Firebase Hosting にデプロイします。
学習する内容
- Angular マテリアルと CDK を使用する方法
- Angular アプリに Firebase 統合を追加する方法
- Firestore で永続データを保持する方法
- 単一のコマンドで Angular CLI を使用し、アプリを Firebase Hosting にデプロイする方法
必要なもの
この Codelab は、Google アカウントを持ち、Angular と Angular CLI の基本的な知識があることを前提としています。
では始めましょう。
2. 新しいプロジェクトを作成する
まず、新しい Angular ワークスペースを作成します。
ng new kanban-fire
? Would you like to add Angular routing? No
? Which stylesheet format would you like to use? CSS
このステップは数分かかることがあります。Angular CLI は、プロジェクト構造を作成し、すべての依存関係をインストールします。インストール プロセスが完了したら、kanban-fire
ディレクトリに移動し、Angular CLI の開発サーバーを起動します。
ng serve
http://localhost:4200 を開くと、次のような出力が表示されます。
エディタで src/app/app.component.html
を開き、その内容をすべて削除します。http://localhost:4200 に戻ると、空白ページが表示されます。
3. マテリアルと CDK を追加する
Angular には、@angular/material
パッケージの一部として、マテリアル デザインに対応したユーザー インターフェース コンポーネントの実装が付属しています。@angular/material
の依存関係の一つに、コンポーネント開発キット(CDK)があります。CDK は、ユーザー補助ユーティリティ、ドラッグ&ドロップ、オーバーレイなどのプリミティブを提供します。Google は @angular/cdk
パッケージで CDK を配布しています。
アプリにマテリアルを追加するには、次のコマンドを実行します。
ng add @angular/material
このコマンドは、グローバルなマテリアル タイポグラフィ スタイルを使用したい場合と Angular マテリアルにブラウザ アニメーションを設定したい場合に、テーマを選択するよう求めます。この Codelab と同じ結果を得るために「Indigo/Pink」を選択し、最後の 2 つの質問に「Yes」と回答してください。
ng add
コマンドにより、@angular/material
とその依存関係がインストールされ、AppModule
で BrowserAnimationsModule
がインポートされます。次のステップとして、このモジュールが提供するコンポーネントの使用を開始できます。
まず、ツールバーとアイコンを AppComponent
に追加しましょう。app.component.html
を開き、次のマークアップを追加します。
src/app/app.component.html
<mat-toolbar color="primary">
<mat-icon>local_fire_department</mat-icon>
<span>Kanban Fire</span>
</mat-toolbar>
ここでは、マテリアル デザイン テーマのメインカラーを使用したツールバーを追加し、その中の「Kanban Fire」というラベルの横で local_fire_depeartment
アイコンを使用しています。コンソールを見ると、Angular がいくつかのエラーをスローしていることがわかります。これらを修正するには、AppModule
に次のインポートを追加します。
src/app/app.module.ts
...
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatIconModule } from '@angular/material/icon';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatToolbarModule,
MatIconModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Angular マテリアルのツールバーとアイコンを使用するので、AppModule
で対応するモジュールをインポートする必要があります。
次のような画面が表示されます。
たった 4 行の HTML と 2 つのインポートにしては、悪くない結果です。
4. タスクを視覚化する
次のステップとして、カンバンボードでタスクを視覚化するために使用できるコンポーネントを作成しましょう。
src/app
ディレクトリに移動して、次の CLI コマンドを実行します。
ng generate component task
このコマンドによって、TaskComponent
が生成され、その宣言が AppModule
に追加されます。task
ディレクトリ内に task.ts
という名前のファイルを作成します。このファイルを使用して、カンバンボードにタスクのインターフェースを定義します。各タスクには、オプションの id
、title
、および description
フィールド(すべて文字列型)があります。
src/app/task/task.ts
export interface Task {
id?: string;
title: string;
description: string;
}
ここで、task.component.ts
を更新しましょう。TaskComponent
が Task
型のオブジェクトを入力として受け取り、それを「edit
」の出力として生成するようにします。
src/app/task/task.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Task } from './task';
@Component({
selector: 'app-task',
templateUrl: './task.component.html',
styleUrls: ['./task.component.css']
})
export class TaskComponent {
@Input() task: Task | null = null;
@Output() edit = new EventEmitter<Task>();
}
TaskComponent
のテンプレートを編集します。task.component.html
を開き、その内容を次の HTML で置き換えます。
src/app/task/task.component.html
<mat-card class="item" *ngIf="task" (dblclick)="edit.emit(task)">
<h2>{{ task.title }}</h2>
<p>
{{ task.description }}
</p>
</mat-card>
コンソールにエラーが表示されることに注意してください。
'mat-card' is not a known element:
1. If 'mat-card' is an Angular component, then verify that it is part of this module.
2. If 'mat-card' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.ng
上記のテンプレートでは、@angular/material
からの mat-card
コンポーネントを使用していますが、対応するモジュールをアプリにインポートしていません。上記のエラーを修正するには、AppModule
で MatCardModule
をインポートする必要があります。
src/app/app.module.ts
...
import { MatCardModule } from '@angular/material/card';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatCardModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
次に、AppComponent
にいくつかのタスクを作成し、TaskComponent
を使用してそれらを視覚化します。
AppComponent
で、todo
という名前の配列を定義し、その中に次の 2 つのタスクを追加します。
src/app/app.component.ts
...
import { Task } from './task/task';
@Component(...)
export class AppComponent {
todo: Task[] = [
{
title: 'Buy milk',
description: 'Go to the store and buy milk'
},
{
title: 'Create a Kanban app',
description: 'Using Firebase and Angular create a Kanban app!'
}
];
}
ここで、app.component.html
の末尾に次の *ngFor
ディレクティブを追加します。
src/app/app.component.html
<app-task *ngFor="let task of todo" [task]="task"></app-task>
ブラウザを開くと、次のような画面が表示されます。
5. タスクのドラッグ&ドロップを実装する
いよいよ興味深いパートに入ります。タスクが取りうる 3 種類の状態を表す 3 つのスイムレーンを作成し、Angular CDK を使用してドラッグ&ドロップ機能を実装します。
app.component.html
で、*ngFor
ディレクティブを含む一番上の app-task
コンポーネントを削除し、次のコードで置き換えます。
src/app/app.component.html
<div class="container-wrapper">
<div class="container">
<h2>Backlog</h2>
<mat-card
cdkDropList
id="todo"
#todoList="cdkDropList"
[cdkDropListData]="todo"
[cdkDropListConnectedTo]="[doneList, inProgressList]"
(cdkDropListDropped)="drop($event)"
class="list">
<p class="empty-label" *ngIf="todo.length === 0">Empty list</p>
<app-task (edit)="editTask('todo', $event)" *ngFor="let task of todo" cdkDrag [task]="task"></app-task>
</mat-card>
</div>
<div class="container">
<h2>In progress</h2>
<mat-card
cdkDropList
id="inProgress"
#inProgressList="cdkDropList"
[cdkDropListData]="inProgress"
[cdkDropListConnectedTo]="[todoList, doneList]"
(cdkDropListDropped)="drop($event)"
class="list">
<p class="empty-label" *ngIf="inProgress.length === 0">Empty list</p>
<app-task (edit)="editTask('inProgress', $event)" *ngFor="let task of inProgress" cdkDrag [task]="task"></app-task>
</mat-card>
</div>
<div class="container">
<h2>Done</h2>
<mat-card
cdkDropList
id="done"
#doneList="cdkDropList"
[cdkDropListData]="done"
[cdkDropListConnectedTo]="[todoList, inProgressList]"
(cdkDropListDropped)="drop($event)"
class="list">
<p class="empty-label" *ngIf="done.length === 0">Empty list</p>
<app-task (edit)="editTask('done', $event)" *ngFor="let task of done" cdkDrag [task]="task"></app-task>
</mat-card>
</div>
</div>
ここでは多くのことが行われています。このスニペットの個々の部分を順に見ていきましょう。テンプレートの最上位の構造は次のとおりです。
src/app/app.component.html
...
<div class="container-wrapper">
<div class="container">
<h2>Backlog</h2>
...
</div>
<div class="container">
<h2>In progress</h2>
...
</div>
<div class="container">
<h2>Done</h2>
...
</div>
</div>
ここでは、「container-wrapper
」という名前のクラスを使用して、3 つのスイムレーンをすべてラップする div
を作成しています。各スイムレーンのクラス名は「container
」で、h2
タグ内にタイトルが含まれています。
それでは、1 つ目のスイムレーンの構造を見てみましょう。
src/app/app.component.html
...
<div class="container">
<h2>Backlog</h2>
<mat-card
cdkDropList
id="todo"
#todoList="cdkDropList"
[cdkDropListData]="todo"
[cdkDropListConnectedTo]="[doneList, inProgressList]"
(cdkDropListDropped)="drop($event)"
class="list"
>
<p class="empty-label" *ngIf="todo.length === 0">Empty list</p>
<app-task (edit)="editTask('todo', $event)" *ngFor="let task of todo" cdkDrag [task]="task"></app-task>
</mat-card>
</div>
...
まず、スイムレーンを mat-card
として定義します。これは cdkDropList
ディレクティブを使用します。mat-card
を使用する理由は、このコンポーネントが提供するスタイルにあります。cdkDropList
は、後で要素内にタスクをドロップするために使用できます。次の 2 つの入力も設定します。
cdkDropListData
- データ配列を指定できるドロップリストの入力cdkDropListConnectedTo
- 現在のcdkDropList
の接続先である他のcdkDropList
への参照。この入力を設定することにより、アイテムをドロップできる他のリストを指定します
さらに、cdkDropListDropped
出力を使用してドロップ イベントを処理します。cdkDropList
がこの出力を生成したら、AppComponent
内で宣言されている drop
メソッドを呼び出し、現在のイベントを引数として渡します。
このコンテナの識別子として使用する id
と、スタイルを設定するための class
名も指定している点に注意してください。次に、mat-card
の子のコンテンツを見てみましょう。次の 2 つの要素があります。
todo
リストにアイテムがないときに「Empty list」というテキストを表示するために使用する段落。app-task
コンポーネント。リスト名と$event
オブジェクトを使用してeditTask
メソッドを呼び出すことにより、最初に宣言したedit
出力を処理している点に注意してください。これは、正しいリストからの編集済みタスクを置き換えるのに役立ちます。次に、上記の処理と同様にtodo
リストを反復処理し、task
入力を渡します。ただし、今回はcdkDrag
ディレクティブも追加します。これにより、個々のタスクがドラッグ可能になります。
これらをすべて機能させるには、app.module.ts
を更新し、DragDropModule
のインポートを含める必要があります。
src/app/app.module.ts
...
import { DragDropModule } from '@angular/cdk/drag-drop';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
DragDropModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
また、inProgress
配列と done
配列を、editTask
および drop
メソッドとともに宣言する必要があります。
src/app/app.component.ts
...
import { CdkDragDrop, transferArrayItem } from '@angular/cdk/drag-drop';
@Component(...)
export class AppComponent {
todo: Task[] = [...];
inProgress: Task[] = [];
done: Task[] = [];
editTask(list: string, task: Task): void {}
drop(event: CdkDragDrop<Task[]|null>): void {
if (event.previousContainer === event.container) {
return;
}
if (!event.container.data || !event.previousContainer.data) {
return;
}
transferArrayItem(
event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex
);
}
}
drop
メソッドで、タスクの移動元と同じリストにドロップしているかどうかを最初にチェックしている点に注意してください。ドロップしている場合は、何もせずに返します。ドロップしていない場合は、現在のタスクを目的のスイムレーンに移動します。
結果は次のようになります。
以上により、2 つのリスト間でアイテムを移動できるようになりました。
6 新しいタスクを作成する
次に、新しいタスクを作成する機能を実装しましょう。そのためには、AppComponent
のテンプレートを更新します。
src/app/app.component.html
<mat-toolbar color="primary">
...
</mat-toolbar>
<div class="content-wrapper">
<button (click)="newTask()" mat-button>
<mat-icon>add</mat-icon> Add Task
</button>
<div class="container-wrapper">
<div class="container">
...
</div>
</div>
container-wrapper
を囲む最上位の div
要素を作成し、「Add Task」というラベルの横に「add
」マテリアル アイコンを備えたボタンを追加します。スイムレーンのリストの上に、ボタンを配置する追加のラッパーが必要です。スイムレーンは、後で Flexbox を使用して隣り合わせに配置します。このボタンはマテリアル ボタン コンポーネントを使用するので、AppModule
で対応するモジュールをインポートする必要があります。
src/app/app.module.ts
...
import { MatButtonModule } from '@angular/material/button';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatButtonModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
次に、AppComponent
にタスクを追加する機能を実装しましょう。マテリアル ダイアログを使用します。このダイアログには、タイトルと説明の 2 つのフィールドを含むフォームがあります。ユーザーが [Add Task] ボタンをクリックしたときは、ダイアログを開きます。ユーザーがフォームを送信したときは、新しく作成されたタスクを todo
リストに追加します。
この機能を AppComponent
に実装する方法の概要を見てみましょう。
src/app/app.component.ts
...
import { MatDialog } from '@angular/material/dialog';
@Component(...)
export class AppComponent {
...
constructor(private dialog: MatDialog) {}
newTask(): void {
const dialogRef = this.dialog.open(TaskDialogComponent, {
width: '270px',
data: {
task: {},
},
});
dialogRef
.afterClosed()
.subscribe((result: TaskDialogResult|undefined) => {
if (!result) {
return;
}
this.todo.push(result.task);
});
}
}
MatDialog
クラスを注入するコンストラクタを宣言します。newTask
内で、次の処理を行います。
- 少し後で定義する
TaskDialogComponent
を使用して新しいダイアログを開きます。 - ダイアログの幅を
270px.
に指定します。 - 空のタスクをデータとしてダイアログに渡します。
TaskDialogComponent
内で、このデータ オブジェクトへの参照を取得できます。 - 「閉じる」イベントをサブスクライブ登録し、
result
オブジェクトから取得したタスクをtodo
配列に追加します。
これを機能させるには、まず AppModule
で MatDialogModule
をインポートする必要があります。
src/app/app.module.ts
...
import { MatDialogModule } from '@angular/material/dialog';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatDialogModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
それでは、TaskDialogComponent
を作成しましょう。src/app
ディレクトリに移動して、次のコマンドを実行します。
ng generate component task-dialog
この機能を実装するには、まず src/app/task-dialog/task-dialog.component.html
を開き、次の内容で置き換えます。
src/app/task-dialog/task-dialog.component.html
<mat-form-field>
<mat-label>Title</mat-label>
<input matInput cdkFocusInitial [(ngModel)]="data.task.title" />
</mat-form-field>
<mat-form-field>
<mat-label>Description</mat-label>
<textarea matInput [(ngModel)]="data.task.description"></textarea>
</mat-form-field>
<div mat-dialog-actions>
<button mat-button [mat-dialog-close]="{ task: data.task }">OK</button>
<button mat-button (click)="cancel()">Cancel</button>
</div>
上記のテンプレートで、title
と description
の 2 つのフィールドを含むフォームを作成します。cdkFocusInput
ディレクティブを使用して、ユーザーがダイアログを開いたときは title
入力を自動的にフォーカスします。
テンプレート内でどのようにコンポーネントの data
プロパティを参照しているかに注意してください。これは、AppComponent
で dialog
の open
メソッドに渡す data
と同じです。対応するフィールドの内容をユーザーが変更したときにタスクのタイトルと説明を更新するには、ngModel
との双方向データ バインディングを使用します。
ユーザーが [OK] ボタンをクリックしたときは、{ task: data.task }
という結果を自動的に返します。これは、上記のテンプレートのフォーム フィールドを使用して変更したタスクです。
次に、コンポーネントのコントローラを実装しましょう。
src/app/task-dialog/task-dialog.component.ts
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Task } from '../task/task';
@Component({
selector: 'app-task-dialog',
templateUrl: './task-dialog.component.html',
styleUrls: ['./task-dialog.component.css'],
})
export class TaskDialogComponent {
private backupTask: Partial<Task> = { ...this.data.task };
constructor(
public dialogRef: MatDialogRef<TaskDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: TaskDialogData
) {}
cancel(): void {
this.data.task.title = this.backupTask.title;
this.data.task.description = this.backupTask.description;
this.dialogRef.close(this.data);
}
}
TaskDialogComponent
でダイアログへの参照を注入して、ダイアログを閉じることができるようにします。また、MAT_DIALOG_DATA
トークンに関連付けられたプロバイダの値も挿入します。これは、上記の AppComponent
の open メソッドに渡したデータ オブジェクトです。また、プライベート プロパティ backupTask
も宣言します。これは、データ オブジェクトと一緒に渡したタスクのコピーです。
ユーザーが [Cancel] ボタンを押したときは、this.data.task
の変更された可能性のあるプロパティを復元してダイアログを閉じ、this.data
を結果として渡します。
参照したものの、まだ宣言していない 2 つの型(TaskDialogData
と TaskDialogResult
)があります。src/app/task-dialog/task-dialog.component.ts
内で、ファイルの末尾に次の宣言を追加します。
src/app/task-dialog/task-dialog.component.ts
...
export interface TaskDialogData {
task: Partial<Task>;
enableDelete: boolean;
}
export interface TaskDialogResult {
task: Task;
delete?: boolean;
}
機能を準備する前の最後の作業として、AppModule
でいくつかのモジュールをインポートする必要があります。
src/app/app.module.ts
...
import { MatInputModule } from '@angular/material/input';
import { FormsModule } from '@angular/forms';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatInputModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
[Add Task] ボタンをクリックすると、次のユーザー インターフェースが表示されます。
7. アプリのスタイルを改善する
アプリケーションの外観をより魅力的にするために、スタイルを微調整してレイアウトを改善しましょう。スイムレーンを隣り合わせに配置します。また、[Add Task] ボタンと空のリストのラベルを微調整します。
src/app/app.component.css
を開き、一番下に以下のスタイルを追加します。
src/app/app.component.css
mat-toolbar {
margin-bottom: 20px;
}
mat-toolbar > span {
margin-left: 10px;
}
.content-wrapper {
max-width: 1400px;
margin: auto;
}
.container-wrapper {
display: flex;
justify-content: space-around;
}
.container {
width: 400px;
margin: 0 25px 25px 0;
}
.list {
border: solid 1px #ccc;
min-height: 60px;
border-radius: 4px;
}
app-new-task {
margin-bottom: 30px;
}
.empty-label {
font-size: 2em;
padding-top: 10px;
text-align: center;
opacity: 0.2;
}
上記のスニペットでは、ツールバーとそのラベルのレイアウトを調整しています。また、コンテンツの幅を 1400px
、マージンを auto
に設定することで、コンテンツが水平方向に配置されるようにしています。次に、Flexbox を使用してスイムレーンを隣り合わせに配置します。最後に、タスクと空のリストを視覚化する方法を調整します。
アプリを再読み込みすると、次のユーザー インターフェースが表示されます。
アプリのスタイルは大幅に改善されましたが、まだ厄介な問題があります。つまり、タスクを移動する際の表示が次のようになります。
「Buy milk」タスクのドラッグを開始すると、同じタスクのカードが 2 つ表示されます(ドラッグ中のカードとスイムレーン内のカード)。Angular CDK には、この問題を解決するために使用できる CSS クラスが用意されています。
src/app/app.component.css
の末尾に、次のようなスタイルのオーバーライドを追加します。
src/app/app.component.css
.cdk-drag-animating {
transition: transform 250ms;
}
.cdk-drag-placeholder {
opacity: 0;
}
要素のドラッグ中に、Angular CDK をドラッグ&ドロップしてクローンを作成し、元のアイテムをドロップしようとしている位置にそれを挿入します。この要素が表示されないようにするため、CDK がプレースホルダに追加しようとしている cdk-drag-placeholder
クラスに不透明度プロパティを設定します。
さらに、ユーザーが要素をドロップする際に、CDK は cdk-drag-animating
クラスを追加します。要素を直接スナップする代わりに滑らかなアニメーションを表示するため、時間が 250ms
の遷移を定義します。
また、タスクのスタイルにいくつかの微調整を加えます。task.component.css
で、ホスト要素の表示を block
に設定し、いくらかのマージンを設定します。
src/app/task/task.component.css
:host {
display: block;
}
.item {
margin-bottom: 10px;
cursor: pointer;
}
8. 既存のタスクを編集および削除する
既存のタスクを編集および削除するには、これまでに実装した機能のほとんどを再利用します。ユーザーがタスクをダブルクリックしたときは、TaskDialogComponent
を開いて、フォームの 2 つのフィールドにタスクの title
と description
を入力します。
また、TaskDialogComponent
に削除ボタンを追加します。ユーザーがこのボタンをクリックしたときは、削除手順を渡します。これは最終的に AppComponent
に含まれます。
TaskDialogComponent
では、テンプレートにのみ変更を加える必要があります。
src/app/task-dialog/task-dialog.component.html
<mat-form-field>
...
</mat-form-field>
<div mat-dialog-actions>
...
<button
*ngIf="data.enableDelete"
mat-fab
color="primary"
aria-label="Delete"
[mat-dialog-close]="{ task: data.task, delete: true }">
<mat-icon>delete</mat-icon>
</button>
</div>
このボタンは、マテリアルの削除アイコンを表示します。ユーザーがこのボタンをクリックしたときは、ダイアログを閉じ、オブジェクト リテラル { task: data.task, delete: true }
を結果として渡します。また、mat-fab
を使用してボタンを円形にし、色をメインカラーに設定して、ダイアログ データの削除が有効になっている場合にのみ、これを表示します。
編集および削除機能の残りの実装は、AppComponent
にあります。editTask
メソッドを次のように置き換えてください。
src/app/app.component.ts
@Component({ ... })
export class AppComponent {
...
editTask(list: 'done' | 'todo' | 'inProgress', task: Task): void {
const dialogRef = this.dialog.open(TaskDialogComponent, {
width: '270px',
data: {
task,
enableDelete: true,
},
});
dialogRef.afterClosed().subscribe((result: TaskDialogResult|undefined) => {
if (!result) {
return;
}
const dataList = this[list];
const taskIndex = dataList.indexOf(task);
if (result.delete) {
dataList.splice(taskIndex, 1);
} else {
dataList[taskIndex] = task;
}
});
}
...
}
editTask
メソッドの引数を見てみましょう。
- 型
'done' | 'todo' | 'inProgress',
のリスト。これは、個々のスイムレーンに関連付けられたプロパティに対応する値を持つ文字列リテラル共用体型です。 - 編集しようとしている現在のタスク。
メソッドの本文では、最初に TaskDialogComponent
のインスタンスを開きます。その data
として、編集しようとしているタスクを指定するオブジェクト リテラルを渡します。また、enableDelete
プロパティを true
に設定して、フォームの編集ボタンを有効にします。
ダイアログから結果を取得したら、次の 2 つのシナリオを処理します。
delete
フラグがtrue
に設定されたとき(つまり、ユーザーが削除ボタンを押したとき)は、対応するリストからタスクを削除します。- または、ただ単に、指定されたインデックス上のタスクを、ダイアログの結果から取得したタスクで置き換えます。
9. 新しい Firebase プロジェクトを作成する
次に、新しい Firebase プロジェクトを作成しましょう。
- Firebase コンソールに移動します。
- 「KanbanFire」という名前の新しいプロジェクトを作成します。
10. プロジェクトに Firebase を追加する
このセクションでは、プロジェクトを Firebase と統合します。Firebase チームは、2 つのテクノロジーの統合を可能にする @angular/fire
パッケージを提供しています。アプリに Firebase サポートを追加するには、ワークスペースのルート ディレクトリを開いて、次のコマンドを実行します。
ng add @angular/fire
このコマンドを実行すると、@angular/fire
パッケージがインストールされ、いくつかの質問が表示されます。ターミナルには次のように表示されます。
その後、インストールによってブラウザ ウィンドウが開き、Firebase アカウントによる認証が可能になります。最後に、Firebase プロジェクトを選択するよう求められ、ディスクにいくつかのファイルが作成されます。
次に、Firestore データベースを作成する必要があります。[Cloud Firestore] で [データベースを作成] をクリックします。
その後、テストモードでデータベースを作成します。
最後にリージョンを選択します。
後は、Firebase 構成を環境に追加するだけです。プロジェクト構成は、Firebase コンソールで確認できます。
- [プロジェクトの概要] の横にある歯車アイコンをクリックします。
- [プロジェクトの設定] を選択します。
[アプリ] で [ウェブアプリ] を選択します。
次に、アプリケーションを登録して、[Firebase Hosting] が有効になっていることを確認します。
[アプリの登録] をクリックした後で、構成を src/environments/environment.ts
にコピーできます。
最終的に、構成ファイルは次のようになります。
src/environments/environment.ts
export const environment = {
production: false,
firebase: {
apiKey: '<your-key>',
authDomain: '<your-project-authdomain>',
databaseURL: '<your-database-URL>',
projectId: '<your-project-id>',
storageBucket: '<your-storage-bucket>',
messagingSenderId: '<your-messaging-sender-id>'
}
};
11. Firestore にデータを移動する
Firebase SDK の設定が完了したので、@angular/fire
を使用してデータを Firestore に移動しましょう。まず、AppModule
で必要なモジュールをインポートします。
src/app/app.module.ts
...
import { environment } from 'src/environments/environment';
import { AngularFireModule } from '@angular/fire';
import { AngularFirestoreModule } from '@angular/fire/firestore';
@NgModule({
declarations: [AppComponent, TaskDialogComponent, TaskComponent],
imports: [
...
AngularFireModule.initializeApp(environment.firebase),
AngularFirestoreModule
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
この後 Firestore を使用するので、AppComponent
のコンストラクタに AngularFirestore
を注入する必要があります。
src/app/app.component.ts
...
import { AngularFirestore } from '@angular/fire/firestore';
@Component({...})
export class AppComponent {
...
constructor(private dialog: MatDialog, private store: AngularFirestore) {}
...
}
次に、スイムレーン配列を初期化する方法を更新します。
src/app/app.component.ts
...
@Component({...})
export class AppComponent {
todo = this.store.collection('todo').valueChanges({ idField: 'id' }) as Observable<Task[]>;
inProgress = this.store.collection('inProgress').valueChanges({ idField: 'id' }) as Observable<Task[]>;
done = this.store.collection('done').valueChanges({ idField: 'id' }) as Observable<Task[]>;
...
}
ここでは、AngularFirestore
を使用して、データベースから直接コレクションの内容を取得しています。valueChanges
は配列ではなくオブザーバブルを返す点に注意してください。また、このコレクション内のドキュメントの id フィールドの名前を、Task
インターフェースで使用する名前に合わせて id
としている点にも注意してください。valueChanges
によって返されるオブザーバブルは、変更されるたびにタスクのコレクションを出力します。
配列ではなくオブザーバブルを処理するので、タスクの追加、削除、編集を行う方法と、スイムレーン間でタスクを移動する機能を更新する必要があります。メモリ内配列を変更する代わりに、Firebase SDK を使用してデータベース内のデータを更新します。
まず、並べ替えがどのようになるかを見てみましょう。src/app/app.component.ts
の drop
メソッドを次のコードで置き換えます。
src/app/app.component.ts
drop(event: CdkDragDrop<Task[]>): void {
if (event.previousContainer === event.container) {
return;
}
const item = event.previousContainer.data[event.previousIndex];
this.store.firestore.runTransaction(() => {
const promise = Promise.all([
this.store.collection(event.previousContainer.id).doc(item.id).delete(),
this.store.collection(event.container.id).add(item),
]);
return promise;
});
transferArrayItem(
event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex
);
}
上記のスニペットでは、新しいコードはハイライト表示されています。現在のスイムレーンから目的のスイムレーンにタスクを移動するには、最初のコレクションからタスクを削除し、2 つ目のコレクションにそれを追加します。2 つのオペレーションが 1 つに見えるように実行する(つまり、オペレーションをアトミックにする)ため、Firestore トランザクションで実行します。
次に、Firestore を使用するように editTask
メソッドを更新します。「閉じる」ダイアログ ハンドラ内で、次のコード行を変更する必要があります。
src/app/app.component.ts
...
dialogRef.afterClosed().subscribe((result: TaskDialogResult|undefined) => {
if (!result) {
return;
}
if (result.delete) {
this.store.collection(list).doc(task.id).delete();
} else {
this.store.collection(list).doc(task.id).update(task);
}
});
...
Firestore SDK を使用して操作するタスクに対応するターゲット ドキュメントにアクセスして、削除または更新します。
最後に、新しいタスクを作成するメソッドを更新する必要があります。this.todo.push('task')
を this.store.collection('todo').add(result.task)
で置き換えます。
コレクションが配列ではなくオブザーバブルになっている点に注意してください。それらを視覚化するには、AppComponent
のテンプレートを更新する必要があります。そのためには、todo
、inProgress
、および done
プロパティの各アクセスを、それぞれ todo | async
、inProgress | async
、done | async
に置き換えるだけです。
非同期パイプは、コレクションに関連付けられたオブザーバブルを自動的にサブスクライブ登録します。オブザーバブルが新しい値を出力すると、Angular は自動的に変更検出を実行し、出力された配列を処理します。
例として、todo
スイムレーンで行う必要がある変更を見てみましょう。
src/app/app.component.html
<mat-card
cdkDropList
id="todo"
#todoList="cdkDropList"
[cdkDropListData]="todo | async"
[cdkDropListConnectedTo]="[doneList, inProgressList]"
(cdkDropListDropped)="drop($event)"
class="list">
<p class="empty-label" *ngIf="(todo | async)?.length === 0">Empty list</p>
<app-task (edit)="editTask('todo', $event)" *ngFor="let task of todo | async" cdkDrag [task]="task"></app-task>
</mat-card>
データを cdkDropList
ディレクティブに渡す際に、非同期パイプを適用します。*ngIf
ディレクティブの内部と同じですが、todo | async
が null
または undefined
でない場合にランタイム エラーが発生しないようにするため、length
プロパティにアクセスする際にオプションのチェーン(Angular ではセーフ ナビゲーション演算子とも呼ばれます)を使用している点に注意してください。
以上により、ユーザー インターフェースで新しいタスクを作成して Firestore を開くと、次のように表示されます。
12. 楽観的更新を改善する
現在、アプリケーションでは楽観的更新を行っています。Firestore には信頼できる情報源が存在しますが、同時にタスクのローカルコピーもあります。したがって、コレクションに関連付けられたオブザーバブルのいずれかが出力されたら、タスクの配列を取得します。ユーザー アクションによって状態が変化したら、最初にローカル値を更新し、次に Firestore に変更を伝播します。
あるスイムレーンから別のスイムレーンにタスクを移動するときは、各スイムレーン内のタスクを表す配列のローカル インスタンスで動作する transferArrayItem,
を呼び出します。Firebase SDK は、これらの配列を不変なものとして扱います。つまり、Angular が次に変更検出を実行したら、配列の新しいインスタンスを取得します。これらは、前の状態をレンダリングしてからタスクを移動します。
同時に、Firestore の更新をトリガーします。Firebase SDK は正しい値による更新をトリガーするので、数ミリ秒でユーザー インターフェースが正しい状態になります。これにより、先ほど移動したタスクが最初のリストから次のリストに転送されます。このことは、次の GIF を見るとよくわかります。
この問題を解決する適切な方法はアプリケーションによって異なりますが、いずれの場合も、データが更新されるまで一貫した状態を維持する必要があります。
valueChanges
から受け取った元のオブザーバーをラップする BehaviorSubject
を利用できます。内部的に、BehaviorSubject
は transferArrayItem
からの更新を永続的に保持する可変配列を保持します。
修正を実装するには、AppComponent
を更新するだけです。
src/app/app.component.ts
...
import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore';
import { BehaviorSubject } from 'rxjs';
const getObservable = (collection: AngularFirestoreCollection<Task>) => {
const subject = new BehaviorSubject<Task[]>([]);
collection.valueChanges({ idField: 'id' }).subscribe((val: Task[]) => {
subject.next(val);
});
return subject;
};
@Component(...)
export class AppComponent {
todo = getObservable(this.store.collection('todo')) as Observable<Task[]>;
inProgress = getObservable(this.store.collection('inProgress')) as Observable<Task[]>;
done = getObservable(this.store.collection('done')) as Observable<Task[]>;
...
}
上記のスニペットでは、コレクションに関連付けられたオブザーバブルが変更されるたびに値を出力する BehaviorSubject
の作成のみを行っています。
BehaviorSubject
は、変更検出の呼び出し全体で配列を再利用し、Firestore から新しい値を取得する場合にのみ更新されます。そのため、すべてが想定どおりに機能します。
13. アプリケーションをデプロイする
アプリをデプロイするには、次のコマンドを実行するだけです。
ng deploy
このコマンドは次のことを行います。
- コンパイル時の最適化を適用して、本番環境構成でアプリをビルドする。
- アプリを Firebase Hosting にデプロイする。
- 結果をプレビューできるように URL を出力する。
14. 完了
お疲れさまでした。これで、Angular と Firebase を使用してカンバンボードを作成することができました。
この演習では、異なるタスクのステータスを表す 3 つの列を含むユーザー インターフェースを作成しました。また、Angular CDK を使用して、各列の間でのタスクのドラッグ&ドロップを実装しました。次に、Angular マテリアルを使用して、新しいタスクの作成と既存のタスクの編集を行うためのフォームを作成しました。さらに、@angular/fire
の使用方法を学び、アプリケーションのすべての状態を Firestore に移動しました。最後に、完成したアプリケーションを Firebase Hosting にデプロイしました。
次のステップ
この演習では、テスト構成を使用してアプリケーションをデプロイしました。アプリを本番環境にデプロイする前に、正しい権限を設定していることを確認する必要があります。そのための手順については、こちらをご覧ください。
現在は、特定のスイムレーン内の各タスクの順序を保持していません。この機能を実装するには、タスク ドキュメントの順序フィールドを使用し、それに基づいて並べ替えを行います。
また、この演習では単一ユーザー専用のカンバンボードを作成しました。つまり、アプリを開いたすべての人に対して同じカンバンボードが表示されます。アプリのユーザーごとに個別のボードを提供する機能を実装するには、データベース構造を変更する必要があります。Firestore のベスト プラクティスについては、こちらをご覧ください。