1. Einführung
Zuletzt aktualisiert: 11.09.2020
Umfang
In diesem Codelab erstellen wir ein Web-Kanban-Board mit Angular und Firebase. Unsere endgültige App wird drei Kategorien von Aufgaben haben: Backlog, In Bearbeitung und Abgeschlossen. Wir können Aufgaben erstellen, löschen und per Drag-and-drop von einer Kategorie in eine andere verschieben.
Wir entwickeln die Benutzeroberfläche mit Angular und verwenden Firestore als persistenten Speicher. Am Ende des Codelabs stellen wir die App mit der Angular CLI in Firebase Hosting bereit.
Lerninhalte
- Angular Material und das CDK verwenden
- So fügen Sie Ihrer Angular-App eine Firebase-Integration hinzu.
- So behalten Sie Ihre nichtflüchtigen Daten in Firestore.
- So stellen Sie Ihre App mit einem einzigen Befehl über die Angular CLI in Firebase Hosting bereit.
Voraussetzungen
In diesem Codelab wird davon ausgegangen, dass Sie ein Google-Konto haben und über grundlegende Kenntnisse von Angular und der Angular CLI verfügen.
Los gehts!
2. Neues Projekt erstellen
Erstellen wir zuerst einen neuen Angular-Arbeitsbereich:
ng new kanban-fire
? Would you like to add Angular routing? No
? Which stylesheet format would you like to use? CSS
Dieser Schritt kann einige Minuten dauern. Die Angular CLI erstellt die Projektstruktur und installiert alle Abhängigkeiten. Wenn die Installation abgeschlossen ist, wechseln Sie zum Verzeichnis kanban-fire
und starten Sie den Entwicklungsserver der Angular CLI:
ng serve
Öffnen Sie http://localhost:4200. Die Ausgabe sollte in etwa so aussehen:
Öffnen Sie in Ihrem Editor src/app/app.component.html
und löschen Sie den gesamten Inhalt. Wenn Sie zu http://localhost:4200 zurückkehren, sollte eine leere Seite angezeigt werden.
3. Material und CDK hinzufügen
Angular bietet die Implementierung von Material Design-konformen Benutzeroberflächenkomponenten als Teil des @angular/material
-Pakets. Eine der Abhängigkeiten von @angular/material
ist das Component Development Kit (CDK). Das CDK bietet Primitives wie Barrierefreiheits-Dienstprogramme, Drag-and-drop und Overlay. Wir stellen das CDK im Paket @angular/cdk
zur Verfügung.
So fügen Sie Ihrem App-Lauf Material hinzu:
ng add @angular/material
Mit diesem Befehl werden Sie aufgefordert, ein Design auszuwählen, wenn Sie die globalen Material-Typografiestile verwenden möchten, und ob Sie die Browseranimationen für Angular Material einrichten möchten. Wählen Sie „Indigo/Pink“ aus, um dasselbe Ergebnis wie in diesem Codelab zu erhalten, und beantworten Sie die letzten beiden Fragen mit „Ja“.
Mit dem Befehl ng add
werden @angular/material
und seine Abhängigkeiten installiert und BrowserAnimationsModule
in AppModule
importiert. Im nächsten Schritt können wir die Komponenten dieses Moduls verwenden.
Fügen wir zuerst eine Symbolleiste und ein Symbol zur AppComponent
hinzu. Öffnen Sie app.component.html
und fügen Sie das folgende Markup hinzu:
src/app/app.component.html
<mat-toolbar color="primary">
<mat-icon>local_fire_department</mat-icon>
<span>Kanban Fire</span>
</mat-toolbar>
Hier fügen wir eine Symbolleiste mit der Primärfarbe unseres Material Design-Themes hinzu. Darin verwenden wir das local_fire_depeartment
-Symbol neben dem Label „Kanban Fire“. Wenn Sie sich jetzt Ihre Konsole ansehen, werden Sie feststellen, dass Angular einige Fehler ausgibt. Fügen Sie zur Behebung der Probleme AppModule
die folgenden Importe hinzu:
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 { }
Da wir die Angular Material-Symbolleiste und das Symbol verwenden, müssen wir die entsprechenden Module in AppModule
importieren.
Auf dem Bildschirm sollte jetzt Folgendes angezeigt werden:
Das ist nicht schlecht für nur vier Zeilen HTML und zwei Importe.
4. Aufgaben visualisieren
Als Nächstes erstellen wir eine Komponente, mit der wir die Aufgaben im Kanban-Board visualisieren können.
Wechseln Sie in das Verzeichnis src/app
und führen Sie den folgenden CLI-Befehl aus:
ng generate component task
Mit diesem Befehl wird die TaskComponent
generiert und ihre Deklaration wird der AppModule
hinzugefügt. Erstellen Sie im Verzeichnis task
eine Datei mit dem Namen task.ts
. Wir verwenden diese Datei, um die Benutzeroberfläche der Aufgaben im Kanban-Board zu definieren. Jede Aufgabe hat die optionalen Felder id
, title
und description
, die alle vom Typ „String“ sind:
src/app/task/task.ts
export interface Task {
id?: string;
title: string;
description: string;
}
Jetzt aktualisieren wir task.component.ts
. Wir möchten, dass TaskComponent
ein Objekt vom Typ Task
als Eingabe akzeptiert und die Ausgaben „edit
“ ausgeben kann:
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>();
}
Bearbeite die Vorlage von TaskComponent
. Öffnen Sie task.component.html
und ersetzen Sie den Inhalt durch den folgenden HTML-Code:
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>
In der Konsole werden jetzt Fehler angezeigt:
'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
In der Vorlage oben verwenden wir die Komponente mat-card
aus @angular/material
, aber wir haben das entsprechende Modul nicht in die App importiert. Um den Fehler oben zu beheben, müssen wir MatCardModule
in AppModule
importieren:
src/app/app.module.ts
...
import { MatCardModule } from '@angular/material/card';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatCardModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Als Nächstes erstellen wir einige Aufgaben in AppComponent
und visualisieren sie mit TaskComponent
.
Definieren Sie in AppComponent
ein Array namens todo
und fügen Sie darin zwei Aufgaben hinzu:
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!'
}
];
}
Fügen Sie nun unten in app.component.html
die folgende *ngFor
-Anweisung ein:
src/app/app.component.html
<app-task *ngFor="let task of todo" [task]="task"></app-task>
Wenn Sie den Browser öffnen, sollten Sie Folgendes sehen:
5. Drag-and-drop für Aufgaben implementieren
Jetzt kommt der spaßige Teil! Wir erstellen drei Swimlanes für die drei verschiedenen Status, in denen sich Aufgaben befinden können, und implementieren mithilfe des Angular CDK eine Drag-and-drop-Funktion.
Entfernen Sie in app.component.html
die Komponente app-task
mit der *ngFor
-Anweisung oben und ersetzen Sie sie durch Folgendes:
src/app/app.component.html
<div class="content-wrapper">
<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>
</div>
Hier ist viel los. Sehen wir uns die einzelnen Teile dieses Snippets Schritt für Schritt an. Das ist die Struktur der Vorlage auf oberster Ebene:
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>
Hier erstellen wir ein div
, das alle drei Swimlanes umschließt, mit dem Klassennamen „container-wrapper
“. Jede Swimlane hat den Klassennamen „container
“ und einen Titel innerhalb eines h2
-Tags.
Sehen wir uns nun die Struktur der ersten Swimlane an:
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>
...
Zuerst definieren wir den Swimlane als mat-card
, der die cdkDropList
-Anweisung verwendet. Wir verwenden ein mat-card
, weil diese Komponente Stile bereitstellt. Mit dem cdkDropList
-Element können wir später Aufgaben in das Element einfügen. Außerdem legen wir die folgenden beiden Eingaben fest:
cdkDropListData
: Eingabe der Drop-down-Liste, mit der das Datenarray angegeben werden kanncdkDropListConnectedTo
– Verweise auf die anderencdkDropList
, mit denen die aktuellecdkDropList
verbunden ist. Mit dieser Eingabe geben wir an, in welche anderen Listen wir Elemente einfügen können.
Außerdem möchten wir das Drop-Ereignis mit der cdkDropListDropped
-Ausgabe verarbeiten. Sobald cdkDropList
diese Ausgabe ausgibt, rufen wir die Methode drop
auf, die in AppComponent
deklariert ist, und übergeben das aktuelle Ereignis als Argument.
Beachten Sie, dass wir auch eine id
als Kennung für diesen Container und einen class
-Namen angeben, damit wir ihn formatieren können. Sehen wir uns nun die untergeordneten Inhalte von mat-card
an. Die beiden Elemente sind:
- Ein Absatz, in dem der Text „Leere Liste“ angezeigt wird, wenn die
todo
-Liste keine Elemente enthält - Die
app-task
-Komponente : Hier wird die ursprünglich deklarierteedit
-Ausgabe verarbeitet, indem die MethodeeditTask
mit dem Namen der Liste und dem$event
-Objekt aufgerufen wird. So können wir die bearbeitete Aufgabe in der richtigen Liste ersetzen. Als Nächstes durchlaufen wir die Listetodo
wie oben und übergeben die Eingabetask
. Dieses Mal fügen wir jedoch auch diecdkDrag
-Anweisung hinzu. Dadurch werden die einzelnen Aufgaben ziehbar.
Damit das alles funktioniert, müssen wir app.module.ts
aktualisieren und einen Import in DragDropModule
einfügen:
src/app/app.module.ts
...
import { DragDropModule } from '@angular/cdk/drag-drop';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
DragDropModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Außerdem müssen wir die Arrays inProgress
und done
sowie die Methoden editTask
und drop
deklarieren:
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[]>): 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
);
}
}
In der Methode drop
prüfen wir zuerst, ob das Element in dieselbe Liste eingefügt wird, aus der die Aufgabe stammt. In diesem Fall kehren wir sofort zurück. Andernfalls wird die aktuelle Aufgabe in den Ziel-Swimlane übertragen.
Das Ergebnis sollte so aussehen:
Jetzt sollten Sie bereits Artikel zwischen den beiden Listen übertragen können.
6. Neue Aufgaben erstellen
Implementieren wir nun eine Funktion zum Erstellen neuer Aufgaben. Dazu aktualisieren wir die Vorlage von 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>
Wir erstellen ein div
-Element der obersten Ebene um das container-wrapper
-Element und fügen neben dem Label „Aufgabe hinzufügen“ eine Schaltfläche mit dem Material-Symbol „add
“ ein. Wir benötigen den zusätzlichen Wrapper, um die Schaltfläche über der Liste der Swimlanes zu positionieren, die wir später mit Flexbox nebeneinander platzieren. Da für diese Schaltfläche die Material-Schaltflächenkomponente verwendet wird, müssen wir das entsprechende Modul in AppModule
importieren:
src/app/app.module.ts
...
import { MatButtonModule } from '@angular/material/button';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatButtonModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Implementieren wir nun die Funktion zum Hinzufügen von Aufgaben in AppComponent
. Wir verwenden einen Material-Dialog. Im Dialogfeld wird ein Formular mit zwei Feldern angezeigt: „Titel“ und „Beschreibung“. Wenn der Nutzer auf die Schaltfläche „Aufgabe hinzufügen“ klickt, wird das Dialogfeld geöffnet. Wenn der Nutzer das Formular sendet, wird die neu erstellte Aufgabe der Liste todo
hinzugefügt.
Sehen wir uns die allgemeine Implementierung dieser Funktion in AppComponent
an:
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);
});
}
}
Wir deklarieren einen Konstruktor, in den wir die Klasse MatDialog
einfügen. Im newTask
:
- Öffnen Sie ein neues Dialogfeld mit dem
TaskDialogComponent
, das wir gleich definieren. - Geben Sie an, dass das Dialogfeld eine Breite von
270px.
haben soll. - Übergeben Sie eine leere Aufgabe als Daten an den Dialog. In
TaskDialogComponent
können wir einen Verweis auf dieses Datenobjekt abrufen. - Wir abonnieren das „close“-Ereignis und fügen die Aufgabe aus dem
result
-Objekt demtodo
-Array hinzu.
Damit das funktioniert, müssen wir zuerst MatDialogModule
in AppModule
importieren:
src/app/app.module.ts
...
import { MatDialogModule } from '@angular/material/dialog';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatDialogModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Erstellen wir nun die TaskDialogComponent
. Wechseln Sie zum Verzeichnis src/app
und führen Sie Folgendes aus:
ng generate component task-dialog
Um die Funktion zu implementieren, öffnen Sie zuerst src/app/task-dialog/task-dialog.component.html
und ersetzen Sie den Inhalt durch Folgendes:
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>
In der Vorlage oben erstellen wir ein Formular mit zwei Feldern für title
und description
. Wir verwenden die cdkFocusInput
-Anweisung, um den Fokus automatisch auf die title
-Eingabe zu setzen, wenn der Nutzer das Dialogfeld öffnet.
Beachten Sie, wie wir in der Vorlage auf das Attribut data
der Komponente verweisen. Dies ist derselbe data
, den wir an die open
-Methode des dialog
im AppComponent
übergeben. Um den Titel und die Beschreibung der Aufgabe zu aktualisieren, wenn der Nutzer den Inhalt der entsprechenden Felder ändert, verwenden wir die bidirektionale Datenbindung mit ngModel
.
Wenn der Nutzer auf die Schaltfläche „OK“ klickt, geben wir automatisch das Ergebnis { task: data.task }
zurück. Das ist die Aufgabe, die wir mit den Formularfeldern in der Vorlage oben geändert haben.
Implementieren wir nun den Controller der Komponente:
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);
}
}
In TaskDialogComponent
fügen wir einen Verweis auf den Dialog ein, damit wir ihn schließen können. Außerdem fügen wir den Wert des Anbieters ein, der mit dem MAT_DIALOG_DATA
-Token verknüpft ist. Dies ist das Datenobjekt, das wir im AppComponent
oben an die Methode „open“ übergeben haben. Außerdem deklarieren wir die private Property backupTask
, die eine Kopie der Aufgabe ist, die wir zusammen mit dem Datenobjekt übergeben haben.
Wenn der Nutzer auf die Schaltfläche „Abbrechen“ drückt, stellen wir die möglicherweise geänderten Eigenschaften von this.data.task
wieder her und schließen das Dialogfeld. Dabei wird this.data
als Ergebnis übergeben.
Wir haben auf zwei Typen verwiesen, die wir noch nicht deklariert haben: TaskDialogData
und TaskDialogResult
. Fügen Sie in src/app/task-dialog/task-dialog.component.ts
die folgenden Deklarationen unten in der Datei hinzu:
src/app/task-dialog/task-dialog.component.ts
...
export interface TaskDialogData {
task: Partial<Task>;
enableDelete: boolean;
}
export interface TaskDialogResult {
task: Task;
delete?: boolean;
}
Bevor die Funktion bereit ist, müssen wir noch einige Module in AppModule
importieren.
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 { }
Wenn Sie jetzt auf die Schaltfläche „Aufgabe hinzufügen“ klicken, sollte die folgende Benutzeroberfläche angezeigt werden:
7. Stile der App verbessern
Damit die Anwendung optisch ansprechender wird, passen wir das Layout an, indem wir die Formatierungen etwas optimieren. Wir möchten die Swimlanes nebeneinander platzieren. Außerdem möchten wir einige kleinere Anpassungen an der Schaltfläche „Aufgabe hinzufügen“ und am Label für die leere Liste vornehmen.
Öffnen Sie src/app/app.component.css
und fügen Sie unten die folgenden Formatierungen hinzu:
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;
}
Im Snippet oben passen wir das Layout der Symbolleiste und ihr Label an. Außerdem sorgen wir dafür, dass die Inhalte horizontal ausgerichtet sind, indem wir ihre Breite auf 1400px
und ihren Rand auf auto
festlegen. Als Nächstes werden die Swimlanes mit Flexbox nebeneinander platziert. Zum Schluss werden einige Anpassungen an der Visualisierung von Aufgaben und leeren Listen vorgenommen.
Nachdem Ihre App neu geladen wurde, sollte die folgende Benutzeroberfläche angezeigt werden:
Wir haben die Stile unserer App zwar deutlich verbessert, aber es gibt immer noch ein lästiges Problem, wenn wir Aufgaben verschieben:
Wenn wir die Aufgabe „Milch kaufen“ ziehen, sehen wir zwei Karten für dieselbe Aufgabe: die, die wir ziehen, und die in der Swimlane. Das Angular CDK bietet uns CSS-Klassennamen, mit denen wir dieses Problem beheben können.
Fügen Sie die folgenden Stilüberschreibungen am Ende von src/app/app.component.css
hinzu:
src/app/app.component.css
.cdk-drag-animating {
transition: transform 250ms;
}
.cdk-drag-placeholder {
opacity: 0;
}
Wenn wir ein Element ziehen, wird es durch die Drag-and-drop-Funktion des Angular CDK geklont und an der Position eingefügt, an der wir das Original ablegen möchten. Damit dieses Element nicht sichtbar ist, legen wir die Opacity-Eigenschaft in der cdk-drag-placeholder
-Klasse fest, die vom CDK dem Platzhalter hinzugefügt wird.
Wenn wir ein Element ablegen, fügt das CDK außerdem die Klasse cdk-drag-animating
hinzu. Damit das Element nicht direkt einrastet, sondern eine flüssige Animation angezeigt wird, definieren wir einen Übergang mit der Dauer 250ms
.
Außerdem möchten wir einige kleinere Anpassungen an den Stilen unserer Aufgaben vornehmen. Legen wir in task.component.css
die Anzeige des Host-Elements auf block
fest und legen wir einige Ränder fest:
src/app/task/task.component.css
:host {
display: block;
}
.item {
margin-bottom: 10px;
cursor: pointer;
}
8. Vorhandene Aufgaben bearbeiten und löschen
Zum Bearbeiten und Entfernen vorhandener Aufgaben verwenden wir die meisten der bereits implementierten Funktionen wieder. Wenn der Nutzer auf eine Aufgabe doppelklickt, wird das TaskDialogComponent
geöffnet und die beiden Felder im Formular werden mit dem title
und description
der Aufgabe ausgefüllt.
Wir fügen dem TaskDialogComponent
auch eine Schaltfläche zum Löschen hinzu. Wenn der Nutzer darauf klickt, wird ein Löschbefehl übergeben, der in AppComponent
landet.
Die einzige Änderung, die wir in TaskDialogComponent
vornehmen müssen, betrifft die Vorlage:
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>
Auf dieser Schaltfläche wird das Material-Symbol „Löschen“ angezeigt. Wenn der Nutzer darauf klickt, schließen wir das Dialogfeld und übergeben das Objektliteral { task: data.task, delete: true }
als Ergebnis. Außerdem wird die Schaltfläche mit mat-fab
kreisförmig dargestellt, ihre Farbe wird auf „primär“ festgelegt und sie wird nur angezeigt, wenn das Löschen von Dialogfelddaten aktiviert ist.
Die restliche Implementierung der Bearbeitungs- und Löschfunktionen befindet sich in AppComponent
. Ersetzen Sie die editTask
-Methode durch Folgendes:
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;
}
});
}
...
}
Sehen wir uns die Argumente der Methode editTask
an:
- Eine Liste vom Typ
'done' | 'todo' | 'inProgress',
, die ein Stringliteral-Union-Typ mit Werten ist, die den Eigenschaften der einzelnen Swimlanes entsprechen. - Die aktuelle Aufgabe, die wir bearbeiten möchten.
Im Methodenkörper öffnen wir zuerst eine Instanz von TaskDialogComponent
. Als data
übergeben wir ein Objektliteral, das die zu bearbeitende Aufgabe angibt und die Bearbeitungsschaltfläche im Formular aktiviert, indem die Eigenschaft enableDelete
auf true
gesetzt wird.
Wenn wir das Ergebnis aus dem Dialogfeld erhalten, werden zwei Szenarien behandelt:
- Wenn das
delete
-Flag auftrue
gesetzt ist (d.h., wenn der Nutzer auf die Schaltfläche „Löschen“ geklickt hat), wird die Aufgabe aus der entsprechenden Liste entfernt. - Alternativ ersetzen wir die Aufgabe am angegebenen Index einfach durch die Aufgabe, die wir aus dem Dialogergebnis erhalten haben.
9. Neues Firebase-Projekt erstellen
Erstellen wir nun ein neues Firebase-Projekt.
- Rufen Sie die Firebase Console auf.
- Erstellen Sie ein neues Projekt mit dem Namen „KanbanFire“.
10. Firebase dem Projekt hinzufügen
In diesem Abschnitt integrieren wir unser Projekt in Firebase. Das Firebase-Team bietet das Paket @angular/fire
an, das die Integration zwischen den beiden Technologien ermöglicht. Wenn Sie Ihrer App Firebase-Unterstützung hinzufügen möchten, öffnen Sie das Stammverzeichnis Ihres Arbeitsbereichs und führen Sie den folgenden Befehl aus:
ng add @angular/fire
Mit diesem Befehl wird das @angular/fire
-Paket installiert und Sie werden aufgefordert, einige Fragen zu beantworten. In Ihrem Terminal sollte etwa Folgendes angezeigt werden:
Währenddessen wird ein Browserfenster geöffnet, in dem Sie sich mit Ihrem Firebase-Konto authentifizieren können. Zum Schluss werden Sie aufgefordert, ein Firebase-Projekt auszuwählen. Anschließend werden einige Dateien auf Ihrer Festplatte erstellt.
Als Nächstes müssen wir eine Firestore-Datenbank erstellen. Klicken Sie unter „Cloud Firestore“ auf „Datenbank erstellen“.
Erstellen Sie dann eine Datenbank im Testmodus:
Wählen Sie abschließend eine Region aus:
Jetzt müssen Sie nur noch die Firebase-Konfiguration zu Ihrer Umgebung hinzufügen. Sie finden die Projektkonfiguration in der Firebase Console.
- Klicken Sie neben „Projektübersicht“ auf das Zahnradsymbol.
- Wählen Sie „Projekteinstellungen“ aus.
Wählen Sie unter „Meine Apps“ eine Web-App aus:
Registrieren Sie als Nächstes Ihre Anwendung und aktivieren Sie „Firebase Hosting“:
Nachdem Sie auf „App registrieren“ geklickt haben, können Sie Ihre Konfiguration in src/environments/environment.ts
kopieren:
Ihre Konfigurationsdatei sollte im Endeffekt dann in etwa so aussehen:
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. Daten zu Firestore migrieren
Nachdem wir das Firebase SDK eingerichtet haben, verwenden wir @angular/fire
, um unsere Daten zu Firestore zu migrieren. Importieren wir zuerst die Module, die wir in AppModule
benötigen:
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 {}
Da wir Firestore verwenden, müssen wir AngularFirestore
in den Konstruktor von AppComponent
einfügen:
src/app/app.component.ts
...
import { AngularFirestore } from '@angular/fire/firestore';
@Component({...})
export class AppComponent {
...
constructor(private dialog: MatDialog, private store: AngularFirestore) {}
...
}
Als Nächstes aktualisieren wir die Initialisierung der Swimlane-Arrays:
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[]>;
...
}
Hier verwenden wir AngularFirestore
, um die Inhalte der Sammlung direkt aus der Datenbank abzurufen. valueChanges
gibt ein Observable anstelle eines Arrays zurück. Außerdem geben wir an, dass das ID-Feld für die Dokumente in dieser Sammlung id
heißen soll, damit es mit dem Namen übereinstimmt, den wir in der Task
-Schnittstelle verwenden. Das von valueChanges
zurückgegebene Observable gibt jedes Mal, wenn es sich ändert, eine Sammlung von Aufgaben aus.
Da wir mit Observables anstelle von Arrays arbeiten, müssen wir die Art und Weise, wie wir Aufgaben hinzufügen, entfernen und bearbeiten, sowie die Funktionalität zum Verschieben von Aufgaben zwischen Swimlanes aktualisieren. Anstatt unsere In-Memory-Arrays zu ändern, verwenden wir das Firebase SDK, um die Daten in der Datenbank zu aktualisieren.
Sehen wir uns zuerst an, wie die Neuordnung aussehen würde. Ersetzen Sie die drop
-Methode in src/app/app.component.ts
durch:
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
);
}
Im obigen Snippet ist der neue Code hervorgehoben. Um eine Aufgabe von der aktuellen Spalte in die Zielspalte zu verschieben, entfernen wir sie aus der ersten Sammlung und fügen sie der zweiten hinzu. Da wir zwei Vorgänge ausführen, die wie einer aussehen sollen (d.h. den Vorgang atomar machen), führen wir sie in einer Firestore-Transaktion aus.
Als Nächstes aktualisieren wir die Methode editTask
, damit Firestore verwendet wird. Im Handler für das Schließen des Dialogfelds müssen wir die folgenden Codezeilen ändern:
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);
}
});
...
Wir greifen mit dem Firestore SDK auf das Zieldokument zu, das der Aufgabe entspricht, die wir bearbeiten, und löschen oder aktualisieren es.
Schließlich müssen wir noch die Methode zum Erstellen neuer Aufgaben aktualisieren. Ersetzen Sie this.todo.push('task')
durch this.store.collection('todo').add(result.task)
.
Beachten Sie, dass unsere Sammlungen jetzt keine Arrays mehr, sondern Observables sind. Damit sie visualisiert werden können, muss die Vorlage von AppComponent
aktualisiert werden. Ersetzen Sie einfach jeden Zugriff auf die Attribute todo
, inProgress
und done
durch todo | async
, inProgress | async
bzw. done | async
.
Die Async-Pipe abonniert automatisch die Observables, die den Sammlungen zugeordnet sind. Wenn die Observables einen neuen Wert ausgeben, führt Angular automatisch die Änderungsermittlung aus und verarbeitet das ausgegebene Array.
Sehen wir uns beispielsweise die Änderungen an, die wir im todo
-Swimlane vornehmen müssen:
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>
Wenn wir die Daten an die cdkDropList
-Anweisung übergeben, wenden wir die asynchrone Pipe an. Das ist auch in der *ngIf
-Anweisung der Fall. Dort verwenden wir jedoch auch die optionale Verkettung (in Angular auch als Safe Navigation Operator bezeichnet), wenn wir auf die length
-Property zugreifen. So wird sichergestellt, dass kein Laufzeitfehler auftritt, wenn todo | async
nicht null
oder undefined
ist.
Wenn Sie jetzt eine neue Aufgabe über die Benutzeroberfläche erstellen und Firestore öffnen, sollten Sie Folgendes sehen:
12. Optimistische Updates verbessern
In der Anwendung führen wir derzeit optimistische Updates durch. Unsere zentrale Informationsquelle ist Firestore, aber gleichzeitig haben wir lokale Kopien der Aufgaben. Wenn einer der Observables, die mit den Sammlungen verknüpft sind, ein Signal sendet, erhalten wir ein Array von Aufgaben. Wenn der Status durch eine Nutzeraktion geändert wird, aktualisieren wir zuerst die lokalen Werte und übertragen die Änderung dann an Firestore.
Wenn wir eine Aufgabe von einem Swimlane in einen anderen verschieben, rufen wir transferArrayItem,
auf, das für lokale Instanzen der Arrays ausgeführt wird, die die Aufgaben in den einzelnen Swimlanes darstellen. Das Firebase SDK behandelt diese Arrays als unveränderlich. Das bedeutet, dass wir beim nächsten Ausführen der Änderungsermittlung durch Angular neue Instanzen erhalten, die den vorherigen Zustand rendern, bevor wir die Aufgabe übertragen haben.
Gleichzeitig lösen wir ein Firestore-Update aus und das Firebase SDK löst ein Update mit den richtigen Werten aus. Nach wenigen Millisekunden wird die Benutzeroberfläche also in den richtigen Zustand versetzt. Dadurch wird die gerade übertragene Aufgabe von der ersten in die nächste Liste verschoben. Das GIF unten veranschaulicht das Ganze:
Die richtige Lösung für dieses Problem variiert von Anwendung zu Anwendung. In jedem Fall müssen wir jedoch dafür sorgen, dass wir einen konsistenten Status beibehalten, bis unsere Daten aktualisiert werden.
Wir können BehaviorSubject
nutzen, das den ursprünglichen Observer umschließt, den wir von valueChanges
erhalten. Intern verwaltet BehaviorSubject
ein veränderliches Array, in dem die Aktualisierung von transferArrayItem
gespeichert wird.
Um eine Korrektur zu implementieren, müssen wir nur die AppComponent
aktualisieren:
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[]>;
...
}
Im obigen Snippet erstellen wir lediglich ein BehaviorSubject
, das jedes Mal einen Wert ausgibt, wenn sich das mit der Sammlung verknüpfte Observable ändert.
Alles funktioniert wie erwartet, da BehaviorSubject
das Array bei Aufrufen der Änderungserkennung wiederverwendet und nur aktualisiert wird, wenn wir einen neuen Wert von Firestore erhalten.
13. Anwendung bereitstellen
Um unsere App bereitzustellen, müssen wir nur Folgendes ausführen:
ng deploy
Mit diesem Befehl wird Folgendes ausgeführt:
- Erstellen Sie Ihre App mit der Produktionskonfiguration und wenden Sie Optimierungen zur Kompilierzeit an.
- Stellen Sie Ihre App in Firebase Hosting bereit.
- Geben Sie eine URL aus, damit Sie sich das Ergebnis in der Vorschau ansehen können.
14. Glückwunsch
Herzlichen Glückwunsch! Sie haben erfolgreich ein Kanban-Board mit Angular und Firebase erstellt.
Sie haben eine Benutzeroberfläche mit drei Spalten erstellt, die den Status verschiedener Aufgaben darstellen. Mit dem Angular CDK haben Sie Drag-and-drop-Funktionen für Aufgaben zwischen den Spalten implementiert. Anschließend haben Sie mit Angular Material ein Formular zum Erstellen neuer und Bearbeiten vorhandener Aufgaben erstellt. Als Nächstes haben Sie gelernt, wie Sie @angular/fire
verwenden, und den gesamten Anwendungsstatus in Firestore verschoben. Zum Schluss haben Sie Ihre Anwendung in Firebase Hosting bereitgestellt.
Nächste Schritte
Denken Sie daran, dass wir die Anwendung mit Testkonfigurationen bereitgestellt haben. Bevor Sie Ihre App in der Produktion bereitstellen, müssen Sie die richtigen Berechtigungen einrichten. Hier erfahren Sie, wie das geht.
Derzeit wird die Reihenfolge der einzelnen Aufgaben in einem bestimmten Swimlane nicht beibehalten. Dazu können Sie ein Order-Feld im Aufgaben-Dokument verwenden und danach sortieren.
Außerdem haben wir das Kanban-Board nur für einen einzelnen Nutzer erstellt. Das bedeutet, dass jeder, der die App öffnet, dasselbe Kanban-Board sieht. Wenn Sie separate Boards für verschiedene Nutzer Ihrer App implementieren möchten, müssen Sie die Datenbankstruktur ändern. Weitere Informationen zu Best Practices für Firestore