Webanwendung mit Angular und Firebase erstellen

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.

b23bd3732d0206b.pngs

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:

5ede7bc5b1109bf3

Ö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:

a39cf8f8428a03bc

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:

d96fccd13c63ceb1

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 &quotcontainer" 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 kann
  • cdkDropListConnectedTo: verweist auf die anderen cdkDropLists, mit denen der aktuelle cdkDropList 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 verarbeitete edit-Ausgabe verarbeiten, indem wir die editTask-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 Liste todo und übergeben die Eingabe task. Dieses Mal wird jedoch auch die cdkDrag-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:

460f86bcd10454cf.png

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 zum todo-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:

33bcb987fade2a87

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:

69225f0b1aa5cb50

Auch wenn wir die Gestaltung unserer Apps erheblich verbessert haben, bestehen weiterhin Probleme, wenn wir Aufgaben verschieben:

f9aae712027624af.png

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 auf true 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:

9ba88c0d52d18d0.png

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“.

1e4a08b5a2462956

Erstellen Sie anschließend eine Datenbank im Testmodus:

ac1181b2c32049f9

Wählen Sie zuletzt eine Region aus:

34bb94cc542a0597

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.

c8253a20031de8a9

Wählen Sie unter „Meine Apps“ eine Webanwendung aus:

428a1abcd0f90b23.png

Registrieren Sie anschließend Ihre Anwendung und aktivieren Sie das Firebase Hosting:

586e44cb27dd8f39_png

Nachdem Sie auf „App registrieren“ geklickt haben, können Sie die Konfiguration in src/environments/environment.ts kopieren:

e30f142d79cecf8f

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:

dd7ee20c0a10ebe2.png

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:

70b946eebfa6f316

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:

  1. Erstellen Sie Ihre App mit der Produktionskonfiguration, indem Sie die Kompilierdauer optimieren.
  2. Anwendung in Firebase Hosting bereitstellen
  3. 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