1. Einführung
Zuletzt aktualisiert:11.09.2020
Inhalte, die Sie erstellen werden
In diesem Codelab erstellen wir mit Angular und Firebase ein Web-Kanban-Board. In der endgültigen App gibt es drei Kategorien von Aufgaben: Rückstand, Vorgang läuft und abgeschlossen. Aufgaben können per Drag-and-drop erstellt, gelöscht und aus einer Kategorie in eine andere übertragen werden.
Wir entwickeln die Benutzeroberfläche mit Angular und verwenden Firestore als nichtflüchtigen Speicher. Am Ende des Codelabs stellen Sie die App mithilfe der Angular-Befehlszeile in Firebase Hosting bereit.
Lerninhalte
- Verwendung von Angular und die CDK.
- Anleitung zum Hinzufügen einer Firebase-Integration zu Angular.
- Persistente Daten in Firestore beibehalten
- Bereitstellung Ihrer App in Firebase Hosting mit der Angular-Befehlszeile mit einem einzigen Befehl
Voraussetzungen
In diesem Codelab wird davon ausgegangen, dass Sie ein Google-Konto und ein grundlegendes Verständnis von Angular und der Angular-Befehlszeile haben.
Los gehts:
2. Neues Projekt erstellen
Zuerst erstellen wir 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. Angular-Befehlszeileen erstellen Ihre Projektstruktur und installieren alle Abhängigkeiten. Wechseln Sie nach der Installation zum Verzeichnis kanban-fire
und starten Sie den Entwicklungsserver von Angular-Befehlszeile:
ng serve
Öffnen Sie http://localhost:4200. Die Ausgabe sollte in etwa so aussehen:
Öffnen Sie im 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 die CDK hinzufügen
Angular umfasst eine Implementierung von Material Design-konformen Komponenten der Benutzeroberfläche, die Teil des @angular/material
-Pakets ist. Eine der Abhängigkeiten von @angular/material
ist das Component Development Kit oder die CDK. Die CDK liefert Primitives wie A11y-Dienstprogramme, Drag-and-drop und Overlay. Die CDK wird im @angular/cdk
-Paket verteilt.
So fügen Sie Ihrer App Material hinzu:
ng add @angular/material
Mit diesem Befehl werden Sie aufgefordert, ein Design auszuwählen, wenn Sie die globalen Materialtypografie verwenden möchten und 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 ng add
-Befehl wird @angular/material
, dessen Abhängigkeiten, installiert und der BrowserAnimationsModule
in AppModule
importiert. Im nächsten Schritt können Sie die von diesem Modul angebotenen Komponenten verwenden.
Zuerst fügen wir der AppComponent
eine Symbolleiste und ein Symbol hinzu. Öffne app.component.html
und füge 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 Hauptfarbe unseres Material Design-Designs hinzu. Hier verwenden wir das Symbol local_fire_depeartment
neben dem Label Kantan Fire. Wenn Sie sich jetzt Ihre Konsole ansehen, werden Sie bemerken, dass Angular einige Fehler wirft. Fügen Sie AppModule
die folgenden Importe hinzu, um das Problem zu beheben:
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 Symbolleiste und das Symbol „Angular-Material“ verwenden, müssen wir die entsprechenden Module in AppModule
importieren.
Auf dem Bildschirm sollte nun Folgendes angezeigt werden:
Kein Problem mit nur vier Zeilen HTML und zwei Importen.
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
Durch diesen Befehl wird TaskComponent
generiert und seine Deklaration zu AppModule
hinzugefügt. Erstellen Sie im Verzeichnis task
eine Datei namens task.ts
. Wir verwenden diese Datei, um die Benutzeroberfläche der Aufgaben auf dem Kanban-Board zu definieren. Jede Aufgabe hat die optionalen Felder id
, title
und description
alle vom Typ String:
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
als Eingabe ein Objekt vom Typ Task
akzeptiert und es 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>();
}
Vorlage von TaskComponent
bearbeiten! Ö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
, haben aber nicht das entsprechende Modul in die App importiert. Um den Fehler oben zu beheben, müssen wir die MatCardModule
in das 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 dem TaskComponent
.
Definieren Sie in AppComponent
ein Array mit dem Namen 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üge jetzt unten auf app.component.html
die folgende *ngFor
-Anweisung hinzu:
src/app/app.component.html
<app-task *ngFor="let task of todo" [task]="task"></app-task>
Wenn Sie den Browser öffnen, sollte Folgendes zu sehen sein:
5. Drag-and-drop für Aufgaben implementieren
Wir sind jetzt bereit für den lustigen Teil. Nun erstellen wir drei Swimlanes für die drei verschiedenen Statusaufgaben und implementieren mithilfe der Angular-CDK eine Drag-and-drop-Funktion.
Entfernen Sie in app.component.html
die Komponente app-task
mit der Anweisung *ngFor
oben und ersetzen Sie sie durch:
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>
Hier gibt es viel zu tun. Sehen wir uns die einzelnen Teile dieses Snippets Schritt für Schritt an. Dies ist die oberste Struktur der Vorlage:
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 Schwimmspuren umschließt, mit dem Klassennamen "container-wrapper
. Jede Schwimmspur hat einen Klassennamen "container
" mit einem Titel innerhalb des h2
Tags.
Schauen wir uns jetzt 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 die Swimlane als mat-card
, die die cdkDropList
-Anweisung verwendet. Wir verwenden mat-card
aufgrund der Stile, die diese Komponente bereitstellt. Über cdkDropList
können wir später Aufgaben in dem Element ablegen. Außerdem legen wir die folgenden beiden Eingaben fest:
cdkDropListData
– Eingabe der Drop-down-Liste, mit der das Datenarray angegeben werden kanncdkDropListConnectedTo
: verweist auf die anderencdkDropList
s, mit denen der aktuellecdkDropList
verbunden ist. In den Einstellungen geben wir an, in welche anderen Listen wir Elemente aufnehmen können.
Außerdem möchten wir das Drop-Ereignis mit der Ausgabe cdkDropListDropped
verarbeiten. Sobald cdkDropList
diese Ausgabe ausgibt, werden die in AppComponent
deklarierte Methode drop
aufgerufen und das aktuelle Ereignis als Argument übergeben.
Wir geben auch ein id
-Element an, das als Kennung für diesen Container verwendet werden soll, sowie einen class
-Namen für die Gestaltung. Schauen wir uns jetzt die Inhalte der untergeordneten Elemente von mat-card
an. Es gibt die beiden folgenden Elemente:
- Ein Absatz, in dem Text angezeigt wird, wenn sich keine Elemente auf der Liste „
todo
“ befinden - Die
app-task
-Komponente : Beachten Sie, dass wir hier die verarbeiteteedit
-Ausgabe verarbeiten, indem wir dieeditTask
-Methode mit dem Namen der Liste und dem$event
-Objekt aufrufen. So können wir die bearbeitete Aufgabe aus der richtigen Liste ersetzen. Als Nächstes wiederholen wir wie oben beschrieben die Listetodo
und übergeben die Eingabetask
. Dieses Mal wird jedoch auch diecdkDrag
-Anweisung hinzugefügt. Dadurch können die einzelnen Aufgaben verschoben werden.
Damit das alles funktioniert, müssen wir app.module.ts
aktualisieren und einen Import in DragDropModule
hinzufü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
zusammen mit den 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[]|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
);
}
}
Beachten Sie, dass wir in der drop
-Methode zuerst prüfen, ob wir in der Liste fallen, von der die Aufgabe stammt. Wenn dies der Fall ist, kehren wir sofort zurück. Andernfalls übertragen wir die aktuelle Aufgabe in die Zielbadebahn.
Das Ergebnis sollte so aussehen:
Von nun an sollten Sie in der Lage sein, Elemente zwischen den beiden Listen zu übertragen.
6. Neue Aufgaben erstellen
Jetzt implementieren wir eine Funktion zum Erstellen neuer Aufgaben. Zu diesem Zweck 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
-Objekt und fügen eine Schaltfläche mit dem Materialsymbol add
neben dem Label „Aufgabe hinzufügen“ hinzu. Wir benötigen den zusätzlichen Wrapper, um die Schaltfläche über der Liste der Swimlanes zu positionieren, die wir später mithilfe der Flexbox nebeneinander platzieren. Da auf dieser Schaltfläche die Komponente „Material-Schaltfläche“ 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 { }
Jetzt implementieren wir die Funktion zum Hinzufügen von Aufgaben in der AppComponent
. Wir verwenden das Dialogfeld „Material“. Im Dialogfeld haben wir ein Formular mit zwei Feldern: Titel und Beschreibung. Wenn der Nutzer auf die Schaltfläche „Aufgabe hinzufügen“ klickt, wird das Dialogfeld geöffnet. Wenn der Nutzer das Formular absendet, wird die neu erstellte Aufgabe der Liste „todo
“ hinzugefügt.
Sehen wir uns die allgemeine Implementierung dieser Funktion in der 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 MatDialog
-Klasse einschleusen. In der newTask
haben wir:
- Öffnen Sie ein neues Dialogfeld mit dem
TaskDialogComponent
, das wir kurz definieren. - Geben Sie an, dass das Dialogfeld eine Breite von
270px.
haben soll - Übergeben Sie eine leere Aufgabe als Daten an das Dialogfeld. In
TaskDialogComponent
können wir einen Verweis auf dieses Datenobjekt abrufen. - Wir schließen das Abschlussereignis und fügen die Aufgabe aus dem
result
-Objekt zumtodo
-Array hinzu.
Damit dies funktioniert, musst du zuerst die MatDialogModule
in die AppModule
importieren:
src/app/app.module.ts
...
import { MatDialogModule } from '@angular/material/dialog';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatDialogModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Jetzt erstellen wir die TaskDialogComponent
. Rufen Sie das Verzeichnis src/app
auf und führen Sie Folgendes aus:
ng generate component task-dialog
Wenn du die Funktion implementieren möchtest, öffne zuerst src/app/task-dialog/task-dialog.component.html
und ersetze den Inhalt durch:
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 die title
und die description
. Die Anweisung cdkFocusInput
wird automatisch verwendet, um die Eingabe title
hervorzuheben, wenn der Nutzer das Dialogfeld öffnet.
In der Vorlage verweisen wir auf die Property data
der Komponente. Dies ist dieselbe data
, die wir an die open
-Methode der dialog
in der AppComponent
übergeben. Zum Aktualisieren des Titels und der Beschreibung der Aufgabe verwenden wir die bidirektionale Datenbindung mit ngModel
, wenn der Nutzer den Inhalt der entsprechenden Felder ändert.
Wenn der Nutzer auf die Schaltfläche „OK“ klickt, wird automatisch das Ergebnis { task: data.task }
zurückgegeben. Diese Aufgabe wurde über die Formularfelder in der Vorlage oben verändert.
Jetzt implementieren wir 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
wird ein Verweis auf das Dialogfeld eingefügt, damit wir es schließen können. Außerdem wird der Wert des Anbieters eingefügt, der mit dem MAT_DIALOG_DATA
-Token verknüpft ist. Dies ist das Datenobjekt, das wir an die offene Methode in AppComponent
oben übergeben haben. Außerdem deklarieren wir die private Property backupTask
. Das ist eine Kopie der Aufgabe, die wir an das Datenobjekt übergeben haben.
Wenn der Nutzer auf die Schaltfläche zum Abbrechen klickt, stellen wir die möglicherweise geänderten Eigenschaften von this.data.task
wieder her und schließen das Dialogfeld, wobei this.data
als Ergebnis übergeben wird.
Es gibt zwei Typen, auf die wir verwiesen haben, aber noch nicht deklariert: TaskDialogData
und TaskDialogResult
. Fügen Sie unten in der Datei src/app/task-dialog/task-dialog.component.ts
die folgenden Deklarationen ein:
src/app/task-dialog/task-dialog.component.ts
...
export interface TaskDialogData {
task: Partial<Task>;
enableDelete: boolean;
}
export interface TaskDialogResult {
task: Task;
delete?: boolean;
}
Bevor wir die Funktionalität vorbereiten können, müssen wir nur noch einige Module in die 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. App-Stile verbessern
Damit das Design für die Anwendung ansprechender wird, passen wir das Layout an, indem wir die Designs leicht verändern. Wir möchten die Schwimmspuren nebeneinander positionieren. Wir möchten auch einige kleine Anpassungen der Schaltfläche „Aufgabe hinzufügen“ und des leeren Listenlabels vornehmen.
Öffnen Sie src/app/app.component.css
und fügen Sie unten die folgenden Stile 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 wird das Layout der Symbolleiste und des Labels angepasst. Außerdem sorgen wir für die horizontale Ausrichtung der Inhalte, indem wir die Breite auf 1400px
und den Rand auf auto
festlegen. Mithilfe von Flexbox platzieren wir die Swimlane so nebeneinander und nehmen dann Anpassungen an der Darstellung von Aufgaben und leeren Listen vor.
Sobald Ihre App neu geladen ist, sollten Sie die folgende Benutzeroberfläche sehen:
Auch wenn wir die Gestaltung unserer Apps erheblich verbessert haben, bestehen weiterhin Probleme, wenn wir Aufgaben verschieben:
Wenn wir beginnen, die Aufgabe „Milch kaufen“ zu verschieben, werden zwei Karten für dieselbe Aufgabe angezeigt – die Karte, die wir verschieben, und die Karte in der Schwimmbahn. Angular CDK liefert uns CSS-Klassennamen, mit denen wir dieses Problem beheben können.
Füge am Ende von src/app/app.component.css
die folgenden Stilüberschreibungen hinzu:
src/app/app.component.css
.cdk-drag-animating {
transition: transform 250ms;
}
.cdk-drag-placeholder {
opacity: 0;
}
Während wir ein Element ziehen, wird es von der Angular-CDK per Drag-and-drop geklont und an der Stelle eingefügt, an der das Original abgelegt wird. Damit dieses Element nicht sichtbar ist, legen wir die Deckkraft-Eigenschaft in der cdk-drag-placeholder
-Klasse fest, die der CDK dem Platzhalter hinzufügt.
Außerdem fügt die CDK beim Hinzufügen eines Elements die Klasse cdk-drag-animating
hinzu. Um eine flüssige Animation zu zeigen, anstatt das Element direkt auszurichten, definieren wir einen Übergang mit der Dauer 250ms
.
Wir möchten auch einige kleinere Anpassungen an den Stilen unserer Aufgaben vornehmen. Legen Sie in task.component.css
Folgendes fest, um das Hostelement auf block
anzuzeigen und Ränder festzulegen:
src/app/task/task.component.css
:host {
display: block;
}
.item {
margin-bottom: 10px;
cursor: pointer;
}
8. Vorhandene Aufgaben bearbeiten und löschen
Die vorhandenen Funktionen können wir nutzen, um bestehende Aufgaben zu bearbeiten und zu entfernen. Wenn ein Nutzer auf eine Aufgabe doppelklickt, wird TaskDialogComponent
geöffnet und die zwei Felder im Formular werden mit den Aufgaben title
und description
ausgefüllt.
Zur TaskDialogComponent
fügen wir außerdem eine Schaltfläche zum Löschen hinzu. Wenn der Nutzer darauf klickt, wird eine Löschanweisung übergeben, die am AppComponent
endet.
Die einzige Änderung, die wir in „TaskDialogComponent
“ vornehmen müssen, ist 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>
Über diese Schaltfläche wird das Symbol zum Löschen von Material angezeigt. Wenn der Nutzer darauf klickt, schließen wir das Dialogfeld und geben das Objektliteral { task: data.task, delete: true }
weiter. Beachten Sie auch, dass die Schaltfläche mit mat-fab
gekennzeichnet und als Farbe festgelegt wird und nur angezeigt wird, wenn das Löschen von Dialogdaten aktiviert ist.
Der Rest der Implementierung der Funktionen zum Bearbeiten und Löschen befindet sich in der AppComponent
. Ersetzen Sie die Methode editTask
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',
. Dabei handelt es sich um einen String-Literaltyp mit Werten, die den Properties entsprechen, die mit den einzelnen Swimlanes verknüpft sind. - Die aktuelle Aufgabe, die Sie bearbeiten möchten.
Im Hauptteil der Methode öffnen wir zuerst eine Instanz von TaskDialogComponent
. Als data
übergeben Sie ein Objektliteral, das die zu bearbeitende Aufgabe angibt. Außerdem ermöglicht sie die Bearbeitungsschaltfläche im Formular, indem die Property enableDelete
auf true
festgelegt wird.
Im Ergebnis des Dialogfelds werden zwei Szenarien behandelt:
- Wenn das Flag
delete
auftrue
gesetzt ist, d.h., der Nutzer hat die Schaltfläche „Löschen“ angeklickt, entfernen wir die Aufgabe aus der entsprechenden Liste. - Alternativ ersetzen wir einfach die Aufgabe für den angegebenen Index durch die Aufgabe, die wir aus dem Dialogfeld erhalten haben.
9. Neues Firebase-Projekt wird erstellt
Erstellen Sie jetzt ein neues Firebase-Projekt.
- Rufen Sie die Firebase Console auf.
- Erstellen Sie ein neues Projekt mit dem Namen „KanbanFire“.
10. Firebase zum Projekt hinzufügen
In diesem Abschnitt integrieren wir unser Projekt mit Firebase. Das Firebase-Team bietet das Paket @angular/fire
, mit dem die beiden Technologien verknüpft werden können. Wenn Sie Ihrer App Firebase-Support hinzufügen möchten, öffnen Sie das Stammverzeichnis Ihres Arbeitsbereichs und führen Sie den folgenden Befehl aus:
ng add @angular/fire
Durch diesen Befehl wird das @angular/fire
-Paket installiert und einige Fragen gestellt. Im Terminal sollte Folgendes zu sehen sein:
In der Zwischenzeit wird bei der Installation ein Browserfenster geöffnet, sodass Sie sich mit Ihrem Firebase-Konto authentifizieren können. Schließlich werden Sie aufgefordert, ein Firebase-Projekt auszuwählen, und es werden einige Dateien auf dem Laufwerk erstellt.
Als Nächstes müssen Sie eine Firestore-Datenbank erstellen. Klicken Sie unter „Cloud Firestore“ auf „Datenbank erstellen“.
Erstellen Sie anschließend eine Datenbank im Testmodus:
Wählen Sie zuletzt 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 auf das Zahnradsymbol neben „Projektübersicht“.
- Wählen Sie die Projekteinstellungen aus.
Wählen Sie unter „Meine Apps“ eine Webanwendung aus:
Registrieren Sie anschließend Ihre Anwendung und aktivieren Sie das Firebase Hosting:
Nachdem Sie auf „App registrieren“ geklickt haben, können Sie die Konfiguration in src/environments/environment.ts
kopieren:
Ihre Konfigurationsdatei sollte am Ende 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 verschieben
Nachdem Sie das Firebase SDK eingerichtet haben, können Sie jetzt mit @angular/fire
Ihre Daten in Firestore verschieben. Zuerst importieren wir die erforderlichen Module in 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 {}
Da wir Firestore verwenden, müssen wir AngularFirestore
in den Konstruktor von AppComponent
einschleusen:
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 Methode zum Initialisieren 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 den AngularFirestore
, um den Inhalt der Sammlung direkt aus der Datenbank abzurufen. Beachten Sie, dass valueChanges
anstelle eines Arrays ein beobachtbares Objekt zurückgibt. Außerdem geben wir an, dass das ID-Feld für die Dokumente in dieser Sammlung mit dem Namen id
benannt werden soll, damit er mit dem Namen in der Task
-Schnittstelle übereinstimmt. Die von valueChanges
beobachtbare Beobachtung gibt bei jeder Änderung eine Sammlung von Aufgaben aus.
Da wir mit Beobachtbarkeits- anstelle von Arrays arbeiten, müssen wir die Art und Weise aktualisieren, wie wir Aufgaben hinzufügen, entfernen und bearbeiten, und die Funktion zum Verschieben von Aufgaben zwischen Swimlane. 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 Reihenfolge aussehen würde. Ersetze 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 Snippet oben ist der neue Code hervorgehoben. Um eine Aufgabe von der aktuellen Schwimmspur zur Zielposition zu verschieben, werden sie aus der ersten Sammlung entfernt und der zweiten hinzugefügt. Da wir zwei Vorgänge ausführen möchten, die wir ähneln möchten, d.h. den Vorgang atomar machen, führen wir sie in einer Firestore-Transaktion aus.
Als Nächstes aktualisieren wir die Methode editTask
, um Firestore zu verwenden. Im Dialogfeld zum Schließen des Dialogfelds müssen folgende Codezeilen geändert werden:
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 der Aufgabe zu, die wir bearbeiten, und löschen oder aktualisieren es.
Abschließend müssen Sie 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, sondern beobachtbare sind. Zum Visualisieren müssen wir die Vorlage von AppComponent
aktualisieren. Dazu musst du nur die Zugriffsrechte der Properties todo
, inProgress
und done
jeweils durch todo | async
, inProgress | async
bzw. done | async
ersetzen.
Mit der asynchronen Pipeline werden die mit den Sammlungen verknüpften Beobachtbarkeiten automatisch abonniert. Wenn die Beobachtbarkeit einen neuen Wert ausgibt, führt Angular automatisch die Änderungserkennung aus und verarbeitet das ausgegebene Array.
Beispielsweise können wir uns die Änderungen ansehen, die wir in der Swimlane von todo
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, werden die asynchronen Pipelines verwendet. Es ist in der *ngIf
-Anweisung identisch, aber wir verwenden für das Aufrufen der length
-Property auch die optionale Verkettung (auch als „Navigationsoperator“ in Angular bezeichnet). Damit soll sichergestellt werden, dass kein Laufzeitfehler angezeigt wird, wenn todo | async
nicht null
oder undefined
ist.
Wenn Sie auf der Benutzeroberfläche eine neue Aufgabe erstellen und Firestore öffnen, sollte Folgendes angezeigt werden:
12. Optimale Updates verbessern
In der App werden derzeit optimistische Aktualisierungen durchgeführt. In Firestore ist unsere Quelle der Wahrheit eingetragen. Gleichzeitig liegen uns aber lokale Kopien der Aufgaben vor. Sobald eine Beobachtbarkeit der Sammlungen ausgegeben wird, erhalten wir eine Reihe von Aufgaben. Wenn eine Nutzeraktion den Status ändert, aktualisieren wir zuerst die lokalen Werte und übertragen die Änderungen dann an Firestore.
Wenn wir eine Aufgabe von einer Swimlane in eine andere verschieben, rufen wir transferArrayItem,
auf, das auf lokalen Instanzen der Arrays ausgeführt wird, die die Aufgaben in jeder Swimlane darstellen. Diese Arrays werden vom Firebase SDK als unveränderlich behandelt. Das bedeutet, dass Angular bei der nächsten Ausführungserkennung eine neue Instanz davon erhält, die den vorherigen Zustand rendert, bevor die Aufgabe übertragen wurde.
Gleichzeitig starten wir ein Firestore-Update und das Firebase SDK ein Update mit den richtigen Werten. Es kann also einige Millisekunden dauern, bis die Benutzeroberfläche den korrekten Status erreicht. Dadurch wird die soeben übertragene Aufgabe von der ersten Liste zur nächsten verschoben. Dies erkennen Sie am GIF unten:
Die richtige Vorgehensweise zur Lösung dieses Problems variiert je nach Anwendung, aber wir müssen in jedem Fall dafür sorgen, dass der Status konstant bleibt, bis unsere Daten aktualisiert wurden.
Wir können BehaviorSubject
nutzen, um den ursprünglichen Server von valueChanges
zu erfassen. Im Hintergrund hält BehaviorSubject
ein änderbares Array bereit, das das Update von transferArrayItem
fortsetzt.
Zum Implementieren einer Fehlerbehebung müssen wir lediglich 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[]>;
...
}
Mit dem obigen Snippet wird lediglich eine BehaviorSubject
erstellt, die jedes Mal einen Wert ausgibt, wenn die mit der Sammlung verknüpfte Beobachtbarkeit geändert wird.
Es funktioniert alles wie erwartet, da BehaviorSubject
das Array für Änderungserkennungsaufrufe wiederverwendet und nur dann aktualisiert, wenn ein neuer Wert aus Firestore abgerufen wird.
13. Anwendung bereitstellen
Wir müssen nur Ihre Anwendung bereitstellen:
ng deploy
Durch diesen Befehl werden folgende Aktionen ausgeführt:
- Erstellen Sie Ihre App mit der Produktionskonfiguration, indem Sie die Kompilierdauer optimieren.
- Anwendung in Firebase Hosting bereitstellen
- Geben Sie eine URL aus, um eine Vorschau des Ergebnisses zu sehen.
14. Glückwunsch
Glückwunsch! Sie haben ein Kanban-Board mit Angular und Firebase erstellt.
Sie haben eine Benutzeroberfläche mit drei Spalten erstellt, die den Status verschiedener Aufgaben darstellen. Mithilfe von Angular CDK haben Sie Aufgaben per Drag-and-drop in den Spalten implementiert. Dann haben Sie mit Angular-Material ein Formular erstellt, um neue Aufgaben zu erstellen und vorhandene zu bearbeiten. Als Nächstes haben Sie erfahren, wie Sie @angular/fire
verwenden und den Anwendungsstatus zu Firestore verschoben. Zuletzt haben Sie Ihre Anwendung in Firebase Hosting bereitgestellt.
Was liegt als Nächstes an?
Denken Sie daran, dass wir die Anwendung mithilfe von Testkonfigurationen bereitgestellt haben. Bevor Sie Ihre App für die Produktion bereitstellen, müssen Sie die richtigen Berechtigungen einrichten. Hier erhalten Sie die entsprechenden Informationen.
Zurzeit wird die Reihenfolge der einzelnen Aufgaben in einer bestimmten Schwimmspur nicht beibehalten. Um das zu implementieren, können Sie ein Auftragsfeld im Aufgabendokument nutzen und entsprechend sortieren.
Außerdem haben wir das Kanban-Board nur für einen einzelnen Nutzer erstellt. Das bedeutet, dass wir ein einziges Kanban-Board für alle haben, die die App öffnen. Wenn Sie für die einzelnen Nutzer Ihrer App separate Boards implementieren möchten, müssen Sie die Datenbankstruktur ändern. Informationen zu Best Practices von Firestore