1. Wstęp
Ostatnia aktualizacja: 11 września 2020 r.
Co stworzysz
W tym ćwiczeniu stworzymy tablicę internetową banban z Angular i Firebase. Ostateczna wersja aplikacji będzie zawierać 3 kategorie zadań: zaległości, trwające i ukończone. Będzie można tworzyć, usuwać zadania i przenosić je z jednej kategorii do drugiej.
Będziemy rozwijać interfejs użytkownika za pomocą Angular i korzystać z Firestore jako naszego stałego sklepu. Na koniec ćwiczeń z programowania wdrożymy aplikację w Hostingu Firebase przy użyciu interfejsu wiersza poleceń Angular.
Czego się nauczysz:
- Jak używać materiałów Angular i CDK.
- Jak dodać integrację z Firebase do aplikacji Angular.
- Jak zachować trwałe dane w Firestore.
- Jak wdrożyć aplikację w Hostingu Firebase przy użyciu interfejsu wiersza poleceń Angular za pomocą jednego polecenia.
Czego potrzebujesz
W tym ćwiczeniu zakładamy, że masz konto Google oraz znasz podstawy Angular i interfejsu wiersza poleceń Angular.
Zaczynajmy!
2. Tworzenie nowego projektu
Najpierw utwórz nowy obszar roboczy Angular:
ng new kanban-fire
? Would you like to add Angular routing? No
? Which stylesheet format would you like to use? CSS
Może to potrwać kilka minut. Interfejs wiersza poleceń Angular tworzy strukturę projektu i instaluje wszystkie zależności. Po zakończeniu procesu instalacji przejdź do katalogu kanban-fire
i uruchom serwer programowania aplikacji Angular CLI'.
ng serve
Otwórz stronę http://localhost:4200. Dane wyjściowe powinny przypominać te:
W edytorze otwórz src/app/app.component.html
i usuń całą jego zawartość. Po powrocie do adresu http://localhost:4200 powinna wyświetlić się pusta strona.
3. Dodawanie materiału i CDK
Angular jest częścią komponentów interfejsu użytkownika zgodnych ze stylem Material Design, które są częścią pakietu @angular/material
. Jedna z zależności narzędzia @angular/material
to Component Development Kit lub CDK. CDK udostępnia elementy podstawowe, np. narzędzia a11y, przeciąganie i upuszczanie oraz nakładki. Rozsyłamy CDK w pakiecie @angular/cdk
.
Aby dodać materiał do uruchomienia aplikacji:
ng add @angular/material
To polecenie pozwala wybrać motyw, jeśli chcesz używać globalnych stylów typografii materiału i chcesz skonfigurować animacje przeglądarki dla materiału Angular. Wybierz „&diquo;Indigo/Pink"”, aby uzyskać taki sam wynik jak w tym ćwiczeniu z programowania, i odpowiedziaj „Tak&tak”, na ostatnie dwa pytania.
Polecenie ng add
zainstaluje @angular/material
, jego zależności i zaimportuje BrowserAnimationsModule
w AppModule
. W następnym kroku możemy zacząć używać komponentów dostępnych w tym module.
Najpierw dodaj pasek narzędzi i ikonę do AppComponent
. Otwórz app.component.html
i dodaj te znaczniki:
src/app/app.component.html
<mat-toolbar color="primary">
<mat-icon>local_fire_department</mat-icon>
<span>Kanban Fire</span>
</mat-toolbar>
Dodajemy do niego pasek narzędzi w kolorze głównym w stylu Material Design, a w jego wnętrzu znajduje się ikona local_fire_depeartment
obok etykiety „&Kanban Fire”." Jeśli teraz wyświetlisz konsolę, zobaczysz, że Angular zwraca kilka błędów. Aby je naprawić, dodaj te importy do usługi AppModule
:
src/app/app.module.ts
...
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatIconModule } from '@angular/material/icon';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatToolbarModule,
MatIconModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Używamy paska narzędzi i ikony w Angular, więc musimy zaimportować odpowiednie moduły w AppModule
.
Na ekranie powinny pojawić się następujące elementy:
Całkiem spoko
4. Wizualizacja zadań
W następnym kroku utwórzmy komponent, którego możemy użyć do wizualizacji zadań na tablicy kanban.
Przejdź do katalogu src/app
i uruchom to polecenie interfejsu wiersza poleceń:
ng generate component task
To polecenie generuje TaskComponent
i dodaje deklarację do AppModule
. W katalogu task
utwórz plik o nazwie task.ts
. Użyjemy tego pliku do zdefiniowania interfejsu zadań na tablicy kanban. Każde zadanie będzie zawierać opcjonalne pola id
, title
i description
, które zawierają ciąg znaków:
src/app/task/task.ts
export interface Task {
id?: string;
title: string;
description: string;
}
A teraz zaktualizujmy task.component.ts
. Chcemy, aby obiekt TaskComponent
akceptował jako obiekt wejściowy typu Task
i chceł w ten sposób emitować dane wyjściowe edit
"
src/app/task/task.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Task } from './task';
@Component({
selector: 'app-task',
templateUrl: './task.component.html',
styleUrls: ['./task.component.css']
})
export class TaskComponent {
@Input() task: Task | null = null;
@Output() edit = new EventEmitter<Task>();
}
Edytuj szablon TaskComponent
'! Otwórz plik task.component.html
i zastąp jego zawartość tym kodem HTML:
src/app/task/task.component.html
<mat-card class="item" *ngIf="task" (dblclick)="edit.emit(task)">
<h2>{{ task.title }}</h2>
<p>
{{ task.description }}
</p>
</mat-card>
Uwaga: w konsoli wyświetlają się teraz błędy:
'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
W powyższym szablonie używamy komponentu mat-card
z elementu @angular/material
, ale nie zaimportujemy tego modułu w aplikacji. Aby naprawić błąd powyżej, zaimportujemy MatCardModule
w narzędziu AppModule
:
src/app/app.module.ts
...
import { MatCardModule } from '@angular/material/card';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatCardModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Następnie utworzymy kilka zadań w AppComponent
i zwizualizujemy je za pomocą TaskComponent
.
W AppComponent
zdefiniuj tablicę o nazwie todo
i dodaj do niej dwa zadania:
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!'
}
];
}
Następnie na dole strony app.component.html
dodaj tę dyrektywę *ngFor
:
src/app/app.component.html
<app-task *ngFor="let task of todo" [task]="task"></app-task>
Po otwarciu przeglądarki powinna wyświetlić się ta informacja:
5. Wdrażanie funkcji przeciągania i upuszczania w zadaniach
Pora na zabawę! Utwórzmy 3 ścieżki dla 3 różnych zadań, które można wykonać w danym stanie, a dzięki Angular CDK zaimplementujemy funkcję przeciągania i upuszczania.
W systemie app.component.html
usuń komponent app-task
z dyrektywą *ngFor
na górze i zastąp go fragmentem:
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>
Wiele się tu dzieje. Przyjrzyjmy się poszczególnym częściom tego fragmentu kodu. Oto struktura najwyższego poziomu szablonu:
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>
Tworzymy tutaj div
, który opakowuje wszystkie 3 ścieżki, nadając im nazwę klasy „container-wrapper
”."”. Każda ścieżka ma nazwę klasy "container
" i tytuł w tagu h2
.
Przyjrzyjmy się teraz strukturze pierwszej ścieżki:
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>
...
Najpierw definiujemy ścieżkę jako mat-card
, która korzysta z dyrektywy cdkDropList
. Używamy mat-card
ze względu na style obsługiwane przez ten komponent. cdkDropList
umożliwi nam późniejsze pominięcie zadań w elemencie. Ustawiamy też 2 takie dane wejściowe:
cdkDropListData
– dane wejściowe listy rozwijanej, która umożliwia nam określenie tablicy danychcdkDropListConnectedTo
– odwołania do pozostałych elementówcdkDropList
, z którymi jest obecnie połączonycdkDropList
. To ustawienie określa, na których innych listach można upuścić elementy.
Dodatkowo chcemy obsługiwać zdarzenie spadku, używając danych wyjściowych cdkDropListDropped
. Gdy cdkDropList
wywoła dane wyjściowe, wywołamy metodę drop
zadeklarowaną w elemencie AppComponent
i przekażemy bieżące zdarzenie jako argument.
Zauważ, że określamy również właściwość id
, która ma być używana jako identyfikator tego kontenera, oraz nazwę class
, abyśmy mogli nadać jej styl. Przyjrzyjmy się treściom elementu mat-card
. Oto dwa elementy:
- Akapit, który służy do wyświetlania tekstu „Pusta lista”, gdy na liście
todo
nie ma żadnych elementów - Komponent
app-task
. Zwróć uwagę, że tutaj przetwarzamy dane wyjścioweedit
, które zadeklarowano pierwotnie przez wywołanie metodyeditTask
z nazwą listy i obiektem$event
. Pomoże nam to zastąpić edytowane zadanie z odpowiedniej listy. Następnie, podobnie jak powyżej, powtarzamy listętodo
i przekazujemy dane wejściowetask
. Jednak tym razem dodajemy również dyrektywęcdkDrag
. Umożliwia przeciąganie pojedynczych zadań.
Aby wszystko działało, musisz zaktualizować plik app.module.ts
i zaimportować go do usługi DragDropModule
:
src/app/app.module.ts
...
import { DragDropModule } from '@angular/cdk/drag-drop';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
DragDropModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Musisz też zadeklarować tablicę inProgress
i done
, a także metody editTask
i drop
:
src/app/app.component.ts
...
import { CdkDragDrop, transferArrayItem } from '@angular/cdk/drag-drop';
@Component(...)
export class AppComponent {
todo: Task[] = [...];
inProgress: Task[] = [];
done: Task[] = [];
editTask(list: string, task: Task): void {}
drop(event: CdkDragDrop<Task[]|null>): void {
if (event.previousContainer === event.container) {
return;
}
if (!event.container.data || !event.previousContainer.data) {
return;
}
transferArrayItem(
event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex
);
}
}
Zwróć uwagę, że w metodzie drop
najpierw sprawdzamy, czy trafia ona na tę samą listę, z której pochodzi zadanie. W takim przypadku od razu wracamy. W przeciwnym razie bieżące zadanie zostanie przeniesione do docelowej ścieżki.
Wynik powinien wyglądać tak:
Na tym etapie możesz już przenieść elementy między tymi listami.
6. Tworzenie nowych zadań
Teraz zaimplementujemy funkcję tworzenia nowych zadań. W tym celu zaktualizujmy szablon strony 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>
Tworzymy element div
najwyższego poziomu wokół elementu container-wrapper
i dodajemy przycisk z &&t;add
" ikoną materiału obok etykiety "Dodaj zadanie. Potrzebujemy dodatkowego kodu, aby umieścić przycisk nad listą ścieżek, a następnie umieścić je obok siebie za pomocą flexbox. Ponieważ ten przycisk używa komponentu przycisku Materiał, musimy zaimportować odpowiedni moduł w elemencie AppModule
:
src/app/app.module.ts
...
import { MatButtonModule } from '@angular/material/button';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatButtonModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
A teraz zaimplementuj funkcję dodawania zadań w AppComponent
. Użyjemy okna Material Design. W oknie dialogowym pojawi się formularz z dwoma polami: tytuł i opis. Gdy użytkownik kliknie przycisk „Dodaj zadanie”, otworzymy to okno. Gdy użytkownik prześle formularz, nowo utworzone zadanie zostanie dodane do listy todo
.
Przyjrzyjmy się ogólnej implementacji tej funkcji w AppComponent
:
src/app/app.component.ts
...
import { MatDialog } from '@angular/material/dialog';
@Component(...)
export class AppComponent {
...
constructor(private dialog: MatDialog) {}
newTask(): void {
const dialogRef = this.dialog.open(TaskDialogComponent, {
width: '270px',
data: {
task: {},
},
});
dialogRef
.afterClosed()
.subscribe((result: TaskDialogResult|undefined) => {
if (!result) {
return;
}
this.todo.push(result.task);
});
}
}
Deklarujemy konstruktor, w którym wstawiamy klasę MatDialog
. newTask
:
- Otwórz nowe okno dialogowe za pomocą
TaskDialogComponent
, które zdefiniujemy trochę. - Określ, że okno ma mieć szerokość
270px.
- Przekaż puste zadanie w oknie jako dane. W
TaskDialogComponent
znajdziemy odniesienie do tego obiektu danych. - Zasubskrybujemy zdarzenie zamknięcia i dodajemy zadanie z obiektu
result
do tablicytodo
.
Aby upewnić się, że wszystko działa, musisz najpierw zaimportować MatDialogModule
w AppModule
:
src/app/app.module.ts
...
import { MatDialogModule } from '@angular/material/dialog';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatDialogModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
A teraz utwórzmy TaskDialogComponent
. Przejdź do katalogu src/app
i uruchom polecenie:
ng generate component task-dialog
Aby wdrożyć tę funkcję, otwórz ją i zastąp jej treść: src/app/task-dialog/task-dialog.component.html
src/app/task-dialog/task-dialog.component.html
<mat-form-field>
<mat-label>Title</mat-label>
<input matInput cdkFocusInitial [(ngModel)]="data.task.title" />
</mat-form-field>
<mat-form-field>
<mat-label>Description</mat-label>
<textarea matInput [(ngModel)]="data.task.description"></textarea>
</mat-form-field>
<div mat-dialog-actions>
<button mat-button [mat-dialog-close]="{ task: data.task }">OK</button>
<button mat-button (click)="cancel()">Cancel</button>
</div>
W szablonie powyżej tworzymy formularz z dwoma polami danych title
i description
. Dyrektywa cdkFocusInput
automatycznie ustawia pole wyboru title
, gdy użytkownik otworzy okno.
Zwróć uwagę, jak w szablonie odwołujemy się do właściwości data
komponentu. To będzie ta sama właściwość data
, którą przekazujemy do metody open
dialog
w AppComponent
. Aby zaktualizować tytuł i opis zadania, gdy użytkownik zmieni zawartość odpowiednich pól, stosujemy dwukierunkowe wiązanie danych z ngModel
.
Gdy użytkownik kliknie przycisk OK, automatycznie zwrócimy wynik { task: data.task }
, czyli zadanie, które mutowaliśmy za pomocą pól formularza w powyższym szablonie.
Teraz zaimplementuj kontroler komponentu:
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);
}
}
W elemencie TaskDialogComponent
wstawiamy odwołanie do okna dialogowego, abyśmy mogli je zamknąć, a także określamy wartość dostawcy powiązaną z tokenem MAT_DIALOG_DATA
. To jest obiekt danych, który przekazaliśmy do metody otwartej w AppComponent
powyżej. Deklarujemy również właściwość prywatną backupTask
, która jest kopią zadania, którą przekazaliśmy razem z obiektem danych.
Gdy użytkownik naciśnie przycisk Anuluj, przywracamy potencjalną zmianę właściwości this.data.task
i zamkniemy to okno, przesyłając wynik this.data
.
Mamy tu 2 typy, które nie zostały jeszcze zadeklarowane: TaskDialogData
i TaskDialogResult
. W dolnej części pliku src/app/task-dialog/task-dialog.component.ts
umieść te deklaracje:
src/app/task-dialog/task-dialog.component.ts
...
export interface TaskDialogData {
task: Partial<Task>;
enableDelete: boolean;
}
export interface TaskDialogResult {
task: Task;
delete?: boolean;
}
Ostatnią rzeczą, którą musimy zrobić, zanim wszystko będzie gotowe, jest zaimportowanie kilku modułów w AppModule
.
src/app/app.module.ts
...
import { MatInputModule } from '@angular/material/input';
import { FormsModule } from '@angular/forms';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatInputModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Kliknięcie przycisku „Dodaj zadanie” powinno spowodować wyświetlenie następującego interfejsu:
7. Ulepszanie stylów aplikacji
Aby aplikacja była bardziej atrakcyjna wizualnie, możemy nieco ulepszyć jej układ. Chcemy, aby pasy płynące znajdowały się obok siebie. Chcemy też wprowadzić drobne zmiany w przycisku „Dodaj zadanie” i pustą etykietę listy.
Otwórz src/app/app.component.css
i dodaj te style na dole:
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;
}
W powyższym fragmencie kodu dostosowujemy układ paska narzędzi i jego etykiety. Dopilnujemy też, aby treść była wyrównana w poziomie, ustawiając szerokość na 1400px
, a jej margines na auto
. Następnie korzystamy z flexbox, aby umieścić obok siebie linie ścienne i na tej podstawie wprowadzić pewne zmiany w sposobie wizualizacji zadań i pustych list.
Po ponownym załadowaniu aplikacji powinien wyświetlić się następujący interfejs:
Chociaż znacznie ulepszyliśmy nasze aplikacje, nadal występuje irytujący problem z przenoszeniem zadań:
Gdy przeciągniemy zadanie „Kup mleko”, zobaczysz 2 karty dla tego samego zadania – tę, którą przeciągamy i tę, która znajduje się na ścieżce. Angular CDK dostarcza nam nazw klas CSS, dzięki którym można rozwiązać ten problem.
Na dole elementu src/app/app.component.css
dodaj te zastąpienia stylu:
src/app/app.component.css
.cdk-drag-animating {
transition: transform 250ms;
}
.cdk-drag-placeholder {
opacity: 0;
}
Podczas przeciągania elementu Angular CDK&ponuje jego kopię, a następnie wstawia go w miejsce, w którym umieścimy oryginał. Aby mieć pewność, że ten element nie będzie widoczny, ustaw właściwość przezroczystości w klasie cdk-drag-placeholder
, którą CDK doda do zmiennej.
Dodatkowo, gdy odrzucamy element, CDK dodaje klasę cdk-drag-animating
. Aby wyświetlić płynną animację zamiast bezpośrednio przyciągać element, definiujemy przejście o czasie trwania 250ms
.
Chcemy też wprowadzić drobne modyfikacje stylów naszych zadań. W zasadzie task.component.css
zezwól elementowi wyświetlania na block
i ustaw marginesy:
src/app/task/task.component.css
:host {
display: block;
}
.item {
margin-bottom: 10px;
cursor: pointer;
}
8. Edytowanie i usuwanie dotychczasowych zadań
Aby edytować i usunąć istniejące zadania, wykorzystamy większość funkcji, które zostały już wdrożone. Gdy użytkownik kliknie dwukrotnie zadanie, otworzymy TaskDialogComponent
i wypełnimy oba pola formularza wartościami title
i description
zadania.
Na karcie TaskDialogComponent
znajdzie się też przycisk usuwania. Gdy użytkownik go kliknie, przekażemy instrukcję usuwania, która pojawi się w AppComponent
.
Jedyną zmianą, jaką musimy wprowadzić w zasadzie TaskDialogComponent
, jest szablon:
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>
Ten przycisk zawiera ikonę usuwania materiału. Gdy użytkownik go kliknie, zamkniemy to okno i przekażemy dosłowny obiekt { task: data.task, delete: true }
. Zwróć uwagę, że przycisk jest ustawiony na okrągły za pomocą zasady mat-fab
, ustawiamy jego kolor jako główny i pokazuje go tylko wtedy, gdy dane w oknie dialogowym mają włączone usuwanie.
Resztę funkcji edytowania i usuwania znajdziesz w AppComponent
. Zastąp jego metodę editTask
tymi fragmentami:
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;
}
});
}
...
}
Przeanalizujmy argumenty metody editTask
:
- Lista typu
'done' | 'todo' | 'inProgress',
, która jest typem ciągu literału z wartościami odpowiadającymi właściwościom poszczególnych ścieżek. - Bieżące zadanie, które chcesz edytować.
W treści metody najpierw otwieramy wystąpienie elementu TaskDialogComponent
. Obiekt data
przekazuje dosłowny obiekt, który wskazuje zadanie, które chcesz edytować, oraz włącza przycisk edycji w formularzu, ustawiając właściwość enableDelete
na true
.
Gdy pojawi się wynik, wyświetlimy 2 scenariusze:
- Gdy flaga
delete
jest ustawiona natrue
(tzn. gdy użytkownik naciśnie przycisk usuwania), usuniemy zadanie z odpowiedniej listy. - Możesz też zastąpić zadanie w indeksie podanym zadaniem z okna.
9. Tworzenie nowego projektu Firebase
A teraz utwórz nowy projekt Firebase.
- Otwórz Konsolę Firebase.
- Utwórz nowy projekt o nazwie "KanbanFire".
10. Dodaję Firebase do projektu
W tej sekcji zintegrujemy nasz projekt z Firebase. Zespół Firebase oferuje pakiet @angular/fire
, który zapewnia integrację między 2 technologiami. Aby dodać obsługę Firebase do aplikacji, otwórz katalog główny Workspace i uruchom:
ng add @angular/fire
Polecenie to zainstaluje pakiet @angular/fire
i zada Ci kilka pytań. W terminalu powinno pojawić się coś takiego:
W międzyczasie instalacja otworzy okno przeglądarki, aby umożliwić uwierzytelnienie za pomocą konta Firebase. Na koniec trzeba wybrać projekt Firebase i utworzyć pliki na dysku.
Następnie musimy utworzyć bazę danych Firestore! W sekcji "Cloud Firestore" kliknij "Utwórz bazę danych&"
Następnie utwórz bazę danych w trybie testowym:
Na koniec wybierz region:
Teraz wystarczy tylko dodać do środowiska konfigurację Firebase. Konfigurację projektu znajdziesz w konsoli Firebase.
- Kliknij ikonę koła zębatego obok Przeglądu projektu.
- Wybierz ustawienia projektu.
W sekcji „Twoje aplikacje” wybierz &aplikację;
Następnie zarejestruj aplikację i upewnij się, że włączona jest usługa „Hosting Firebase”:
Gdy klikniesz „Zarejestruj aplikację”, możesz skopiować konfigurację do src/environments/environment.ts
:
Na koniec plik konfiguracji powinien wyglądać tak:
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. Przenoszenie danych do Firestore
Po skonfigurowaniu pakietu Firebase SDK za pomocą @angular/fire
przenieśmy dane do Firestore. Najpierw zaimportujmy moduły, których potrzebujemy w 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 {}
Ponieważ będziemy używać Firestore, musimy wstrzyknąć AngularFirestore
do konstruktora AppComponent
:
src/app/app.component.ts
...
import { AngularFirestore } from '@angular/fire/firestore';
@Component({...})
export class AppComponent {
...
constructor(private dialog: MatDialog, private store: AngularFirestore) {}
...
}
Następnie aktualizujemy sposób inicjowania tablic tablicowych:
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[]>;
...
}
Tutaj korzystamy z AngularFirestore
, aby pobrać zawartość kolekcji bezpośrednio z bazy danych. Zauważ, że valueChanges
zwraca obserwowalny obiekt zamiast tablicy, a ponadto określamy, że pole identyfikatora dokumentów w tej kolekcji powinno mieć wartość id
, aby odpowiadać nazwie, której używamy w interfejsie Task
. Za każdym razem, gdy następuje zmiana, zwraca ona valueChanges
zadania.
Ze względu na to, że współpracujemy z obserwowalnymi obiektami zamiast tablic, musimy zaktualizować sposób dodawania, usuwania i edytowania zadań, a także funkcję przenoszenia zadań między ścieżkami. Zamiast mutować tablice w pamięci, do aktualizowania danych w bazie danych używamy pakietu SDK Firebase.
Najpierw zobaczmy, jak wygląda zmiana kolejności. Zamień metodę drop
w src/app/app.component.ts
na:
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
);
}
Nowy fragment kodu jest podświetlony w powyższym fragmencie kodu. Aby przenieść zadanie z bieżącej ścieżki do docelowej, usuniemy zadanie z pierwszej kolekcji i dodamy je do drugiej. Wykonujemy dwie operacje, które chcemy upodobnić do jednej (czyli to zrobisz ją atomowo), więc każdą z nich przeprowadzamy w ramach transakcji Firestore.
Teraz zaktualizujmy metodę editTask
, by używała Firestore. W module obsługi okna dialogowego musisz zmienić te wiersze kodu:
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);
}
});
...
Korzystamy z dokumentu docelowego odpowiadającego zadaniu, które obrabiamy przy użyciu pakietu SDK Firestore, i usuwamy je lub aktualizujemy.
Na koniec musimy zaktualizować metodę tworzenia nowych zadań. Zamień this.todo.push('task')
na: this.store.collection('todo').add(result.task)
.
Zwróć uwagę, że nasze kolekcje nie są tablicami, ale obserwowalnymi elementami. Aby móc je zwizualizować, musisz zaktualizować szablon szablonu AppComponent
. Wystarczy, że zastąpisz każdy dostęp do właściwości todo
, inProgress
i done
odpowiednimi wartościami todo | async
, inProgress | async
i done | async
.
Próbka asynchroniczna automatycznie subskrybujesz subskrypcje, które są powiązane z kolekcjami. Gdy obserwowane wartości emitują nową wartość, Angular automatycznie uruchamia wykrywanie zmian i przetwarza emitowaną tablicę.
Przyjrzyjmy się na przykład zmianom, które musimy wprowadzić w ścieżce todo
:
src/app/app.component.html
<mat-card
cdkDropList
id="todo"
#todoList="cdkDropList"
[cdkDropListData]="todo | async"
[cdkDropListConnectedTo]="[doneList, inProgressList]"
(cdkDropListDropped)="drop($event)"
class="list">
<p class="empty-label" *ngIf="(todo | async)?.length === 0">Empty list</p>
<app-task (edit)="editTask('todo', $event)" *ngFor="let task of todo | async" cdkDrag [task]="task"></app-task>
</mat-card>
Gdy przekazujemy dane do dyrektywy cdkDropList
, stosuję asynchroniczny potok. To jest to samo w dyrektywie *ngIf
, ale pamiętaj, że w przypadku uzyskiwania dostępu do właściwości length
stosujemy też opcjonalny łańcuch (nazywany w Angular bezpiecznym narzędziem do nawigacji), aby mieć pewność, że nie wystąpi błąd w czasie działania, gdy todo | async
nie jest null
lub undefined
.
Gdy utworzysz nowe zadanie w interfejsie użytkownika i otworzysz Firestore, powinno pojawić się coś takiego:
12. Poprawa optymistycznych aktualizacji
W aplikacji wprowadzamy obecnie aktualizacje optymistyczne. W Firestore mamy źródło danych zgodnych z prawdą, ale mamy też lokalne kopie zadań. Gdy którekolwiek z obserwacji związanych z kolekcjami wyemitują, otrzymujemy tabelę zadań. Gdy działanie użytkownika powoduje zmianę stanu, najpierw aktualizujemy wartości lokalne, a następnie rozpowszechniamy zmianę w Firestore.
Przenosząc zadanie z jednej ścieżki do drugiej, wywołujemy funkcję transferArrayItem,
, która działa w lokalnych instancjach tablic reprezentujących zadania w każdej ścieżce. Pakiet SDK Firebase traktuje te tablice jako niezmienne. Oznacza to, że gdy następnym razem Angular uruchomi wykrywanie zmian, uzyskamy nowe wystąpienia, które wyrenderują poprzedni stan, zanim przeniesiemy zadanie.
Jednocześnie uruchamiamy aktualizację Firestore i pakiet SDK Firebase uruchamia aktualizację z poprawnymi wartościami, więc w ciągu kilku milisekund interfejs uzyska odpowiedni stan. W ten sposób zadanie właśnie zostało przeniesione z pierwszej listy na następną. Dobrze widać to w pliku GIF poniżej:
Sposób rozwiązania tego problemu różni się w zależności od aplikacji, ale w każdym przypadku musimy dbać o utrzymanie spójnego stanu do czasu aktualizacji danych.
Możemy skorzystać z funkcji BehaviorSubject
, która pakuje pierwotny obserwator z usługi valueChanges
. Pod maską funkcji BehaviorSubject
znajdziesz zmienną tablicę, która zachowuje aktualizację z przeglądarki transferArrayItem
.
Aby zaimplementować poprawkę, wystarczy zaktualizować AppComponent
:
src/app/app.component.ts
...
import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore';
import { BehaviorSubject } from 'rxjs';
const getObservable = (collection: AngularFirestoreCollection<Task>) => {
const subject = new BehaviorSubject<Task[]>([]);
collection.valueChanges({ idField: 'id' }).subscribe((val: Task[]) => {
subject.next(val);
});
return subject;
};
@Component(...)
export class AppComponent {
todo = getObservable(this.store.collection('todo')) as Observable<Task[]>;
inProgress = getObservable(this.store.collection('inProgress')) as Observable<Task[]>;
done = getObservable(this.store.collection('done')) as Observable<Task[]>;
...
}
Tylko w powyższym fragmencie kodu tworzymy element BehaviorSubject
, który emituje wartość za każdym razem, gdy możliwa do zarejestrowania jest powiązana z tą kolekcją.
Wszystko działa zgodnie z oczekiwaniami, ponieważ element BehaviorSubject
ponownie używa tablicy w wywołaniach wykrywania zmian i jest aktualizowany tylko po otrzymaniu nowej wartości z Firestore.
13. Wdrożenie aplikacji
Potrzebujemy tylko wdrożenia aplikacji:
ng deploy
To polecenie spowoduje:
- Przygotuj aplikację z użyciem jej konfiguracji, optymalizując czas kompilacji.
- wdrożyć aplikację w Hostingu Firebase,
- Podaj adres URL, by zobaczyć podgląd wyniku.
14. Gratulacje
Gratulacje, udało Ci się utworzyć tablicę kanban za pomocą Angular i Firebase.
Utworzono interfejs z trzema kolumnami reprezentującymi stan różnych zadań. Za pomocą Angular CDK zaimplementowano przeciąganie i upuszczanie zadań w kolumnach. Następnie, korzystając z materiałów Angular, stworzysz formularz do tworzenia nowych zadań i edytowania istniejących. Następnie dowiesz się, jak używać @angular/fire
, i przenieść cały stan aplikacji do Firestore. Na koniec wdrożyliśmy aplikację w Hostingu Firebase.
Co dalej?
Pamiętaj, że aplikacja została wdrożona za pomocą konfiguracji testowych. Przed wdrożeniem aplikacji w wersji produkcyjnej skonfiguruj odpowiednie uprawnienia. Tutaj dowiesz się, jak to zrobić.
Obecnie nie zachowujemy kolejności poszczególnych zadań na określonej ścieżce. Aby to zrobić, możesz użyć pola zamówienia w dokumencie zadania i posortować je.
Ponadto deskę kanban tworzyliśmy tylko dla jednego użytkownika, co oznacza, że dla każdej osoby, która otworzy aplikację, korzysta z jednej tablicy. Aby zaimplementować osobne tablice dla różnych użytkowników aplikacji, należy zmienić strukturę bazy danych. Tutaj znajdziesz więcej informacji o sprawdzonych metodach Firestore.