1. Introduzione
Ultimo aggiornamento: 11/09/2020
Cosa creerai
In questo codelab, creeremo una bacheca kanban web con Angular e Firebase. La nostra app finale avrà tre categorie di attività: backlog, in corso e completate. Potremo creare, eliminare attività e trasferirle da una categoria all'altra utilizzando il trascinamento.
Svilupperemo l'interfaccia utente utilizzando Angular e Firestore come archivio persistente. Alla fine del codelab eseguiremo il deployment dell'app su Firebase Hosting utilizzando la CLI di Angular.
Cosa imparerai a fare
- Come utilizzare Angular Material e il CDK.
- Come aggiungere l'integrazione di Firebase alla tua app Angular.
- Come conservare i dati persistenti in Firestore.
- Come eseguire il deployment dell'app su Firebase Hosting utilizzando l'interfaccia a riga di comando di Angular con un unico comando.
Che cosa ti serve
Questo codelab presuppone che tu disponga di un Account Google e di una conoscenza di base di Angular e della CLI Angular.
Iniziamo.
2. Creare un nuovo progetto
Innanzitutto, creiamo un nuovo spazio di lavoro Angular:
ng new kanban-fire
? Would you like to add Angular routing? No
? Which stylesheet format would you like to use? CSS
Questo passaggio potrebbe richiedere alcuni minuti. Angular CLI crea la struttura del progetto e installa tutte le dipendenze. Al termine della procedura di installazione, vai alla directory kanban-fire
e avvia il server di sviluppo di Angular CLI:
ng serve
Apri http://localhost:4200 e dovresti vedere un output simile a questo:
Nell'editor, apri src/app/app.component.html
ed elimina tutti i contenuti. Quando torni alla pagina http://localhost:4200, dovresti vedere una pagina vuota.
3. Aggiunta di Material e del CDK
Angular include l'implementazione di componenti dell'interfaccia utente conformi a Material Design come parte del pacchetto @angular/material
. Una delle dipendenze di @angular/material
è il Component Development Kit o CDK. Il CDK fornisce primitive, come utilità di accessibilità, trascinamento e sovrapposizione. Distribuiamo il CDK nel pacchetto @angular/cdk
.
Per aggiungere materiale alla tua corsa:
ng add @angular/material
Questo comando ti chiede di scegliere un tema, se vuoi utilizzare gli stili tipografici globali di Material e se vuoi configurare le animazioni del browser per Angular Material. Scegli "Indaco/Rosa" per ottenere lo stesso risultato di questo codelab e rispondi "Sì" alle ultime due domande.
Il comando ng add
installa @angular/material
, le relative dipendenze e importa BrowserAnimationsModule
in AppModule
. Nel passaggio successivo, possiamo iniziare a utilizzare i componenti offerti da questo modulo.
Innanzitutto, aggiungiamo una barra degli strumenti e un'icona a AppComponent
. Apri app.component.html
e aggiungi il seguente markup:
src/app/app.component.html
<mat-toolbar color="primary">
<mat-icon>local_fire_department</mat-icon>
<span>Kanban Fire</span>
</mat-toolbar>
Qui aggiungiamo una barra degli strumenti utilizzando il colore principale del nostro tema Material Design e al suo interno utilizziamo l'icona local_fire_depeartment
accanto all'etichetta "Kanban Fire". Se ora guardi la console, vedrai che Angular genera alcuni errori. Per correggerli, assicurati di aggiungere le seguenti importazioni a 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 { }
Poiché utilizziamo la barra degli strumenti e l'icona di Angular Material, dobbiamo importare i moduli corrispondenti in AppModule
.
Ora sullo schermo dovresti vedere quanto segue:
Non male con sole 4 righe di HTML e due importazioni.
4. Visualizzare le attività
Come passaggio successivo, creiamo un componente che possiamo utilizzare per visualizzare le attività nella bacheca Kanban.
Vai alla directory src/app
ed esegui questo comando della CLI:
ng generate component task
Questo comando genera TaskComponent
e aggiunge la relativa dichiarazione a AppModule
. All'interno della directory task
, crea un file denominato task.ts
. Utilizzeremo questo file per definire l'interfaccia delle attività nel tabellone Kanban. Ogni attività avrà campi facoltativi id
, title
e description
di tipo stringa:
src/app/task/task.ts
export interface Task {
id?: string;
title: string;
description: string;
}
Ora aggiorniamo task.component.ts
. Vogliamo che TaskComponent
accetti come input un oggetto di tipo Task
e che sia in grado di emettere gli output "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>();
}
Modifica il modello di TaskComponent
. Apri task.component.html
e sostituisci i contenuti con il seguente codice 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>
Nota che ora vengono visualizzati errori nella console:
'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
Nel modello precedente utilizziamo il componente mat-card
di @angular/material
, ma non abbiamo importato il modulo corrispondente nell'app. Per correggere l'errore riportato sopra, dobbiamo importare MatCardModule
in AppModule
:
src/app/app.module.ts
...
import { MatCardModule } from '@angular/material/card';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatCardModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Successivamente creeremo alcune attività in AppComponent
e le visualizzeremo utilizzando TaskComponent
.
In AppComponent
definisci un array chiamato todo
e al suo interno aggiungi due attività:
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!'
}
];
}
Ora, in fondo a app.component.html
aggiungi la seguente direttiva *ngFor
:
src/app/app.component.html
<app-task *ngFor="let task of todo" [task]="task"></app-task>
Quando apri il browser, dovresti visualizzare quanto segue:
5. Implementazione del trascinamento per le attività
Ora siamo pronti per la parte divertente. Creiamo tre corsie per i tre diversi stati in cui possono trovarsi le attività e, utilizzando Angular CDK, implementiamo una funzionalità di trascinamento.
In app.component.html
, rimuovi il componente app-task
con la direttiva *ngFor
in alto e sostituiscilo con:
src/app/app.component.html
<div class="content-wrapper">
<div class="container-wrapper">
<div class="container">
<h2>Backlog</h2>
<mat-card
cdkDropList
id="todo"
#todoList="cdkDropList"
[cdkDropListData]="todo"
[cdkDropListConnectedTo]="[doneList, inProgressList]"
(cdkDropListDropped)="drop($event)"
class="list">
<p class="empty-label" *ngIf="todo.length === 0">Empty list</p>
<app-task (edit)="editTask('todo', $event)" *ngFor="let task of todo" cdkDrag [task]="task"></app-task>
</mat-card>
</div>
<div class="container">
<h2>In progress</h2>
<mat-card
cdkDropList
id="inProgress"
#inProgressList="cdkDropList"
[cdkDropListData]="inProgress"
[cdkDropListConnectedTo]="[todoList, doneList]"
(cdkDropListDropped)="drop($event)"
class="list">
<p class="empty-label" *ngIf="inProgress.length === 0">Empty list</p>
<app-task (edit)="editTask('inProgress', $event)" *ngFor="let task of inProgress" cdkDrag [task]="task"></app-task>
</mat-card>
</div>
<div class="container">
<h2>Done</h2>
<mat-card
cdkDropList
id="done"
#doneList="cdkDropList"
[cdkDropListData]="done"
[cdkDropListConnectedTo]="[todoList, inProgressList]"
(cdkDropListDropped)="drop($event)"
class="list">
<p class="empty-label" *ngIf="done.length === 0">Empty list</p>
<app-task (edit)="editTask('done', $event)" *ngFor="let task of done" cdkDrag [task]="task"></app-task>
</mat-card>
</div>
</div>
</div>
Qui succedono molte cose. Esaminiamo le singole parti di questo snippet passo dopo passo. Questa è la struttura di primo livello del modello:
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>
Qui creiamo un div
che racchiude tutte e tre le corsie, con il nome della classe "container-wrapper
". Ogni corsia ha un nome della classe "container
" e un titolo all'interno di un tag h2
.
Ora esaminiamo la struttura della prima corsia:
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>
...
Innanzitutto, definiamo la corsia come mat-card
, che utilizza la direttiva cdkDropList
. Utilizziamo un mat-card
a causa degli stili forniti da questo componente. cdkDropList
ci consentirà in seguito di rilasciare le attività all'interno dell'elemento. Abbiamo anche impostato i seguenti due input:
cdkDropListData
: input dell'elenco a discesa che consente di specificare l'array di daticdkDropListConnectedTo
: riferimenti agli altricdkDropList
a cui è connesso l'cdkDropList
corrente. Impostando questo input, specifichiamo in quali altre liste possiamo inserire gli elementi
Inoltre, vogliamo gestire l'evento di rilascio utilizzando l'output cdkDropListDropped
. Una volta che cdkDropList
emette questo output, invocheremo il metodo drop
dichiarato all'interno di AppComponent
e passeremo l'evento corrente come argomento.
Tieni presente che specifichiamo anche un id
da utilizzare come identificatore per questo contenitore e un nome class
per poterlo stilizzare. Ora esaminiamo i nodi secondari di mat-card
. I due elementi che abbiamo sono:
- Un paragrafo, che utilizziamo per mostrare il testo "Elenco vuoto" quando non sono presenti elementi nell'elenco
todo
- Il componente
app-task
. Tieni presente che qui gestiamo l'outputedit
che abbiamo dichiarato originariamente chiamando il metodoeditTask
con il nome dell'elenco e l'oggetto$event
. In questo modo, potremo sostituire l'attività modificata con quella dell'elenco corretto. Successivamente, iteriamo l'elencotodo
come abbiamo fatto sopra e passiamo l'inputtask
. Questa volta, però, aggiungiamo anche la direttivacdkDrag
. Rende trascinabili le singole attività.
Per far funzionare tutto questo, dobbiamo aggiornare app.module.ts
e includere un'importazione in DragDropModule
:
src/app/app.module.ts
...
import { DragDropModule } from '@angular/cdk/drag-drop';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
DragDropModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Dobbiamo anche dichiarare gli array inProgress
e done
, insieme ai metodi editTask
e 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[]>): 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
);
}
}
Tieni presente che nel metodo drop
controlliamo innanzitutto di rilasciare l'attività nella stessa lista da cui proviene. In questo caso, il pagamento viene restituito immediatamente. In caso contrario, trasferiamo l'attività corrente nella corsia di destinazione.
Il risultato dovrebbe essere:
A questo punto dovresti già essere in grado di trasferire elementi tra i due elenchi.
6. Creare nuove attività
Ora implementiamo una funzionalità per creare nuove attività. A questo scopo, aggiorniamo il modello di 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>
Creiamo un elemento div
di primo livello intorno a container-wrapper
e aggiungiamo un pulsante con un'icona Material "add
" accanto all'etichetta "Aggiungi attività". Abbiamo bisogno del wrapper aggiuntivo per posizionare il pulsante sopra l'elenco delle corsie, che in seguito posizioneremo una accanto all'altra utilizzando Flexbox. Poiché questo pulsante utilizza il componente pulsante Material, dobbiamo importare il modulo corrispondente in AppModule
:
src/app/app.module.ts
...
import { MatButtonModule } from '@angular/material/button';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatButtonModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Ora implementiamo la funzionalità per aggiungere attività in AppComponent
. Utilizzeremo una finestra di dialogo Material. Nella finestra di dialogo verrà visualizzato un modulo con due campi: titolo e descrizione. Quando l'utente fa clic sul pulsante "Aggiungi attività", si apre la finestra di dialogo e quando l'utente invia il modulo, l'attività appena creata viene aggiunta all'elenco todo
.
Diamo un'occhiata all'implementazione di alto livello di questa funzionalità in 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);
});
}
}
Dichiariamo un costruttore in cui inseriamo la classe MatDialog
. All'interno del newTask
:
- Apri una nuova finestra di dialogo utilizzando
TaskDialogComponent
, che definiremo tra poco. - Specifica che la finestra di dialogo deve avere una larghezza di
270px.
- Passa un'attività vuota alla finestra di dialogo come dati. In
TaskDialogComponent
potremo ottenere un riferimento a questo oggetto dati. - Ci abboniamo all'evento di chiusura e aggiungiamo l'attività dall'oggetto
result
all'arraytodo
.
Per assicurarci che funzioni, dobbiamo prima importare MatDialogModule
in AppModule
:
src/app/app.module.ts
...
import { MatDialogModule } from '@angular/material/dialog';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatDialogModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Ora creiamo il TaskDialogComponent
. Vai alla directory src/app
ed esegui:
ng generate component task-dialog
Per implementarne la funzionalità, apri prima src/app/task-dialog/task-dialog.component.html
e sostituisci il relativo contenuto con:
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>
Nel modello precedente creiamo un modulo con due campi per title
e description
. Utilizziamo la direttiva cdkFocusInput
per mettere automaticamente il focus sull'input title
quando l'utente apre la finestra di dialogo.
Nota come all'interno del modello facciamo riferimento alla proprietà data
del componente. Sarà lo stesso data
che passiamo al metodo open
di dialog
in AppComponent
. Per aggiornare il titolo e la descrizione dell'attività quando l'utente modifica il contenuto dei campi corrispondenti, utilizziamo il data binding bidirezionale con ngModel
.
Quando l'utente fa clic sul pulsante OK, restituiamo automaticamente il risultato { task: data.task }
, ovvero l'attività che abbiamo modificato utilizzando i campi del modulo nel modello precedente.
Ora implementiamo il controller del componente:
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
inseriamo un riferimento alla finestra di dialogo, in modo da poterla chiudere, e inseriamo anche il valore del fornitore associato al token MAT_DIALOG_DATA
. Questo è l'oggetto dati che abbiamo passato al metodo open in AppComponent
sopra. Dichiariamo anche la proprietà privata backupTask
, che è una copia dell'attività che abbiamo superato insieme all'oggetto dati.
Quando l'utente preme il pulsante Annulla, ripristiniamo le proprietà eventualmente modificate di this.data.task
e chiudiamo la finestra di dialogo, passando this.data
come risultato.
Abbiamo fatto riferimento a due tipi che non abbiamo ancora dichiarato: TaskDialogData
e TaskDialogResult
. All'interno di src/app/task-dialog/task-dialog.component.ts
, aggiungi le seguenti dichiarazioni alla fine del file:
src/app/task-dialog/task-dialog.component.ts
...
export interface TaskDialogData {
task: Partial<Task>;
enableDelete: boolean;
}
export interface TaskDialogResult {
task: Task;
delete?: boolean;
}
L'ultima cosa da fare prima di avere la funzionalità pronta è importare alcuni moduli in 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 { }
Quando fai clic sul pulsante "Aggiungi attività", dovresti visualizzare la seguente interfaccia utente:
7. Miglioramento degli stili dell'app
Per rendere l'applicazione più accattivante dal punto di vista visivo, ne miglioreremo il layout modificando leggermente gli stili. Vogliamo posizionare le corsie una accanto all'altra. Vogliamo anche apportare alcune piccole modifiche al pulsante "Aggiungi attività" e all'etichetta dell'elenco vuoto.
Apri src/app/app.component.css
e aggiungi gli stili seguenti in fondo:
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;
}
Nello snippet precedente, modifichiamo il layout della barra degli strumenti e della relativa etichetta. Inoltre, ci assicuriamo che i contenuti siano allineati orizzontalmente impostando la larghezza su 1400px
e il margine su auto
. Successivamente, utilizzando Flexbox, abbiamo posizionato le corsie una accanto all'altra e infine abbiamo apportato alcune modifiche al modo in cui visualizziamo le attività e gli elenchi vuoti.
Una volta ricaricata l'app, dovresti visualizzare la seguente interfaccia utente:
Sebbene abbiamo migliorato notevolmente gli stili della nostra app, abbiamo ancora un problema fastidioso quando spostiamo le attività:
Quando iniziamo a trascinare l'attività"Compra latte", vediamo due schede per la stessa attività: quella che stiamo trascinando e quella nella corsia. L'Angular CDK ci fornisce nomi di classi CSS che possiamo utilizzare per risolvere questo problema.
Aggiungi i seguenti override di stile alla fine di src/app/app.component.css
:
src/app/app.component.css
.cdk-drag-animating {
transition: transform 250ms;
}
.cdk-drag-placeholder {
opacity: 0;
}
Mentre trasciniamo un elemento, la funzionalità di trascinamento della CDK di Angular lo clona e lo inserisce nella posizione in cui rilasceremo l'originale. Per assicurarci che questo elemento non sia visibile, impostiamo la proprietà di opacità nella classe cdk-drag-placeholder
, che il CDK aggiungerà al segnaposto.
Inoltre, quando rilasciamo un elemento, il CDK aggiunge la classe cdk-drag-animating
. Per mostrare un'animazione fluida anziché spostare direttamente l'elemento, definiamo una transizione con durata 250ms
.
Vogliamo anche apportare alcune piccole modifiche agli stili delle nostre attività. In task.component.css
impostiamo la visualizzazione dell'elemento host su block
e impostiamo alcuni margini:
src/app/task/task.component.css
:host {
display: block;
}
.item {
margin-bottom: 10px;
cursor: pointer;
}
8. Modificare ed eliminare le attività esistenti
Per modificare e rimuovere le attività esistenti, riutilizzeremo la maggior parte delle funzionalità che abbiamo già implementato. Quando l'utente fa doppio clic su un'attività, si apre TaskDialogComponent
e i due campi del modulo vengono compilati con title
e description
dell'attività.
Al TaskDialogComponent
aggiungeremo anche un pulsante di eliminazione. Quando l'utente fa clic, trasmettiamo un'istruzione di eliminazione, che finirà in AppComponent
.
L'unica modifica da apportare in TaskDialogComponent
riguarda il modello:
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>
Questo pulsante mostra l'icona Elimina materiale. Quando l'utente fa clic, chiudiamo la finestra di dialogo e passiamo il valore letterale dell'oggetto { task: data.task, delete: true }
come risultato. Inoltre, nota che rendiamo il pulsante circolare utilizzando mat-fab
, impostiamo il colore su primario e lo mostriamo solo quando l'eliminazione dei dati della finestra di dialogo è abilitata.
Il resto dell'implementazione delle funzionalità di modifica ed eliminazione si trova in AppComponent
. Sostituisci il metodo editTask
con il seguente:
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;
}
});
}
...
}
Esaminiamo gli argomenti del metodo editTask
:
- Un elenco di tipo
'done' | 'todo' | 'inProgress',
, che è un tipo di unione letterale di stringhe con valori corrispondenti alle proprietà associate alle singole corsie. - L'attività corrente che vogliamo modificare.
Nel corpo del metodo, apriamo innanzitutto un'istanza di TaskDialogComponent
. Come data
passiamo un oggetto letterale, che specifica l'attività che vogliamo modificare e attiva anche il pulsante di modifica nel modulo impostando la proprietà enableDelete
su true
.
Quando riceviamo il risultato dalla finestra di dialogo, gestiamo due scenari:
- Quando il flag
delete
è impostato sutrue
(ovvero quando l'utente ha premuto il pulsante di eliminazione), rimuoviamo l'attività dall'elenco corrispondente. - In alternativa, sostituiamo l'attività nell'indice specificato con l'attività ottenuta dal risultato della finestra di dialogo.
9. Creare un nuovo progetto Firebase
Ora creiamo un nuovo progetto Firebase.
- Vai alla Console Firebase.
- Crea un nuovo progetto con il nome "KanbanFire".
10. Aggiunta di Firebase al progetto
In questa sezione integreremo il nostro progetto con Firebase. Il team di Firebase offre il pacchetto @angular/fire
, che fornisce l'integrazione tra le due tecnologie. Per aggiungere il supporto di Firebase alla tua app, apri la directory principale del tuo workspace ed esegui:
ng add @angular/fire
Questo comando installa il pacchetto @angular/fire
e ti pone alcune domande. Nel terminale dovresti vedere qualcosa di simile a questo:
Nel frattempo, l'installazione apre una finestra del browser in modo che tu possa eseguire l'autenticazione con il tuo account Firebase. Infine, ti chiede di scegliere un progetto Firebase e crea alcuni file sul disco.
Successivamente, dobbiamo creare un database Firestore. Nella sezione "Cloud Firestore", fai clic su "Crea database".
Dopodiché, crea un database in modalità test:
Infine, seleziona una regione:
L'unica cosa che resta da fare è aggiungere la configurazione Firebase al tuo ambiente. Puoi trovare la configurazione del progetto nella console Firebase.
- Fai clic sull'icona a forma di ingranaggio accanto a Panoramica del progetto.
- Scegli Impostazioni progetto.
Nella sezione "Le tue app", seleziona un'app web:
Successivamente, registra la tua applicazione e assicurati di attivare "Firebase Hosting":
Dopo aver fatto clic su "Registra app", puoi copiare la configurazione in src/environments/environment.ts
:
Alla fine, il file di configurazione dovrebbe avere il seguente aspetto:
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. Spostamento dei dati in Firestore
Ora che abbiamo configurato l'SDK Firebase, utilizziamo @angular/fire
per spostare i dati in Firestore. Innanzitutto, importiamo i moduli di cui abbiamo bisogno 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 {}
Poiché utilizzeremo Firestore, dobbiamo inserire AngularFirestore
nel costruttore di AppComponent
:
src/app/app.component.ts
...
import { AngularFirestore } from '@angular/fire/firestore';
@Component({...})
export class AppComponent {
...
constructor(private dialog: MatDialog, private store: AngularFirestore) {}
...
}
Successivamente, aggiorniamo il modo in cui inizializziamo gli array delle corsie:
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[]>;
...
}
Qui utilizziamo AngularFirestore
per ottenere i contenuti della raccolta direttamente dal database. Tieni presente che valueChanges
restituisce un observable anziché un array e che specifichiamo che il campo ID per i documenti in questa raccolta deve essere chiamato id
in modo che corrisponda al nome che utilizziamo nell'interfaccia Task
. L'observable restituito da valueChanges
emette una raccolta di attività ogni volta che cambia.
Poiché lavoriamo con gli osservabili anziché con gli array, dobbiamo aggiornare il modo in cui aggiungiamo, rimuoviamo e modifichiamo le attività, nonché la funzionalità per spostare le attività tra le corsie. Anziché modificare gli array in memoria, utilizzeremo l'SDK Firebase per aggiornare i dati nel database.
Per prima cosa, vediamo come apparirebbe il riordino. Sostituisci il metodo drop
in src/app/app.component.ts
con:
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
);
}
Nello snippet riportato sopra, il nuovo codice è evidenziato. Per spostare un'attività dalla corsia attuale a quella di destinazione, la rimuoveremo dalla prima raccolta e la aggiungeremo alla seconda. Poiché eseguiamo due operazioni che vogliamo che sembrino una sola (ovvero rendiamo l'operazione atomica), le eseguiamo in una transazione Firestore.
Dopodiché, aggiorniamo il metodo editTask
per utilizzare Firestore. All'interno del gestore della finestra di dialogo di chiusura, dobbiamo modificare le seguenti righe di codice:
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);
}
});
...
Accediamo al documento di destinazione corrispondente all'attività che manipoliamo utilizzando l'SDK Firestore ed eliminiamo o aggiorniamo il documento.
Infine, dobbiamo aggiornare il metodo per creare nuove attività. Sostituisci this.todo.push('task')
con: this.store.collection('todo').add(result.task)
.
Tieni presente che ora le nostre raccolte non sono array, ma osservabili. Per poterli visualizzare, dobbiamo aggiornare il modello di AppComponent
. Basta sostituire ogni accesso alle proprietà todo
, inProgress
e done
con todo | async
, inProgress | async
e done | async
rispettivamente.
La pipe asincrona esegue automaticamente la sottoscrizione agli osservabili associati alle raccolte. Quando gli osservabili emettono un nuovo valore, Angular esegue automaticamente il rilevamento delle modifiche ed elabora l'array emesso.
Ad esempio, esaminiamo le modifiche che dobbiamo apportare nella corsia 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>
Quando passiamo i dati alla direttiva cdkDropList
, applichiamo la pipe asincrona. È lo stesso all'interno della direttiva *ngIf
, ma tieni presente che utilizziamo anche l'incatenamento opzionale (noto anche come operatore di navigazione sicura in Angular) quando accediamo alla proprietà length
per assicurarci di non ricevere un errore di runtime se todo | async
non è null
o undefined
.
Ora, quando crei una nuova attività nell'interfaccia utente e apri Firestore, dovresti vedere qualcosa di simile a questo:
12. Miglioramento degli aggiornamenti ottimistici
Nell'applicazione stiamo attualmente eseguendo aggiornamenti ottimistici. Abbiamo la nostra fonte di verità in Firestore, ma allo stesso tempo abbiamo copie locali delle attività; quando viene emesso uno degli osservabili associati alle raccolte, otteniamo un array di attività. Quando un'azione utente modifica lo stato, aggiorniamo prima i valori locali e poi propaghiamo la modifica a Firestore.
Quando spostiamo un'attività da una corsia all'altra, richiamiamo transferArrayItem,
, che opera sulle istanze locali degli array che rappresentano le attività in ogni corsia. L'SDK Firebase considera questi array come immutabili, il che significa che la prossima volta che Angular esegue il rilevamento delle modifiche, otterremo nuove istanze, che eseguiranno il rendering dello stato precedente prima di trasferire l'attività.
Allo stesso tempo, attiviamo un aggiornamento di Firestore e l'SDK Firebase attiva un aggiornamento con i valori corretti, quindi in pochi millisecondi l'interfaccia utente raggiungerà il suo stato corretto. In questo modo, l'attività appena trasferita passa dal primo elenco a quello successivo. Puoi vedere bene questo aspetto nella GIF di seguito:
Il modo corretto per risolvere questo problema varia da applicazione ad applicazione, ma in tutti i casi dobbiamo assicurarci di mantenere uno stato coerente fino all'aggiornamento dei dati.
Possiamo sfruttare BehaviorSubject
, che esegue il wrapping dell'observer originale che riceviamo da valueChanges
. Dietro le quinte, BehaviorSubject
mantiene un array modificabile che conserva l'aggiornamento da transferArrayItem
.
Per implementare una correzione, è sufficiente aggiornare 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[]>;
...
}
Tutto ciò che facciamo nello snippet precedente è creare un BehaviorSubject
, che emette un valore ogni volta che cambia l'observable associato alla raccolta.
Tutto funziona come previsto, perché BehaviorSubject
riutilizza l'array nelle invocazioni di rilevamento delle modifiche e si aggiorna solo quando riceviamo un nuovo valore da Firestore.
13. Implementazione dell'applicazione
Per eseguire il deployment della nostra app, dobbiamo solo eseguire:
ng deploy
Questo comando:
- Crea la tua app con la configurazione di produzione, applicando le ottimizzazioni in fase di compilazione.
- Esegui il deployment della tua app in Firebase Hosting.
- Restituisci un URL in modo da poter visualizzare l'anteprima del risultato.
14. Complimenti
Congratulazioni, hai creato correttamente una bacheca Kanban con Angular e Firebase.
Hai creato un'interfaccia utente con tre colonne che rappresentano lo stato di diverse attività. Utilizzando Angular CDK, hai implementato il trascinamento delle attività tra le colonne. Poi, utilizzando Angular Material, hai creato un modulo per creare nuove attività e modificare quelle esistenti. Poi hai imparato a utilizzare @angular/fire
e hai spostato tutto lo stato dell'applicazione su Firestore. Infine, hai eseguito il deployment dell'applicazione in Firebase Hosting.
Passaggi successivi
Ricorda che abbiamo eseguito il deployment dell'applicazione utilizzando configurazioni di test. Prima di eseguire il deployment della tua app in produzione, assicurati di configurare le autorizzazioni corrette. Puoi scoprire come farlo qui.
Al momento, non conserviamo l'ordine delle singole attività in una determinata corsia. Per implementare questa funzionalità, puoi utilizzare un campo di ordinamento nel documento delle attività e ordinare in base a questo campo.
Inoltre, abbiamo creato la bacheca Kanban per un solo utente, il che significa che abbiamo una sola bacheca Kanban per chiunque apra l'app. Per implementare bacheche separate per diversi utenti dell'app, devi modificare la struttura del database. Scopri di più sulle best practice di Firestore qui.