Creazione di un'applicazione web con Angular e Firebase

1. Introduzione

Ultimo aggiornamento: 11-09-2020

Cosa imparerai a realizzare

In questo codelab, creeremo una lavagna web con Angular e Firebase. L'app finale prevede tre categorie di attività: backlog, in corso e completate. Potremo creare, eliminare e trasferire le attività da una categoria all'altra con il trascinamento.

Svilupperemo l'interfaccia utente utilizzando Angular e utilizzeremo Firestore come negozio permanente. Alla fine del codelab, eseguiremo il deployment dell'app in Firebase Hosting utilizzando l'interfaccia a riga di comando Angular.

b23bd3732d0206b.png

Cosa imparerai a fare:

  • Come utilizzare il materiale Angular e il CDK.
  • Come aggiungere l'integrazione di Firebase alla tua app Angular.
  • Come mantenere i tuoi dati persistenti in Firestore.
  • Come eseguire il deployment dell'app in Firebase Hosting utilizzando l'interfaccia a riga di comando Angular con un singolo comando.

Che cosa ti serve

Questo codelab presuppone che tu disponga di un Account Google e che tu abbia una conoscenza di base di Angular e dell'interfaccia a riga di comando Angular.

Iniziamo!

2. Creazione di un nuovo progetto

Innanzitutto, creiamo una nuova area di lavoro Angular:

ng new kanban-fire
? Would you like to add Angular routing? No
? Which stylesheet format would you like to use? CSS

Questa operazione potrebbe richiedere alcuni minuti. L'interfaccia a riga di comando Angular crea la struttura del tuo progetto e installa tutte le dipendenze. Al termine del processo di installazione, vai alla directory kanban-fire e avvia il server di sviluppo dell'interfaccia a riga di comando Angular:

ng serve

Apri http://localhost:4200 e dovresti vedere un output simile al seguente:

5ede7bc5b1109bf3.png

Nell'editor, apri src/app/app.component.html ed elimina tutti i suoi contenuti. Quando torni alla pagina http://localhost:4200, dovresti vedere una pagina vuota.

3. Aggiunta di materiale e CDK

Angular viene implementato con un'implementazione di componenti dell'interfaccia utente conformi al material design che fanno parte del pacchetto @angular/material. Una delle dipendenze di @angular/material è il component Development Kit o CDK. La CDK fornisce le primitive, ad esempio utilità a11y, trascinamento e overlay. Distribuiamo il CDK nel pacchetto @angular/cdk.

Per aggiungere materiale all'esecuzione dell'app:

ng add @angular/material

Questo comando ti chiede di scegliere un tema, se vuoi usare gli stili tipografici globali e se vuoi configurare le animazioni del browser per il materiale Angular. Scegli "Indaco/Rosa" per ottenere lo stesso risultato di questo codelab e rispondi con "Sì" alle ultime due domande.

Il comando ng add installa @angular/material, le sue 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 di material design e al suo interno usiamo l'icona local_fire_depeartment accanto all'etichetta "Kanban Fire". Se esamini la console ora, noterai 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 l'icona e la barra degli strumenti del materiale Angular, dobbiamo importare i moduli corrispondenti in AppModule.

Nella schermata dovrebbero essere visualizzate le seguenti informazioni:

a39cf8f8428a03bc.png

Non è male con solo quattro righe di codice HTML e due importazioni!

4. Visualizzazione delle attività

Nel passaggio successivo, creiamo un componente che possiamo utilizzare per visualizzare le attività nella bacheca di kanban.

Vai alla directory src/app ed esegui il seguente comando dell'interfaccia a riga di comando:

ng generate component task

Questo comando genera l'elemento TaskComponent e aggiunge la sua dichiarazione all'elemento AppModule. All'interno della directory task, crea un file denominato task.ts. Useremo questo file per definire l'interfaccia delle attività nella bacheca di Kankan. Ogni attività avrà campi id, title e description facoltativi, tutti 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 tipo di tipo Task e vogliamo che sia possibile 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 relativi contenuti con il seguente 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>

Tieni presente 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 sopra riportato stiamo utilizzando il componente mat-card da @angular/material, ma non abbiamo importato il modulo corrispondente nell'app. Per correggere l'errore, devi 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 { }

Ora creiamo alcune attività in AppComponent e le visualizziamo 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 istruzione *ngFor:

src/app/app.component.html

<app-task *ngFor="let task of todo" [task]="task"></app-task>

Quando apri il browser, dovresti vedere quanto segue:

d96fccd13c63ceb1.png

5. Implementare il trascinamento per le attività

Siamo pronti per la parte divertente. Crea tre nuotate per i tre diversi stati in cui è possibile svolgere e utilizzando la funzionalità Angular CDK per implementare una funzionalità di trascinamento.

In app.component.html, rimuovi il componente app-task con l'istruzione *ngFor in alto e sostituiscilo con:

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>

C'è molto da fare. Diamo un'occhiata alle singole parti di questo snippet passo 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 aggrega tutti e tre i costumi, 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 diamo un'occhiata alla struttura della prima piscina:

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

Per prima cosa definiamo mat-card il nuoto, che utilizza l'istruzione cdkDropList. Usiamo un mat-card a causa degli stili forniti da questo componente. In seguito cdkDropList ci consentirà di inserire le attività all'interno dell'elemento. Inoltre, impostiamo i seguenti due input:

  • cdkDropListData: input dell'elenco a discesa che consente di specificare l'array di dati
  • cdkDropListConnectedTo: riferimenti agli altri cdkDropList a cui è collegato il cdkDropList attuale. Se imposti questo valore, specifichiamo in quali altri elenchi puoi inserire elementi

Inoltre, vogliamo gestire l'evento di rilascio utilizzando l'output di cdkDropListDropped. Dopo che cdkDropList emette questo output, stiamo richiamando il metodo drop dichiarato all'interno di AppComponent e trasmettiamo l'evento corrente come argomento.

Tieni presente che specifichiamo anche un id da utilizzare come identificatore per questo contenitore e un nome nel campo class in modo da poterlo applicare come stile. Ora diamo un'occhiata ai contenuti secondari di mat-card. Abbiamo due elementi:

  • Un paragrafo che usiamo per mostrare il testo "Elenco vuoto" quando non sono presenti elementi nell'elenco todo.
  • Il componente app-task. Tieni presente che stiamo gestendo l'output edit che abbiamo dichiarato originariamente chiamando il metodo editTask con il nome dell'elenco e l'oggetto $event. Questo ci aiuterà a sostituire l'attività modificata dall'elenco corretto. Analizziamo l'elenco todo come abbiamo fatto sopra e trasmettiamo l'input di task. Tuttavia, questa volta aggiungiamo anche l'istruzione cdkDrag. In questo modo le singole attività possono essere trascinate.

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[]|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
    );
  }
}

Nota che nel metodo drop verifichiamo innanzitutto che lo stesso elenco provenga dall'attività. In tal caso, torniamo immediatamente. Altrimenti, trasferiamo l'attività corrente alla corsia di destinazione.

Il risultato dovrebbe essere:

460f86bcd10454cf.png

A questo punto dovresti essere già in grado di trasferire elementi tra i due elenchi.

6. Creazione di nuove attività in corso...

Ora implementiamo una funzionalità per la creazione di 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 "materiale" add accanto a un'etichetta "Aggiungi attività". Abbiamo bisogno del wrapper aggiuntivo per posizionare il pulsante in cima all'elenco di costumi da bagno, che posizioneremo successivamente uno accanto all'altro utilizzando flexbox. Poiché questo pulsante utilizza il componente Pulsante materiale, 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à nel AppComponent. Useremo una finestra di dialogo del materiale. Nella finestra di dialogo avremo un modulo con due campi: titolo e descrizione. Quando l'utente fa clic sul pulsante "Aggiungi attività", apriamo la finestra di dialogo e, quando l'utente invia il modulo, aggiungeremo l'attività appena creata all'elenco todo.

Diamo un'occhiata all'implementazione generale 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 di newTask:

  • Apri una nuova finestra di dialogo utilizzando il 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 sotto forma di dati. In TaskDialogComponent saremo in grado di ottenere un riferimento a questo oggetto dati.
  • Ci abboniamo all'evento di chiusura e aggiungiamo l'attività dall'oggetto result all'array todo.

Per assicurarti che funzioni, dobbiamo prima importare MatDialogModule nel 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 l'TaskDialogComponent. Passa alla directory src/app ed esegui:

ng generate component task-dialog

Per implementare la sua funzionalità, apri anzitutto src/app/task-dialog/task-dialog.component.html e sostituisci i relativi contenuti 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 riportato sopra, creiamo un modulo con due campi per title e description. Utilizziamo l'istruzione cdkFocusInput per mettere automaticamente a fuoco l'input title quando l'utente apre la finestra di dialogo.

Nota come il modello fa riferimento alla proprietà data del componente. Questo sarà lo stesso data che passeremo 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 l'associazione di dati bidirezionale con ngModel.

Quando l'utente fa clic sul pulsante OK, viene restituito automaticamente il risultato { task: data.task }, che è l'attività che abbiamo modificato utilizzando i campi del modulo nel modello sopra.

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 inserire anche il valore del provider associato al token MAT_DIALOG_DATA. Si tratta dell'oggetto dati che abbiamo passato al metodo aperto nella sezione precedente, AppComponent. Dichiariamo anche la proprietà privata backupTask, che è una copia dell'attività che abbiamo passato insieme all'oggetto dati.

Quando l'utente preme il pulsante Annulla, vengono ripristinate le possibili proprietà modificate di this.data.task e la finestra di dialogo viene chiusa, trasmettendo this.data come risultato.

Abbiamo fatto riferimento a due tipi, ma non abbiamo ancora dichiarato nulla: TaskDialogData e TaskDialogResult. All'interno di src/app/task-dialog/task-dialog.component.ts aggiungi le seguenti dichiarazioni in fondo al 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 che dobbiamo fare prima di poter abilitare questa funzionalità è importare alcuni moduli nella 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à", dovrebbe essere visualizzata la seguente interfaccia utente:

33bcb987fade2a87.png

7. Miglioramento degli stili dell'app

Per rendere l'applicazione più visivamente accattivante, miglioreremo il suo layout modificandone leggermente gli stili. Vogliamo posizionarli uno accanto all'altro. Vogliamo anche apportare piccole modifiche al pulsante "Aggiungi attività" e all'etichetta dell'elenco vuoto.

Apri src/app/app.component.css e aggiungi i seguenti stili alla fine:

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 riportato sopra, modifichiamo il layout della barra degli strumenti e della relativa etichetta. Ci assicuriamo inoltre che i contenuti siano allineati orizzontalmente impostando la larghezza su 1400px e il margine su auto. Quindi, utilizzando flexbox, abbiamo posizionato i costumi da bagno uno accanto all'altro e alla fine apportiamo alcune modifiche al modo in cui mostriamo le attività e gli elenchi vuoti.

Una volta ricaricata l'app, dovresti vedere la seguente interfaccia utente:

69225f0b1aa5cb50.png

Anche se abbiamo migliorato notevolmente gli stili dell'app, abbiamo ancora un problema fastidioso quando spostiamo attività:

f9aae712027624af.png

Quando iniziamo a trascinare l'attività "Acquista latte", vediamo due schede per la stessa attività: quella che stiamo trascinando e quella in piscina. Il formato Angular CDK ci fornisce i nomi delle classi CSS che possiamo utilizzare per risolvere questo problema.

Aggiungi le seguenti sostituzioni degli stili 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 trascini un elemento, il componente Angular CDK&C33 lo clona e lo inserisce nella posizione in cui posizioneremo l'originale. Per assicurarci che questo elemento non sia visibile, impostiamo la proprietà di opacità nella classe cdk-drag-placeholder, che verrà aggiunta al segnaposto.

Inoltre, quando rimuoviamo un elemento, il CDK aggiunge la classe cdk-drag-animating. Per mostrare un'animazione fluida invece di agganciare direttamente l'elemento, definiamo una transizione con durata 250ms.

Inoltre, ci teniamo a modificare leggermente gli 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à, apriamo TaskDialogComponent e compila i due campi del modulo con title e description dell'attività.

In TaskDialogComponent verrà aggiunto anche un pulsante Elimina. Quando un utente fa clic sul link, viene trasmessa un'istruzione di eliminazione che termina con AppComponent.

L'unica modifica che dobbiamo apportare in TaskDialogComponent è nel suo 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 di eliminazione del materiale. Facendo clic sull'utente, chiuderemo la finestra di dialogo e, di conseguenza, passeremo il valore letterale oggetto { task: data.task, delete: true }. Tieni presente inoltre che rendiamo il pulsante circolare utilizzando mat-fab, ne impostiamo il colore principale e lo mostriamo solo quando è attiva l'eliminazione dei dati della finestra di dialogo.

Il resto dell'implementazione della funzionalità di modifica ed eliminazione è disponibile 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 stringa con valori corrispondenti alle proprietà associate ai singoli bagnini.
  • L'attività corrente da modificare.

Nel corpo del metodo, per prima cosa apri un'istanza di TaskDialogComponent. Come data, trasmettiamo un valore letterale oggetto, che specifica l'attività da modificare, e abilita 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 su true (ad esempio, quando l'utente ha premuto il pulsante elimina), rimuoviamo l'attività dall'elenco corrispondente.
  • In alternativa, sostituiamo l'attività sull'indice con quella derivata dal risultato della finestra di dialogo.

9. Creazione di un nuovo progetto Firebase

Ora creiamo un nuovo progetto Firebase.

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 Firebase alla tua app, apri la directory radice dell'area di lavoro ed esegui:

ng add @angular/fire

Questo comando installa il pacchetto @angular/fire e ti pone alcune domande. Nel terminale, dovresti visualizzare:

9ba88c0d52d18d0.png

Nel frattempo, l'installazione apre una finestra del browser, perciò puoi autenticarti con il tuo account Firebase. Infine, ti chiede di scegliere un progetto Firebase e creare alcuni file sul disco.

Ora dobbiamo creare un database Firestore. Nella sezione "Cloud Firestore", fai clic su "Crea database".

1e4a08b5a2462956.png

Successivamente, crea un database in modalità di test:

ac1181b2c32049f9.png

Infine, seleziona un'area geografica:

34bb94cc542a0597.png

L'unica cosa che manca ora è 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.

c8253a20031de8a9.png

Nella sezione "Le tue app", seleziona un'app web &:

428a1abcd0f90b23.png

Registra quindi la tua applicazione e assicurati di attivare "Firebase Hosting":

586e44cb27dd8f39.png

Dopo aver fatto clic su "Registra app", puoi copiare la configurazione in src/environments/environment.ts:

e30f142d79cecf8f.png

Alla fine, il tuo file di configurazione dovrebbe essere simile a questo:

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 nostri 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é useremo 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 la modalità di inizializzazione degli array per nuoto:

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 recuperare i contenuti della raccolta direttamente dal database. Tieni presente che valueChanges restituisce un osservabile anziché un array; inoltre, specifichiamo che il campo id per i documenti in questa raccolta deve essere chiamato id per corrispondere al nome che utilizziamo nell'interfaccia di Task. L'osservabile restituito da valueChanges emette un insieme di attività ogni volta che viene modificato.

Poiché lavoriamo con osservabili invece di array, dobbiamo aggiornare il modo in cui aggiungiamo, rimuoviamo e modifichiamo le attività, nonché la funzionalità per spostare le attività tra piscine. Invece di cambiare gli array in memoria, utilizzeremo l'SDK Firebase per aggiornare i dati nel database.

Innanzitutto, esaminiamo l'aspetto del riordinamento. 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 in alto viene evidenziato il nuovo codice. Per spostare un'attività dalla piscina corrente a quella di destinazione, rimuoviamo l'attività dalla prima raccolta e aggiungila alla seconda. Dato che eseguiamo due operazioni che vogliamo rappresentare una sola, ovvero rendere operativa l'operazione, le eseguiamo in una transazione Firestore.

Adesso aggiorniamo il metodo editTask per usare Firestore. All'interno del Chiudi finestra di dialogo 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 manichiamo utilizzando l'SDK Firestore e lo eliminiamo o lo aggiorniamo.

Infine, dobbiamo aggiornare il metodo di creazione di 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 poterle visualizzare, dobbiamo aggiornare il modello della AppComponent. Devi sostituire ogni accesso delle proprietà todo, inProgress e done rispettivamente con todo | async, inProgress | async e done | async.

La pipe async sottoscrive automaticamente gli osservabili associati alle raccolte. Quando gli osservabili emettono un nuovo valore, Angular esegue automaticamente il rilevamento dei cambiamenti ed elabora l'array emesso.

Ad esempio, diamo un'occhiata alle modifiche da apportare alla piscina 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 trasmettiamo i dati all'istruzione cdkDropList, applichiamo la barra verticale async. È lo stesso all'interno dell'istruzione *ngIf, ma tieni presente che utilizziamo anche il concatenamento facoltativo (noto anche come operatore di navigazione sicura in Angular) quando accedi 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 un elemento simile al seguente:

dd7ee20c0a10ebe2.png

12. Migliorare gli aggiornamenti ottimistici

Nell'applicazione stiamo eseguendo aggiornamenti ottimizzati. Abbiamo la nostra fonte attendibile in Firestore, ma allo stesso tempo disponiamo di copie locali delle attività; quando uno qualsiasi degli osservabili associati alle raccolte emette, otteniamo una serie di attività. Quando un'azione dell'utente modifica lo stato, prima aggiorniamo i valori locali e poi propagamo la modifica a Firestore.

Quando spostiamo un'attività da una piscina all'altra, chiamiamo transferArrayItem,, che opera su istanze locali degli array che rappresentano le attività in ciascuna piscina. L'SDK Firebase considera questi array come immutabili, il che significa che la prossima volta che Angular esegue il rilevamento delle modifiche, riceveremo nuove istanze di esse, che restituiranno lo stato precedente prima che l'attività venga trasferita.

Allo stesso tempo, attiviamo un aggiornamento Firestore e l'SDK Firebase attiva un aggiornamento con i valori corretti, quindi in pochi millisecondi l'interfaccia utente torna allo stato corretto. In questo modo l'attività che abbiamo appena trasferito passa dal primo all'elenco successivo. Puoi vederla bene nella GIF sotto:

70b946eebfa6f316.gif

Il modo corretto per risolvere questo problema varia da applicazione a applicazione, ma in ogni caso dobbiamo assicurarci di mantenere uno stato coerente fino all'aggiornamento dei nostri dati.

Possiamo utilizzare BehaviorSubject, che aggrega l'osservatore originale che riceviamo da valueChanges. Come funziona, BehaviorSubject conserva un array modificabile che persiste l'aggiornamento da transferArrayItem.

Per implementare una correzione, basta aggiornare il 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 quello che facciamo nello snippet riportato sopra è creare un elemento BehaviorSubject, che emette un valore ogni volta che l'osservabile associato alla collezione cambia.

Tutto funziona come previsto, perché BehaviorSubject riutilizza l'array nelle chiamate 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, è sufficiente:

ng deploy

Questo comando:

  1. Crea la tua app con la sua configurazione di produzione, applicando ottimizzazioni in fase di compilazione.
  2. Esegui il deployment della tua app in Firebase Hosting.
  3. Output di un URL in modo da poter visualizzare l'anteprima del risultato.

14. Complimenti

Congratulazioni, hai completato correttamente una lavagna kanban con Angular e Firebase.

Hai creato un'interfaccia utente con tre colonne che rappresentano lo stato di attività diverse. Con la CDK Angular, hai implementato l'opzione di trascinamento delle attività nelle colonne. Utilizzando il materiale Angular, hai creato un modulo per creare nuove attività e modificare quelle esistenti. Ora hai imparato come utilizzare @angular/fire e hai spostato tutto lo stato dell'applicazione in Firestore. Infine, hai eseguito il deployment dell'applicazione in Firebase Hosting.

Qual è il passaggio successivo?

Ricorda che abbiamo eseguito il deployment dell'applicazione utilizzando configurazioni di test. Prima di eseguire il deployment dell'app in produzione, assicurati di aver configurato le autorizzazioni corrette. La procedura da seguire è descritta qui.

Al momento non conserviamo l'ordine delle singole attività in una determinata piscina. Per implementare questa funzionalità, puoi utilizzare un campo di ordine nel documento dell'attività e ordinare i dati in base a tale campo.

Inoltre, abbiamo creato la bacheca kanban per un solo utente, il che significa che abbiamo una singola kanban board per chiunque apra l'app. Per implementare bacheche separate per diversi utenti della tua app, dovrai cambiare la struttura del tuo database. Scopri le best practice di Firestore qui.